mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 14:52:16 +00:00
Bug 1896848 - Fallback to Android Photo Picker when no permissions granted. r=android-reviewers,jonalmeida
Differential Revision: https://phabricator.services.mozilla.com/D214094
This commit is contained in:
parent
8dc31f9fda
commit
8f68d07560
@ -8,6 +8,7 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Level
|
||||
import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Method
|
||||
import mozilla.components.concept.engine.prompt.PromptRequest.File
|
||||
import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection.Type
|
||||
import mozilla.components.concept.identitycredential.Account
|
||||
import mozilla.components.concept.identitycredential.Provider
|
||||
@ -443,3 +444,21 @@ sealed class PromptRequest(
|
||||
val onDismiss: () -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current [PromptRequest] is a request to pick an image.
|
||||
*
|
||||
* @return true if the current request is a request for selecting one or more images, false otherwise.
|
||||
*/
|
||||
fun PromptRequest?.isPhotoRequest(): Boolean {
|
||||
return this is File && mimeTypes.any { it.startsWith("image/") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current [PromptRequest] is a request to pick a video.
|
||||
*
|
||||
* @return true if the current request is a request for selecting one or more videos, false otherwise.
|
||||
*/
|
||||
fun PromptRequest?.isVideoRequest(): Boolean {
|
||||
return this is File && mimeTypes.any { it.startsWith("video/") }
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package mozilla.components.feature.prompts
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.VisibleForTesting.Companion.PRIVATE
|
||||
import androidx.core.view.isVisible
|
||||
@ -76,6 +77,7 @@ import mozilla.components.feature.prompts.facts.emitPromptDismissedFact
|
||||
import mozilla.components.feature.prompts.facts.emitPromptDisplayedFact
|
||||
import mozilla.components.feature.prompts.facts.emitSuccessfulAddressAutofillFormDetectedFact
|
||||
import mozilla.components.feature.prompts.facts.emitSuccessfulCreditCardAutofillFormDetectedFact
|
||||
import mozilla.components.feature.prompts.file.AndroidPhotoPicker
|
||||
import mozilla.components.feature.prompts.file.FilePicker
|
||||
import mozilla.components.feature.prompts.file.FileUploadsDirCleaner
|
||||
import mozilla.components.feature.prompts.identitycredential.DialogColors
|
||||
@ -203,6 +205,7 @@ class PromptFeature private constructor(
|
||||
private val addressDelegate: AddressDelegate = DefaultAddressDelegate(),
|
||||
private val fileUploadsDirCleaner: FileUploadsDirCleaner,
|
||||
onNeedToRequestPermissions: OnNeedToRequestPermissions,
|
||||
androidPhotoPicker: AndroidPhotoPicker?,
|
||||
) : LifecycleAwareFeature,
|
||||
PermissionsFeature,
|
||||
Prompter,
|
||||
@ -258,6 +261,7 @@ class PromptFeature private constructor(
|
||||
addressDelegate: AddressDelegate = DefaultAddressDelegate(),
|
||||
fileUploadsDirCleaner: FileUploadsDirCleaner,
|
||||
onNeedToRequestPermissions: OnNeedToRequestPermissions,
|
||||
androidPhotoPicker: AndroidPhotoPicker? = null,
|
||||
) : this(
|
||||
container = PromptContainer.Activity(activity),
|
||||
store = store,
|
||||
@ -286,6 +290,7 @@ class PromptFeature private constructor(
|
||||
passwordGeneratorColorsProvider = passwordGeneratorColorsProvider,
|
||||
creditCardDelegate = creditCardDelegate,
|
||||
addressDelegate = addressDelegate,
|
||||
androidPhotoPicker = androidPhotoPicker,
|
||||
)
|
||||
|
||||
constructor(
|
||||
@ -314,6 +319,7 @@ class PromptFeature private constructor(
|
||||
creditCardDelegate: CreditCardDelegate = object : CreditCardDelegate {},
|
||||
addressDelegate: AddressDelegate = DefaultAddressDelegate(),
|
||||
fileUploadsDirCleaner: FileUploadsDirCleaner,
|
||||
androidPhotoPicker: AndroidPhotoPicker? = null,
|
||||
onNeedToRequestPermissions: OnNeedToRequestPermissions,
|
||||
) : this(
|
||||
container = PromptContainer.Fragment(fragment),
|
||||
@ -330,8 +336,6 @@ class PromptFeature private constructor(
|
||||
isCreditCardAutofillEnabled = isCreditCardAutofillEnabled,
|
||||
isAddressAutofillEnabled = isAddressAutofillEnabled,
|
||||
loginExceptionStorage = loginExceptionStorage,
|
||||
fileUploadsDirCleaner = fileUploadsDirCleaner,
|
||||
onNeedToRequestPermissions = onNeedToRequestPermissions,
|
||||
loginDelegate = loginDelegate,
|
||||
suggestStrongPasswordDelegate = suggestStrongPasswordDelegate,
|
||||
isSuggestStrongPasswordEnabled = isSuggestStrongPasswordEnabled,
|
||||
@ -341,10 +345,21 @@ class PromptFeature private constructor(
|
||||
onSavedGeneratedPassword = onSavedGeneratedPassword,
|
||||
creditCardDelegate = creditCardDelegate,
|
||||
addressDelegate = addressDelegate,
|
||||
fileUploadsDirCleaner = fileUploadsDirCleaner,
|
||||
onNeedToRequestPermissions = onNeedToRequestPermissions,
|
||||
androidPhotoPicker = androidPhotoPicker,
|
||||
)
|
||||
|
||||
private val filePicker =
|
||||
FilePicker(container, store, customTabId, fileUploadsDirCleaner, onNeedToRequestPermissions)
|
||||
@VisibleForTesting
|
||||
// var for testing purposes
|
||||
internal var filePicker = FilePicker(
|
||||
container,
|
||||
store,
|
||||
customTabId,
|
||||
fileUploadsDirCleaner,
|
||||
androidPhotoPicker,
|
||||
onNeedToRequestPermissions,
|
||||
)
|
||||
|
||||
@VisibleForTesting(otherwise = PRIVATE)
|
||||
internal var loginPicker =
|
||||
@ -1206,6 +1221,16 @@ class PromptFeature private constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the result received from the Android photo picker.
|
||||
*
|
||||
* @param listOf An array of [Uri] objects representing the selected photos.
|
||||
*/
|
||||
|
||||
fun onAndroidPhotoPickerResult(uriList: Array<Uri>) {
|
||||
filePicker.onAndroidPhotoPickerResult(uriList)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// The PIN request code
|
||||
const val PIN_REQUEST = 303
|
||||
|
@ -0,0 +1,72 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package mozilla.components.feature.prompts.file
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import mozilla.components.feature.prompts.PromptFeature
|
||||
|
||||
/**
|
||||
* Provides functionality for picking photos from the device's gallery using native picker.
|
||||
*
|
||||
* @property context The application [Context].
|
||||
* @property singleMediaPicker An [ActivityResultLauncher] for picking a single photo.
|
||||
* @property multipleMediaPicker An [ActivityResultLauncher] for picking multiple photos.
|
||||
*/
|
||||
class AndroidPhotoPicker(
|
||||
val context: Context,
|
||||
val singleMediaPicker: ActivityResultLauncher<PickVisualMediaRequest>,
|
||||
val multipleMediaPicker: ActivityResultLauncher<PickVisualMediaRequest>,
|
||||
) {
|
||||
internal val isPhotoPickerAvailable =
|
||||
ActivityResultContracts.PickVisualMedia.isPhotoPickerAvailable(context)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Registers a photo picker activity launcher in single-select mode.
|
||||
* Note that you must call singleMediaPicker before the fragment is created.
|
||||
*
|
||||
* @param getFragment A function that returns the [Fragment] which hosts the file picker.
|
||||
* @param getPromptsFeature A function that returns the [PromptFeature]
|
||||
* that handles the result of the photo picker.
|
||||
* @return An [ActivityResultLauncher] for picking a single photo.
|
||||
*/
|
||||
fun singleMediaPicker(
|
||||
getFragment: () -> Fragment,
|
||||
getPromptsFeature: () -> PromptFeature?,
|
||||
): ActivityResultLauncher<PickVisualMediaRequest> {
|
||||
return getFragment.invoke()
|
||||
.registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
|
||||
uri?.let {
|
||||
getPromptsFeature.invoke()?.onAndroidPhotoPickerResult(arrayOf(uri))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a photo picker activity launcher in multi-select mode.
|
||||
* Note that you must call multipleMediaPicker before the fragment is created.
|
||||
*
|
||||
* @param getFragment A function that returns the [Fragment] which hosts the file picker.
|
||||
* @param getPromptsFeature A function that returns the [PromptFeature]
|
||||
* that handles the result of the photo picker.
|
||||
* @return An [ActivityResultLauncher] for picking multiple photos.
|
||||
*/
|
||||
fun multipleMediaPicker(
|
||||
getFragment: () -> Fragment,
|
||||
getPromptsFeature: () -> PromptFeature?,
|
||||
): ActivityResultLauncher<PickVisualMediaRequest> {
|
||||
return getFragment.invoke()
|
||||
.registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uriList ->
|
||||
uriList?.let {
|
||||
getPromptsFeature.invoke()?.onAndroidPhotoPickerResult(uriList.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,9 @@ import android.content.Intent.EXTRA_INITIAL_INTENTS
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore.EXTRA_OUTPUT
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.VisibleForTesting.Companion.PRIVATE
|
||||
import androidx.fragment.app.Fragment
|
||||
@ -19,6 +22,8 @@ import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.prompt.PromptRequest
|
||||
import mozilla.components.concept.engine.prompt.PromptRequest.File
|
||||
import mozilla.components.concept.engine.prompt.isPhotoRequest
|
||||
import mozilla.components.concept.engine.prompt.isVideoRequest
|
||||
import mozilla.components.feature.prompts.PromptContainer
|
||||
import mozilla.components.feature.prompts.consumePromptFrom
|
||||
import mozilla.components.support.base.feature.OnNeedToRequestPermissions
|
||||
@ -51,6 +56,8 @@ internal class FilePicker(
|
||||
private val store: BrowserStore,
|
||||
private var sessionId: String? = null,
|
||||
private var fileUploadsDirCleaner: FileUploadsDirCleaner,
|
||||
@get:VisibleForTesting
|
||||
internal var androidPhotoPicker: AndroidPhotoPicker? = null,
|
||||
override val onNeedToRequestPermissions: OnNeedToRequestPermissions,
|
||||
) : PermissionsFeature {
|
||||
|
||||
@ -218,11 +225,11 @@ internal class FilePicker(
|
||||
if (grantResults.isNotEmpty() && grantResults.any { it == PERMISSION_GRANTED }) {
|
||||
// at least one permission was granted
|
||||
onPermissionsGranted(currentRequest as File)
|
||||
currentRequest = null
|
||||
} else {
|
||||
// all permissions were denied, either when requested or already permanently denied
|
||||
onPermissionsDenied()
|
||||
}
|
||||
currentRequest = null
|
||||
}
|
||||
|
||||
/**
|
||||
@ -245,8 +252,68 @@ internal class FilePicker(
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal fun onPermissionsDenied() {
|
||||
// Nothing left to do. Consume / cleanup the requests.
|
||||
if (canUseAndroidPhotoPicker()) {
|
||||
launchAndroidPhotoPicker()
|
||||
} else {
|
||||
// Nothing left to do. Consume / cleanup the requests.
|
||||
dismissRequest()
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun canUseAndroidPhotoPicker(): Boolean {
|
||||
return androidPhotoPicker != null &&
|
||||
isPhotoOrVideoRequest(currentRequest) &&
|
||||
androidPhotoPicker?.isPhotoPickerAvailable == true
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun isPhotoOrVideoRequest(request: PromptRequest?): Boolean {
|
||||
return request.isPhotoRequest() || request.isVideoRequest()
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun getVisualMediaType(request: PromptRequest?): VisualMediaType {
|
||||
val mimeTypes = (request as? File)?.mimeTypes ?: emptyArray()
|
||||
|
||||
val isPhotoRequest = request.isPhotoRequest()
|
||||
val isVideoRequest = request.isVideoRequest()
|
||||
|
||||
if (mimeTypes.size == 1 && (isPhotoRequest || isVideoRequest)) {
|
||||
return ActivityResultContracts.PickVisualMedia.SingleMimeType(
|
||||
mimeTypes[0],
|
||||
)
|
||||
}
|
||||
|
||||
return when {
|
||||
isPhotoRequest && isVideoRequest -> ActivityResultContracts.PickVisualMedia.ImageAndVideo
|
||||
isPhotoRequest -> ActivityResultContracts.PickVisualMedia.ImageOnly
|
||||
isVideoRequest -> ActivityResultContracts.PickVisualMedia.VideoOnly
|
||||
else -> throw IllegalStateException(
|
||||
"Unexpected state: getVisualMediaType should only be called if isPhotoOrVideoRequest is true",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchAndroidPhotoPicker() {
|
||||
if ((currentRequest as File).isMultipleFilesSelection) {
|
||||
androidPhotoPicker?.multipleMediaPicker?.launch(
|
||||
PickVisualMediaRequest(
|
||||
getVisualMediaType(currentRequest),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
androidPhotoPicker?.singleMediaPicker?.launch(
|
||||
PickVisualMediaRequest(
|
||||
getVisualMediaType(currentRequest),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissRequest() {
|
||||
store.consumePromptFrom<File>(sessionId) { request ->
|
||||
currentRequest = null
|
||||
request.onDismiss()
|
||||
}
|
||||
}
|
||||
@ -298,6 +365,16 @@ internal class FilePicker(
|
||||
fileUploadsDirCleaner.enqueueForCleanup(fileName)
|
||||
}
|
||||
|
||||
fun onAndroidPhotoPickerResult(uriList: Array<Uri>) {
|
||||
if (uriList.size == 1) {
|
||||
(currentRequest as? File)?.onSingleFileSelected?.let { it(container.context, uriList[0]) }
|
||||
} else {
|
||||
(currentRequest as? File)?.onMultipleFilesSelected?.let { it(container.context, uriList) }
|
||||
}
|
||||
|
||||
dismissRequest()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FILE_PICKER_ACTIVITY_REQUEST_CODE = 7113
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -10,8 +10,12 @@ import mozilla.components.concept.engine.prompt.PromptRequest.Confirm
|
||||
import mozilla.components.concept.engine.prompt.PromptRequest.Popup
|
||||
import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
|
||||
import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt
|
||||
import mozilla.components.concept.engine.prompt.isPhotoRequest
|
||||
import mozilla.components.concept.engine.prompt.isVideoRequest
|
||||
import mozilla.components.support.test.mock
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@ -52,4 +56,52 @@ class PromptRequestTest {
|
||||
|
||||
assertEquals(0, invocations)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPhotoRequest returns true when mime types contain image`() {
|
||||
val request = PromptRequest.File(
|
||||
arrayOf("image/png"),
|
||||
onSingleFileSelected = { _, _ -> },
|
||||
onMultipleFilesSelected = { _, _ -> },
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
assertTrue(request.isPhotoRequest())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPhotoRequest returns false when mime types do not contain image`() {
|
||||
val request = PromptRequest.File(
|
||||
arrayOf("video/mp4"),
|
||||
onSingleFileSelected = { _, _ -> },
|
||||
onMultipleFilesSelected = { _, _ -> },
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
assertFalse(request.isPhotoRequest())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isVideoRequest returns true when mime types contain video`() {
|
||||
val request = PromptRequest.File(
|
||||
arrayOf("video/mp4"),
|
||||
onSingleFileSelected = { _, _ -> },
|
||||
onMultipleFilesSelected = { _, _ -> },
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
assertTrue(request.isVideoRequest())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isVideoRequest returns false when mime types do not contain video`() {
|
||||
val request = PromptRequest.File(
|
||||
arrayOf("image/png"),
|
||||
onSingleFileSelected = { _, _ -> },
|
||||
onMultipleFilesSelected = { _, _ -> },
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
assertFalse(request.isVideoRequest())
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,134 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package mozilla.components.feature.prompts.file
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import mozilla.components.feature.prompts.PromptFeature
|
||||
import mozilla.components.support.test.argumentCaptor
|
||||
import mozilla.components.support.test.whenever
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AndroidPhotoPickerTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var fragment: Fragment
|
||||
private lateinit var packageManager: PackageManager
|
||||
private lateinit var singleMediaPicker: ActivityResultLauncher<PickVisualMediaRequest>
|
||||
private lateinit var multipleMediaPicker: ActivityResultLauncher<PickVisualMediaRequest>
|
||||
private lateinit var promptFeature: PromptFeature
|
||||
private lateinit var androidPhotoPicker: AndroidPhotoPicker
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
context = mock()
|
||||
fragment = mock()
|
||||
singleMediaPicker = mock()
|
||||
multipleMediaPicker = mock()
|
||||
promptFeature = mock()
|
||||
|
||||
packageManager = mock()
|
||||
whenever(
|
||||
packageManager.resolveActivity(
|
||||
any(Intent::class.java),
|
||||
anyInt(),
|
||||
),
|
||||
).thenReturn(null)
|
||||
|
||||
whenever(
|
||||
fragment.registerForActivityResult(
|
||||
any<ActivityResultContracts.PickVisualMedia>(),
|
||||
any(),
|
||||
),
|
||||
).thenReturn(mock())
|
||||
|
||||
whenever(context.packageManager).thenReturn(packageManager)
|
||||
androidPhotoPicker = AndroidPhotoPicker(context, singleMediaPicker, multipleMediaPicker)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPhotoPickerAvailable returns true when photo picker is available`() {
|
||||
// on Android 10 and above the system framework provided photo picker should be available
|
||||
assertTrue(androidPhotoPicker.isPhotoPickerAvailable)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [28])
|
||||
fun `isPhotoPickerAvailable returns false when photo picker is not available`() {
|
||||
assertFalse(androidPhotoPicker.isPhotoPickerAvailable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `singleMediaPicker uses a proper ActivityResultContract`() {
|
||||
AndroidPhotoPicker.singleMediaPicker({ fragment }, { promptFeature })
|
||||
|
||||
val contractCaptor = argumentCaptor<ActivityResultContract<Intent, ActivityResult>>()
|
||||
val callbackCaptor = argumentCaptor<ActivityResultCallback<ActivityResult>>()
|
||||
|
||||
verify(fragment).registerForActivityResult(
|
||||
contractCaptor.capture(),
|
||||
callbackCaptor.capture(),
|
||||
)
|
||||
|
||||
assertTrue(contractCaptor.value is ActivityResultContracts.PickVisualMedia)
|
||||
assertNotNull(callbackCaptor.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multipleMediaPicker uses a proper ActivityResultContract`() {
|
||||
AndroidPhotoPicker.multipleMediaPicker({ fragment }, { promptFeature })
|
||||
|
||||
val contractCaptor = argumentCaptor<ActivityResultContract<Intent, ActivityResult>>()
|
||||
val callbackCaptor = argumentCaptor<ActivityResultCallback<ActivityResult>>()
|
||||
|
||||
verify(fragment).registerForActivityResult(
|
||||
contractCaptor.capture(),
|
||||
callbackCaptor.capture(),
|
||||
)
|
||||
|
||||
assertTrue(contractCaptor.value is ActivityResultContracts.PickMultipleVisualMedia)
|
||||
assertNotNull(callbackCaptor.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `singleMediaPicker returns a valid ActivityResultLauncher`() {
|
||||
val launcher = AndroidPhotoPicker.singleMediaPicker(
|
||||
{ fragment },
|
||||
{ promptFeature },
|
||||
)
|
||||
|
||||
assertNotNull(launcher)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multipleMediaPicker returns a valid ActivityResultLauncher`() {
|
||||
val launcher = AndroidPhotoPicker.multipleMediaPicker(
|
||||
{ fragment },
|
||||
{ promptFeature },
|
||||
)
|
||||
|
||||
assertNotNull(launcher)
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager.PERMISSION_DENIED
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.net.toUri
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
@ -40,6 +41,7 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.anyInt
|
||||
import org.mockito.Mockito.doNothing
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.spy
|
||||
@ -193,7 +195,7 @@ class FilePickerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onPermissionsDeny will call onDismiss and consume the file PromptRequest of the actual session`() {
|
||||
fun `onPermissionsDeny will call onDismiss and consume the file PromptRequest of the actual session if androidPhotoPicker is null`() {
|
||||
var onDismissWasCalled = false
|
||||
val filePickerRequest = request.copy {
|
||||
onDismissWasCalled = true
|
||||
@ -324,6 +326,8 @@ class FilePickerTest {
|
||||
@Test
|
||||
fun `onRequestPermissionsResult with FILE_PICKER_REQUEST and PERMISSION_DENIED will call onPermissionsDeny`() {
|
||||
filePicker = spy(filePicker)
|
||||
doNothing().`when`(filePicker).onPermissionsDenied()
|
||||
|
||||
filePicker.onPermissionsResult(emptyArray(), IntArray(1) { PERMISSION_DENIED })
|
||||
|
||||
verify(filePicker).onPermissionsDenied()
|
||||
@ -334,7 +338,12 @@ class FilePickerTest {
|
||||
val permissions = setOf("PermissionA")
|
||||
var permissionsRequested = emptyArray<String>()
|
||||
filePicker = spy(
|
||||
FilePicker(fragment, store, null, fileUploadsDirCleaner = mock()) { requested ->
|
||||
FilePicker(
|
||||
fragment,
|
||||
store,
|
||||
null,
|
||||
fileUploadsDirCleaner = mock(),
|
||||
) { requested ->
|
||||
permissionsRequested = requested
|
||||
},
|
||||
)
|
||||
@ -455,6 +464,177 @@ class FilePickerTest {
|
||||
assertNull(captureUri)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canUseAndroidPhotoPicker returns true when conditions are met`() {
|
||||
val mockAndroidPhotoPicker = mock<AndroidPhotoPicker>()
|
||||
|
||||
val filePickerSpy = spy(filePicker)
|
||||
filePickerSpy.currentRequest = request
|
||||
filePickerSpy.androidPhotoPicker = mockAndroidPhotoPicker
|
||||
|
||||
whenever(filePickerSpy.isPhotoOrVideoRequest(request)).thenReturn(true)
|
||||
whenever(mockAndroidPhotoPicker.isPhotoPickerAvailable).thenReturn(true)
|
||||
|
||||
val result = filePickerSpy.canUseAndroidPhotoPicker()
|
||||
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canUseAndroidPhotoPicker returns false when the request is not for photo or video`() {
|
||||
val mockAndroidPhotoPicker = mock<AndroidPhotoPicker>()
|
||||
|
||||
val filePickerSpy = spy(filePicker)
|
||||
filePickerSpy.currentRequest = request
|
||||
filePickerSpy.androidPhotoPicker = mockAndroidPhotoPicker
|
||||
|
||||
whenever(filePickerSpy.isPhotoOrVideoRequest(request)).thenReturn(false)
|
||||
whenever(mockAndroidPhotoPicker.isPhotoPickerAvailable).thenReturn(true)
|
||||
|
||||
val result = filePickerSpy.canUseAndroidPhotoPicker()
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canUseAndroidPhotoPicker returns false when photo picker is not available`() {
|
||||
val mockAndroidPhotoPicker = mock<AndroidPhotoPicker>()
|
||||
|
||||
val filePickerSpy = spy(filePicker)
|
||||
filePickerSpy.currentRequest = request
|
||||
filePickerSpy.androidPhotoPicker = mockAndroidPhotoPicker
|
||||
|
||||
whenever(filePickerSpy.isPhotoOrVideoRequest(request)).thenReturn(true)
|
||||
whenever(mockAndroidPhotoPicker.isPhotoPickerAvailable).thenReturn(false)
|
||||
|
||||
val result = filePickerSpy.canUseAndroidPhotoPicker()
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canUseAndroidPhotoPicker returns false when androidPhotoPicker is null`() {
|
||||
val mockAndroidPhotoPicker = mock<AndroidPhotoPicker>()
|
||||
|
||||
val filePickerSpy = spy(filePicker)
|
||||
filePickerSpy.currentRequest = request
|
||||
filePickerSpy.androidPhotoPicker = null
|
||||
|
||||
whenever(filePickerSpy.isPhotoOrVideoRequest(request)).thenReturn(true)
|
||||
whenever(mockAndroidPhotoPicker.isPhotoPickerAvailable).thenReturn(false)
|
||||
|
||||
val result = filePickerSpy.canUseAndroidPhotoPicker()
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPhotoOrVideoRequest returns true for image and video mime types`() {
|
||||
val request = PromptRequest.File(
|
||||
arrayOf("image/png", "video/mp4"),
|
||||
onSingleFileSelected = noopSingle,
|
||||
onMultipleFilesSelected = noopMulti,
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
val filePickerSpy = spy(filePicker)
|
||||
|
||||
val result = filePickerSpy.isPhotoOrVideoRequest(request)
|
||||
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isPhotoOrVideoRequest returns false for non-image and non-video mime types`() {
|
||||
val request = PromptRequest.File(
|
||||
arrayOf("application/pdf"),
|
||||
onSingleFileSelected = noopSingle,
|
||||
onMultipleFilesSelected = noopMulti,
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
val filePickerSpy = spy(filePicker)
|
||||
|
||||
val result = filePickerSpy.isPhotoOrVideoRequest(request)
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getVisualMediaType returns SingleMimeType when mime types contain only one mime type`() {
|
||||
val request = PromptRequest.File(
|
||||
arrayOf("image/png"),
|
||||
onSingleFileSelected = { _, _ -> },
|
||||
onMultipleFilesSelected = { _, _ -> },
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
val result = filePicker.getVisualMediaType(request)
|
||||
|
||||
assertTrue(result is ActivityResultContracts.PickVisualMedia.SingleMimeType)
|
||||
assertEquals(
|
||||
"image/png",
|
||||
(result as ActivityResultContracts.PickVisualMedia.SingleMimeType).mimeType,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getVisualMediaType returns ImageAndVideo when mime types contain both image and video`() {
|
||||
val request = PromptRequest.File(
|
||||
arrayOf("image/png", "video/mp4"),
|
||||
onSingleFileSelected = { _, _ -> },
|
||||
onMultipleFilesSelected = { _, _ -> },
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
val result = filePicker.getVisualMediaType(request)
|
||||
|
||||
assertEquals(
|
||||
ActivityResultContracts.PickVisualMedia.ImageAndVideo,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getVisualMediaType returns ImageOnly when mime types contain only image`() {
|
||||
val request = PromptRequest.File(
|
||||
arrayOf("image/png", "image/jpeg"),
|
||||
onSingleFileSelected = { _, _ -> },
|
||||
onMultipleFilesSelected = { _, _ -> },
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
val result = filePicker.getVisualMediaType(request)
|
||||
|
||||
assertEquals(ActivityResultContracts.PickVisualMedia.ImageOnly, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getVisualMediaType returns VideoOnly when mime types contain only video`() {
|
||||
val request = PromptRequest.File(
|
||||
arrayOf("video/mp4", "video/avi"),
|
||||
onSingleFileSelected = { _, _ -> },
|
||||
onMultipleFilesSelected = { _, _ -> },
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
val result = filePicker.getVisualMediaType(request)
|
||||
|
||||
assertEquals(ActivityResultContracts.PickVisualMedia.VideoOnly, result)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun `getVisualMediaType throws IllegalStateException when mime types do not contain image or video`() {
|
||||
val request = PromptRequest.File(
|
||||
arrayOf("application/pdf"),
|
||||
onSingleFileSelected = { _, _ -> },
|
||||
onMultipleFilesSelected = { _, _ -> },
|
||||
onDismiss = {},
|
||||
)
|
||||
|
||||
filePicker.getVisualMediaType(request)
|
||||
}
|
||||
|
||||
private fun prepareSelectedSession(request: PromptRequest? = null): TabSessionState {
|
||||
val promptRequest: PromptRequest = request ?: mock()
|
||||
val content: ContentState = mock()
|
||||
@ -469,6 +649,10 @@ class FilePickerTest {
|
||||
private fun stubContext() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
doReturn(context).`when`(fragment).context
|
||||
filePicker = FilePicker(fragment, store, fileUploadsDirCleaner = fileUploadsDirCleaner) {}
|
||||
filePicker = FilePicker(
|
||||
fragment,
|
||||
store,
|
||||
fileUploadsDirCleaner = fileUploadsDirCleaner,
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +88,7 @@ import mozilla.components.feature.prompts.PromptFeature.Companion.PIN_REQUEST
|
||||
import mozilla.components.feature.prompts.address.AddressDelegate
|
||||
import mozilla.components.feature.prompts.creditcard.CreditCardDelegate
|
||||
import mozilla.components.feature.prompts.dialog.FullScreenNotificationDialog
|
||||
import mozilla.components.feature.prompts.file.AndroidPhotoPicker
|
||||
import mozilla.components.feature.prompts.identitycredential.DialogColors
|
||||
import mozilla.components.feature.prompts.identitycredential.DialogColorsProvider
|
||||
import mozilla.components.feature.prompts.login.LoginDelegate
|
||||
@ -297,11 +298,33 @@ abstract class BaseBrowserFragment :
|
||||
|
||||
private lateinit var savedLoginsLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
// Registers a photo picker activity launcher in single-select mode.
|
||||
private val singleMediaPicker =
|
||||
AndroidPhotoPicker.singleMediaPicker(
|
||||
{ getFragment() },
|
||||
{ getPromptsFeature() },
|
||||
)
|
||||
|
||||
// Registers a photo picker activity launcher in multi-select mode.
|
||||
private val multipleMediaPicker =
|
||||
AndroidPhotoPicker.multipleMediaPicker(
|
||||
{ getFragment() },
|
||||
{ getPromptsFeature() },
|
||||
)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
savedLoginsLauncher = registerForActivityResult { navigateToSavedLoginsFragment() }
|
||||
}
|
||||
|
||||
private fun getFragment(): Fragment {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getPromptsFeature(): PromptFeature? {
|
||||
return promptsFeature.get()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@ -901,6 +924,11 @@ abstract class BaseBrowserFragment :
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
},
|
||||
androidPhotoPicker = AndroidPhotoPicker(
|
||||
requireContext(),
|
||||
singleMediaPicker,
|
||||
multipleMediaPicker,
|
||||
),
|
||||
),
|
||||
owner = this,
|
||||
view = view,
|
||||
|
@ -23,6 +23,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
@ -51,6 +52,7 @@ import mozilla.components.feature.downloads.manager.FetchDownloadManager
|
||||
import mozilla.components.feature.downloads.temporary.ShareDownloadFeature
|
||||
import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature
|
||||
import mozilla.components.feature.prompts.PromptFeature
|
||||
import mozilla.components.feature.prompts.file.AndroidPhotoPicker
|
||||
import mozilla.components.feature.session.PictureInPictureFeature
|
||||
import mozilla.components.feature.session.SessionFeature
|
||||
import mozilla.components.feature.sitepermissions.SitePermissionsFeature
|
||||
@ -167,6 +169,28 @@ class BrowserFragment :
|
||||
// Workaround for tab not existing temporarily.
|
||||
?: createTab("about:blank")
|
||||
|
||||
// Registers a photo picker activity launcher in single-select mode.
|
||||
private val singleMediaPicker =
|
||||
AndroidPhotoPicker.singleMediaPicker(
|
||||
{ getFragment() },
|
||||
{ getPromptFeature() },
|
||||
)
|
||||
|
||||
// Registers a photo picker activity launcher in multi-select mode.
|
||||
private val multipleMediaPicker =
|
||||
AndroidPhotoPicker.multipleMediaPicker(
|
||||
{ getFragment() },
|
||||
{ getPromptFeature() },
|
||||
)
|
||||
|
||||
private fun getFragment(): Fragment {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getPromptFeature(): PromptFeature? {
|
||||
return promptFeature.get()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
requestPermissionLauncher =
|
||||
@ -339,6 +363,11 @@ class BrowserFragment :
|
||||
)
|
||||
}
|
||||
},
|
||||
androidPhotoPicker = AndroidPhotoPicker(
|
||||
requireContext(),
|
||||
singleMediaPicker,
|
||||
multipleMediaPicker,
|
||||
),
|
||||
),
|
||||
this,
|
||||
view,
|
||||
|
Loading…
Reference in New Issue
Block a user