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())
+ }
+}