Add PickVisualMedia & PickMultipleVisualMedia ActivityResultContracts
Add contracts to support the photo picker when available and rely on ACTION_OPEN_DOCUMENT otherwise
Test: Added integration tests
Change-Id: Idab9a0dc5410e4c34ebc2ca9d8af727f5acf4216
diff --git a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
index 9af89ed..52d69d3 100644
--- a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
+++ b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
@@ -22,6 +22,7 @@
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
+import android.os.ext.SdkExtensions.getExtensionVersion
import android.provider.ContactsContract
import android.provider.DocumentsContract
import android.provider.MediaStore
@@ -597,4 +598,158 @@
return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
}
}
+
+ /**
+ * An [ActivityResultContract] to use the photo picker through [MediaStore.ACTION_PICK_IMAGES]
+ * when available, and else rely on ACTION_OPEN_DOCUMENT.
+ *
+ * The input is the mime type to filter by, e.g. `PickVisualMedia.ImageOnly`,
+ * `PickVisualMedia.ImageAndVideo`, `PickVisualMedia.SingleMimeType("image/gif")`.
+ *
+ * The output is a `Uri` when the user has selected a media or `null` when the user hasn't
+ * selected any item. Keep in mind that `Uri` returned by the photo picker isn't writable.
+ *
+ * This can be extended to override [createIntent] if you wish to pass additional
+ * extras to the Intent created by `super.createIntent()`.
+ */
+ open class PickVisualMedia : ActivityResultContract<PickVisualMedia.VisualMediaType, Uri?>() {
+ companion object {
+ @JvmStatic
+ fun isPhotoPickerAvailable(): Boolean {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ||
+ getExtensionVersion(Build.VERSION_CODES.R) >= 2
+ }
+
+ @JvmStatic
+ fun getVisualMimeType(input: VisualMediaType): String? {
+ return when (input) {
+ is ImageOnly -> "image/*"
+ is VideoOnly -> "video/*"
+ is SingleMimeType -> input.mimeType
+ is ImageAndVideo -> null
+ }
+ }
+ }
+
+ sealed interface VisualMediaType
+ object ImageOnly : VisualMediaType
+ object VideoOnly : VisualMediaType
+ object ImageAndVideo : VisualMediaType
+ class SingleMimeType(val mimeType: String) : VisualMediaType
+
+ @CallSuper
+ override fun createIntent(context: Context, input: VisualMediaType): Intent {
+ // Check if Photo Picker is available on the device
+ return if (isPhotoPickerAvailable()) {
+ Intent(MediaStore.ACTION_PICK_IMAGES).apply {
+ type = getVisualMimeType(input)
+ }
+ } else {
+ // For older devices running KitKat and higher and devices running Android 12
+ // and 13 without the SDK extension that includes the Photo Picker, rely on the
+ // ACTION_OPEN_DOCUMENT intent
+ Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ type = getVisualMimeType(input)
+
+ if (type == null) {
+ // ACTION_OPEN_DOCUMENT requires to set this parameter when launching the
+ // intent with multiple mime types
+ type = "*/*"
+ putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
+ }
+ }
+ }
+ }
+
+ final override fun getSynchronousResult(
+ context: Context,
+ input: VisualMediaType
+ ): SynchronousResult<Uri?>? = null
+
+ final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
+ return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
+ }
+ }
+
+ /**
+ * An [ActivityResultContract] to use the Photo Picker through [MediaStore.ACTION_PICK_IMAGES]
+ * when available, and else rely on ACTION_OPEN_DOCUMENT.
+ *
+ * The constructor accepts one parameter `maxItems` to limit the number of selectable items when
+ * using the photo picker to return. Keep in mind that this parameter isn't supported on devices
+ * when the photo picker isn't available.
+ *
+ * The input is the mime type to filter by, e.g. `PickVisualMedia.ImageOnly`,
+ * `PickVisualMedia.ImageAndVideo`, `PickVisualMedia.SingleMimeType("image/gif")`.
+ *
+ * The output is a list `Uri` of the selected media. It can be empty if the user hasn't selected
+ * any items. Keep in mind that `Uri` returned by the photo picker aren't writable.
+ *
+ * This can be extended to override [createIntent] if you wish to pass additional
+ * extras to the Intent created by `super.createIntent()`.
+ */
+ @RequiresApi(19)
+ open class PickMultipleVisualMedia(
+ private val maxItems: Int = getMaxItems()
+ ) : ActivityResultContract<PickVisualMedia.VisualMediaType, List<@JvmSuppressWildcards Uri>>() {
+
+ init {
+ require(maxItems > 1) {
+ "Max items must be higher than 1"
+ }
+ }
+
+ @CallSuper
+ override fun createIntent(
+ context: Context,
+ input: PickVisualMedia.VisualMediaType
+ ): Intent {
+ // Check to see if the photo picker is available
+ return if (PickVisualMedia.isPhotoPickerAvailable()) {
+ Intent(MediaStore.ACTION_PICK_IMAGES).apply {
+ type = PickVisualMedia.getVisualMimeType(input)
+
+ require(maxItems <= MediaStore.getPickImagesMaxLimit()) {
+ "Max items must be less or equals MediaStore.getPickImagesMaxLimit()"
+ }
+
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxItems)
+ }
+ } else {
+ // For older devices running KitKat and higher and devices running Android 12
+ // and 13 without the SDK extension that includes the Photo Picker, rely on the
+ // ACTION_OPEN_DOCUMENT intent
+ Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ type = PickVisualMedia.getVisualMimeType(input)
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+
+ if (type == null) {
+ // ACTION_OPEN_DOCUMENT requires to set this parameter when launching the
+ // intent with multiple mime types
+ type = "*/*"
+ putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
+ }
+ }
+ }
+ }
+
+ final override fun getSynchronousResult(
+ context: Context,
+ input: PickVisualMedia.VisualMediaType
+ ): SynchronousResult<List<@JvmSuppressWildcards Uri>>? = null
+
+ final override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> {
+ return intent.takeIf {
+ resultCode == Activity.RESULT_OK
+ }?.getClipDataUris() ?: emptyList()
+ }
+
+ internal companion object {
+ internal fun getMaxItems() = if (PickVisualMedia.isPhotoPickerAvailable()) {
+ MediaStore.getPickImagesMaxLimit()
+ } else {
+ Integer.MAX_VALUE
+ }
+ }
+ }
}
diff --git a/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt b/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
index 11780b3..a68119b 100644
--- a/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
+++ b/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
@@ -41,9 +41,12 @@
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.activity.result.contract.ActivityResultContracts.GetContent
import androidx.activity.result.contract.ActivityResultContracts.OpenMultipleDocuments
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
+import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.activity.result.contract.ActivityResultContracts.TakePicture
import androidx.activity.result.contract.ActivityResultContracts.TakePicturePreview
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType
import androidx.activity.result.launch
import androidx.activity.result.registerForActivityResult
import androidx.core.content.FileProvider
@@ -77,6 +80,10 @@
toast("Got image: $uri")
}
+ lateinit var pickVisualMedia: ActivityResultLauncher<VisualMediaType>
+
+ lateinit var pickMultipleVisualMedia: ActivityResultLauncher<VisualMediaType>
+
lateinit var createDocument: ActivityResultLauncher<String>
lateinit var openDocuments: ActivityResultLauncher<Array<String>>
@@ -92,6 +99,17 @@
super.onCreate(savedInstanceState)
if (android.os.Build.VERSION.SDK_INT >= 19) {
+ pickVisualMedia = registerForActivityResult(PickVisualMedia()) { uri ->
+ toast("Got image: $uri")
+ }
+ pickMultipleVisualMedia =
+ registerForActivityResult(PickMultipleVisualMedia(5)) { uris ->
+ var media = ""
+ uris.forEach {
+ media += "uri: $it \n"
+ }
+ toast("Got media files: $media")
+ }
createDocument = registerForActivityResult(CreateDocument("image/png")) { uri ->
toast("Created document: $uri")
}
@@ -124,10 +142,19 @@
val uri = FileProvider.getUriForFile(this@MainActivity, packageName, file)
captureVideo.launch(uri)
}
- button("Pick an image") {
+ button("Pick an image (w/ GET_CONTENT)") {
getContent.launch("image/*")
}
if (android.os.Build.VERSION.SDK_INT >= 19) {
+ button("Pick an image (w/ photo picker)") {
+ pickVisualMedia.launch(PickVisualMedia.ImageOnly)
+ }
+ button("Pick a GIF (w/ photo picker)") {
+ pickVisualMedia.launch(PickVisualMedia.SingleMimeType("image/gif"))
+ }
+ button("Pick 5 visual media max (w/ photo picker)") {
+ pickMultipleVisualMedia.launch(PickVisualMedia.ImageAndVideo)
+ }
button("Create document") {
createDocument.launch("Temp")
}