[Media upload] Media pre-processing (#403)

* Create `mediaupload` module for media pre-processing.

* Split `mediapicker` and `mediaupload` modules.
This commit is contained in:
Jorge Martin Espinosa 2023-05-10 10:06:56 +02:00 committed by GitHub
parent 1b39a782c5
commit cd298b9359
33 changed files with 1148 additions and 156 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,14 +16,16 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
}
android {
namespace = "io.element.android.libraries.mediapickers"
namespace = "io.element.android.libraries.mediapickers.api"
dependencies {
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.core)
implementation(projects.libraries.di)
implementation(libs.inject)
testImplementation(libs.test.junit)

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.libraries.mediapickers
package io.element.android.libraries.mediapickers.api
import androidx.activity.compose.ManagedActivityResultLauncher

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.mediapickers.api
import android.net.Uri
import androidx.activity.result.PickVisualMediaRequest
import androidx.compose.runtime.Composable
interface PickerProvider {
@Composable
fun registerGalleryPicker(
onResult: (uri: Uri?, mimeType: String?) -> Unit
): PickerLauncher<PickVisualMediaRequest, Uri?>
@Composable
fun registerFilePicker(
mimeType: String,
onResult: (Uri?) -> Unit
): PickerLauncher<String, Uri?>
@Composable
fun registerCameraPhotoPicker(onResult: (Uri?) -> Unit): PickerLauncher<Uri, Boolean>
@Composable
fun registerCameraVideoPicker(onResult: (Uri?) -> Unit): PickerLauncher<Uri, Boolean>
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.libraries.mediapickers
package io.element.android.libraries.mediapickers.api
import android.net.Uri
import androidx.activity.result.PickVisualMediaRequest

View file

@ -20,6 +20,7 @@ import android.net.Uri
import androidx.activity.result.contract.ActivityResultContracts
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.mediapickers.api.PickerType
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
}
android {
namespace = "io.element.android.libraries.mediapickers.impl"
dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
implementation(libs.inject)
api(projects.libraries.mediapickers.api)
}
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.libraries.mediapickers
package io.element.android.libraries.mediapickers.impl
import android.content.Context
import android.net.Uri
@ -25,12 +25,19 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.core.content.FileProvider
import io.element.android.libraries.core.mimetype.MimeTypes
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.mediapickers.api.ComposePickerLauncher
import io.element.android.libraries.mediapickers.api.NoOpPickerLauncher
import io.element.android.libraries.mediapickers.api.PickerLauncher
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediapickers.api.PickerType
import java.io.File
import java.util.UUID
import javax.inject.Inject
class PickerProvider constructor(private val isInTest: Boolean) {
@ContributesBinding(AppScope::class)
class PickerProviderImpl constructor(private val isInTest: Boolean) : PickerProvider {
@Inject
constructor(): this(false)
@ -57,12 +64,18 @@ class PickerProvider constructor(private val isInTest: Boolean) {
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
*/
@Composable
fun registerGalleryPicker(onResult: (Uri?) -> Unit): PickerLauncher<PickVisualMediaRequest, Uri?> {
override fun registerGalleryPicker(
onResult: (uri: Uri?, mimeType: String?) -> Unit
): PickerLauncher<PickVisualMediaRequest, Uri?> {
// Tests and UI preview can't handle Contexts, so we might as well disable the whole picker
return if (LocalInspectionMode.current || isInTest) {
NoOpPickerLauncher { onResult(null) }
NoOpPickerLauncher { onResult(null, null) }
} else {
rememberPickerLauncher(type = PickerType.ImageAndVideo) { uri -> onResult(uri) }
val context = LocalContext.current
rememberPickerLauncher(type = PickerType.ImageAndVideo) { uri ->
val mimeType = uri?.let { context.contentResolver.getType(it) }
onResult(uri, mimeType)
}
}
}
@ -71,7 +84,10 @@ class PickerProvider constructor(private val isInTest: Boolean) {
* [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.
*/
@Composable
fun registerFilePicker(mimeType: String = MimeTypes.Any, onResult: (Uri?) -> Unit): PickerLauncher<String, Uri?> {
override fun registerFilePicker(
mimeType: String,
onResult: (Uri?) -> Unit,
): PickerLauncher<String, Uri?> {
// Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker
return if (LocalInspectionMode.current || isInTest) {
NoOpPickerLauncher { onResult(null) }
@ -83,11 +99,9 @@ class PickerProvider constructor(private val isInTest: Boolean) {
/**
* Remembers and returns a [PickerLauncher] for taking a photo with a camera app.
* @param [onResult] will be called with either the photo's [Uri] or `null` if nothing was selected.
* @param [deleteAfter] When it's `true`, the taken photo will be automatically removed after calling [onResult].
* It's `true` by default.
*/
@Composable
fun registerCameraPhotoPicker(onResult: (Uri?) -> Unit, deleteAfter: Boolean = true): PickerLauncher<Uri, Boolean> {
override fun registerCameraPhotoPicker(onResult: (Uri?) -> Unit): PickerLauncher<Uri, Boolean> {
// Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker
return if (LocalInspectionMode.current || isInTest) {
NoOpPickerLauncher { onResult(null) }
@ -98,10 +112,6 @@ class PickerProvider constructor(private val isInTest: Boolean) {
rememberPickerLauncher(type = PickerType.Camera.Photo(tmpFileUri)) { success ->
// Execute callback
onResult(if (success) tmpFileUri else null)
// Then remove the file and clear the picker
if (deleteAfter) {
tmpFile.delete()
}
}
}
}
@ -109,11 +119,9 @@ class PickerProvider constructor(private val isInTest: Boolean) {
/**
* Remembers and returns a [PickerLauncher] for recording a video with a camera app.
* @param [onResult] will be called with either the video's [Uri] or `null` if nothing was selected.
* @param [deleteAfter] When it's `true`, the recorded video will be automatically removed after calling [onResult].
* It's `true` by default.
*/
@Composable
fun registerCameraVideoPicker(onResult: (Uri?) -> Unit, deleteAfter: Boolean = true): PickerLauncher<Uri, Boolean> {
override fun registerCameraVideoPicker(onResult: (Uri?) -> Unit): PickerLauncher<Uri, Boolean> {
// Tests and UI preview can't handle Context or FileProviders, so we might as well disable the whole picker
return if (LocalInspectionMode.current || isInTest) {
NoOpPickerLauncher { onResult(null) }
@ -124,10 +132,6 @@ class PickerProvider constructor(private val isInTest: Boolean) {
rememberPickerLauncher(type = PickerType.Camera.Video(tmpFileUri)) { success ->
// Execute callback
onResult(if (success) tmpFileUri else null)
// Then remove the file and clear the picker
if (deleteAfter) {
tmpFile.delete()
}
}
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
}
android {
namespace = "io.element.android.libraries.mediapickers.test"
dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
implementation(libs.inject)
api(projects.libraries.mediapickers.api)
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.mediapickers.test
import android.net.Uri
import androidx.activity.result.PickVisualMediaRequest
import androidx.compose.runtime.Composable
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.mediapickers.api.NoOpPickerLauncher
import io.element.android.libraries.mediapickers.api.PickerLauncher
import io.element.android.libraries.mediapickers.api.PickerProvider
class FakePickerProvider : PickerProvider {
private var mimeType = MimeTypes.Any
private var result: Uri? = null
@Composable
override fun registerGalleryPicker(onResult: (uri: Uri?, mimeType: String?) -> Unit): PickerLauncher<PickVisualMediaRequest, Uri?> {
return NoOpPickerLauncher { onResult(result, mimeType) }
}
@Composable
override fun registerFilePicker(mimeType: String, onResult: (Uri?) -> Unit): PickerLauncher<String, Uri?> {
return NoOpPickerLauncher { onResult(result) }
}
@Composable
override fun registerCameraPhotoPicker(onResult: (Uri?) -> Unit): PickerLauncher<Uri, Boolean> {
return NoOpPickerLauncher { onResult(result) }
}
@Composable
override fun registerCameraVideoPicker(onResult: (Uri?) -> Unit): PickerLauncher<Uri, Boolean> {
return NoOpPickerLauncher { onResult(result) }
}
fun givenResult(value: Uri?) {
this.result = value
}
fun givenMimeType(mimeType: String) {
this.mimeType = mimeType
}
}