diff --git a/demo-app/src/main/AndroidManifest.xml b/demo-app/src/main/AndroidManifest.xml index 85591c5e5..aa1ea1031 100644 --- a/demo-app/src/main/AndroidManifest.xml +++ b/demo-app/src/main/AndroidManifest.xml @@ -53,6 +53,15 @@ android:scheme="wp-oauth-test" /> + + + \ No newline at end of file diff --git a/demo-app/src/main/java/com/gravatar/demoapp/DemoFileProvider.kt b/demo-app/src/main/java/com/gravatar/demoapp/DemoFileProvider.kt new file mode 100644 index 000000000..d71871ad1 --- /dev/null +++ b/demo-app/src/main/java/com/gravatar/demoapp/DemoFileProvider.kt @@ -0,0 +1,5 @@ +package com.gravatar.demoapp + +import androidx.core.content.FileProvider + +internal class DemoFileProvider : FileProvider(R.xml.filepaths) diff --git a/demo-app/src/main/res/xml/filepaths.xml b/demo-app/src/main/res/xml/filepaths.xml new file mode 100644 index 000000000..2a7b51f99 --- /dev/null +++ b/demo-app/src/main/res/xml/filepaths.xml @@ -0,0 +1,6 @@ + + + + diff --git a/gravatar-quickeditor/src/main/AndroidManifest.xml b/gravatar-quickeditor/src/main/AndroidManifest.xml index 64d73c7d2..a31cc434c 100644 --- a/gravatar-quickeditor/src/main/AndroidManifest.xml +++ b/gravatar-quickeditor/src/main/AndroidManifest.xml @@ -17,6 +17,15 @@ android:name="com.gravatar.quickeditor.initializer.QuickEditorContainerInitializer" android:value="androidx.startup" /> + + + diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/QuickEditorFileProvider.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/QuickEditorFileProvider.kt new file mode 100644 index 000000000..9ca3d986c --- /dev/null +++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/QuickEditorFileProvider.kt @@ -0,0 +1,22 @@ +package com.gravatar.quickeditor + +import android.content.Context +import android.net.Uri +import androidx.core.content.FileProvider +import java.io.File + +internal class QuickEditorFileProvider : FileProvider(R.xml.quickeditor_filepaths) { + companion object { + fun getTempCameraImageUri(context: Context): Uri { + val directory = File(context.cacheDir, "quickeditor") + directory.mkdirs() + val file = File(directory, "temp_camera_image.jpg") + val authority = "${context.packageName}.com.quickeditor.fileprovider" + return getUriForFile( + context, + authority, + file, + ) + } + } +} diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/data/FileUtils.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/data/FileUtils.kt index e38b333e7..4ce6d488e 100644 --- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/data/FileUtils.kt +++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/data/FileUtils.kt @@ -8,8 +8,8 @@ import java.io.File internal class FileUtils( private val context: Context, ) { - fun createTempFile(): File { - return File(context.cacheDir, "gravatar_${System.currentTimeMillis()}") + fun createCroppedAvatarFile(): File { + return File(context.cacheDir, "cropped_avatar_${System.currentTimeMillis()}.jpg") } fun deleteFile(uri: Uri) { diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModel.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModel.kt index 74b042e59..62a0271d4 100644 --- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModel.kt +++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModel.kt @@ -71,7 +71,7 @@ internal class AvatarPickerViewModel( fun localImageSelected(imageUri: Uri) { viewModelScope.launch { - _actions.send(AvatarPickerAction.LaunchImageCropper(imageUri, fileUtils.createTempFile())) + _actions.send(AvatarPickerAction.LaunchImageCropper(imageUri, fileUtils.createCroppedAvatarFile())) } } diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/components/AvatarsSection.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/components/AvatarsSection.kt index 405c0d1d9..651f25949 100644 --- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/components/AvatarsSection.kt +++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/components/AvatarsSection.kt @@ -29,12 +29,14 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.gravatar.quickeditor.QuickEditorFileProvider import com.gravatar.quickeditor.R import com.gravatar.quickeditor.ui.avatarpicker.AvatarUi import com.gravatar.quickeditor.ui.avatarpicker.AvatarsSectionUiState @@ -50,14 +52,21 @@ internal fun AvatarsSection( onLocalImageSelected: (Uri) -> Unit, modifier: Modifier = Modifier, ) { + val context = LocalContext.current var popupVisible by rememberSaveable { mutableStateOf(false) } var popupYOffset by rememberSaveable { mutableIntStateOf(0) } + var photoImageUri by rememberSaveable { mutableStateOf(null) } val listState = rememberLazyListState() val pickMedia = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> uri?.let { onLocalImageSelected(it) } } + val takePhoto = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { success -> + val takenPictureUri = photoImageUri + if (success && takenPictureUri != null) onLocalImageSelected(takenPictureUri) + } + LaunchedEffect(state.scrollToIndex) { state.scrollToIndex?.let { listState.scrollToItem(it) } } @@ -133,7 +142,12 @@ internal fun AvatarsSection( popupVisible = false pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, - onTakePhotoClick = { popupVisible = false }, + onTakePhotoClick = { + popupVisible = false + val imageUri = QuickEditorFileProvider.getTempCameraImageUri(context) + photoImageUri = imageUri + takePhoto.launch(imageUri) + }, ) } } diff --git a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/copperlauncher/UCropCropperLauncher.kt b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/copperlauncher/UCropCropperLauncher.kt index c69d120c8..bd644048b 100644 --- a/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/copperlauncher/UCropCropperLauncher.kt +++ b/gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/copperlauncher/UCropCropperLauncher.kt @@ -9,7 +9,8 @@ import com.yalantis.ucrop.UCrop import com.yalantis.ucrop.UCropActivity import java.io.File -private const val UCROP_COMPRESSION_QUALITY = 100 +private const val UCROP_COMPRESSION_QUALITY = 75 +private const val UCROP_MAX_IMAGE_SIZE = 1080 internal class UCropCropperLauncher : CropperLauncher { override fun launch(launcher: ActivityResultLauncher, image: Uri, tempFile: File, context: Context) { @@ -19,6 +20,7 @@ internal class UCropCropperLauncher : CropperLauncher { setToolbarWidgetColor(Color.WHITE) setAllowedGestures(UCropActivity.SCALE, UCropActivity.ROTATE, UCropActivity.NONE) setCompressionQuality(UCROP_COMPRESSION_QUALITY) + withMaxResultSize(UCROP_MAX_IMAGE_SIZE, UCROP_MAX_IMAGE_SIZE) setCircleDimmedLayer(true) } launcher.launch( diff --git a/gravatar-quickeditor/src/main/res/xml/quickeditor_filepaths.xml b/gravatar-quickeditor/src/main/res/xml/quickeditor_filepaths.xml new file mode 100644 index 000000000..5293c2460 --- /dev/null +++ b/gravatar-quickeditor/src/main/res/xml/quickeditor_filepaths.xml @@ -0,0 +1,6 @@ + + + + diff --git a/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModelTest.kt b/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModelTest.kt index 2f2eabb03..9d5a8525b 100644 --- a/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModelTest.kt +++ b/gravatar-quickeditor/src/test/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPickerViewModelTest.kt @@ -267,7 +267,7 @@ class AvatarPickerViewModelTest { fun `given local image when selected then launch cropper action sent`() = runTest { val file = mockk() val uri = mockk() - every { fileUtils.createTempFile() } returns file + every { fileUtils.createCroppedAvatarFile() } returns file viewModel = initViewModel() diff --git a/gravatar/src/main/java/com/gravatar/di/container/GravatarSdkContainer.kt b/gravatar/src/main/java/com/gravatar/di/container/GravatarSdkContainer.kt index 8afa88be7..d6b9e7616 100644 --- a/gravatar/src/main/java/com/gravatar/di/container/GravatarSdkContainer.kt +++ b/gravatar/src/main/java/com/gravatar/di/container/GravatarSdkContainer.kt @@ -8,6 +8,7 @@ import com.gravatar.GravatarApiService import com.gravatar.GravatarConstants.GRAVATAR_API_BASE_URL_V1 import com.gravatar.GravatarConstants.GRAVATAR_API_BASE_URL_V3 import com.gravatar.services.AuthenticationInterceptor +import com.gravatar.services.AvatarUploadTimeoutInterceptor import com.gravatar.services.GravatarApi import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -65,12 +66,14 @@ internal class GravatarSdkContainer private constructor() { fun getGravatarV3Service(okHttpClient: OkHttpClient? = null, oauthToken: String? = null): GravatarApi { return getRetrofitApiV3Builder().apply { - client( - (okHttpClient ?: OkHttpClient()).newBuilder().addInterceptor( - AuthenticationInterceptor(oauthToken), - ).build(), - ) + client(okHttpClient ?: buildOkHttpClient(oauthToken)) }.addConverterFactory(GsonConverterFactory.create(gson)) .build().create(GravatarApi::class.java) } + + private fun buildOkHttpClient(oauthToken: String?) = OkHttpClient() + .newBuilder() + .addInterceptor(AuthenticationInterceptor(oauthToken)) + .addInterceptor(AvatarUploadTimeoutInterceptor()) + .build() } diff --git a/gravatar/src/main/java/com/gravatar/services/AvatarUploadTimeoutInterceptor.kt b/gravatar/src/main/java/com/gravatar/services/AvatarUploadTimeoutInterceptor.kt new file mode 100644 index 000000000..ad960bcc1 --- /dev/null +++ b/gravatar/src/main/java/com/gravatar/services/AvatarUploadTimeoutInterceptor.kt @@ -0,0 +1,27 @@ +package com.gravatar.services + +import okhttp3.Interceptor +import okhttp3.Response +import java.time.Duration +import java.util.concurrent.TimeUnit + +private const val TIMEOUT_DURATION = 5L + +/** + * Increases the timeout for the avatar upload request + */ +internal class AvatarUploadTimeoutInterceptor( + private val timeout: Duration = Duration.ofMinutes(TIMEOUT_DURATION), +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val newChain = if (request.method == "POST" && request.url.encodedPath == "/v3/me/avatars") { + chain.withConnectTimeout(timeout.toMillis().toInt(), TimeUnit.MILLISECONDS) + .withReadTimeout(timeout.toMillis().toInt(), TimeUnit.MILLISECONDS) + .withWriteTimeout(timeout.toMillis().toInt(), TimeUnit.MILLISECONDS) + } else { + chain + } + return newChain.proceed(chain.request().newBuilder().build()) + } +}