Glimpse: Implement ACTION_REVIEW properly
* With this intent, we're expecting an external content URI, treat it as
one
Change-Id: I790531850a6bc6be6d946ae3b0dd4f15d97ebceb
diff --git a/app/src/main/java/org/lineageos/glimpse/ViewActivity.kt b/app/src/main/java/org/lineageos/glimpse/ViewActivity.kt
index a82ab5b..5e3eb13 100644
--- a/app/src/main/java/org/lineageos/glimpse/ViewActivity.kt
+++ b/app/src/main/java/org/lineageos/glimpse/ViewActivity.kt
@@ -8,9 +8,11 @@
import android.content.Intent
import android.net.Uri
import android.os.Bundle
+import android.provider.MediaStore
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.os.bundleOf
import androidx.core.view.WindowCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -19,6 +21,7 @@
import okhttp3.OkHttpClient
import okhttp3.Request
import org.lineageos.glimpse.fragments.MediaViewerFragment
+import org.lineageos.glimpse.models.Media
import org.lineageos.glimpse.models.MediaType
import org.lineageos.glimpse.models.MediaUri
@@ -49,56 +52,177 @@
private fun handleIntent(intent: Intent) {
ioScope.launch {
- val uri = intent.data ?: run {
- runOnUiThread {
- Toast.makeText(
- this@ViewActivity, R.string.intent_media_not_found, Toast.LENGTH_SHORT
- ).show()
- finish()
- }
-
- return@launch
- }
-
- val dataType = intent.type ?: getContentType(uri) ?: run {
- runOnUiThread {
+ when (intent.action) {
+ Intent.ACTION_VIEW -> handleView(intent)
+ MediaStore.ACTION_REVIEW,
+ MediaStore.ACTION_REVIEW_SECURE -> handleReview(intent)
+ else -> runOnUiThread {
Toast.makeText(
this@ViewActivity,
- R.string.intent_media_type_not_found,
+ R.string.intent_action_not_supported,
Toast.LENGTH_SHORT
).show()
finish()
}
+ }
+ }
+ }
- return@launch
+ /**
+ * Handle a [Intent.ACTION_VIEW] intent (view a single media, controls also read-only).
+ * Must be executed on [ioScope].
+ * @param intent The received intent
+ */
+ private fun handleView(intent: Intent) {
+ val uri = intent.data ?: run {
+ runOnUiThread {
+ Toast.makeText(
+ this@ViewActivity,
+ R.string.intent_media_not_found,
+ Toast.LENGTH_SHORT
+ ).show()
+ finish()
}
- val uriType = MediaType.fromMimeType(dataType) ?: run {
- runOnUiThread {
- Toast.makeText(
- this@ViewActivity,
- R.string.intent_media_type_not_supported,
- Toast.LENGTH_SHORT
- ).show()
- finish()
- }
+ return
+ }
- return@launch
+ val dataType = intent.type ?: getContentType(uri) ?: run {
+ runOnUiThread {
+ Toast.makeText(
+ this@ViewActivity,
+ R.string.intent_media_type_not_found,
+ Toast.LENGTH_SHORT
+ ).show()
+ finish()
}
+ return
+ }
+
+ val uriType = MediaType.fromMimeType(dataType) ?: run {
+ runOnUiThread {
+ Toast.makeText(
+ this@ViewActivity,
+ R.string.intent_media_type_not_supported,
+ Toast.LENGTH_SHORT
+ ).show()
+ finish()
+ }
+
+ return
+ }
+
+ runOnUiThread {
+ supportFragmentManager
+ .beginTransaction()
+ .replace(
+ R.id.navHostFragment, MediaViewerFragment.newInstance(
+ null, null, MediaUri(uri, uriType, dataType)
+ )
+ )
+ .commit()
+ }
+ }
+
+ /**
+ * Handle a [MediaStore.ACTION_REVIEW] / [MediaStore.ACTION_REVIEW_SECURE] intent
+ * (view a media together with medias from the same bucket ID).
+ * If uri parsing from [MediaStore] fails, fallback to [handleView].
+ * Must be executed on [ioScope].
+ * @param intent The received intent
+ */
+ private fun handleReview(intent: Intent) {
+ intent.data?.let { getMediaStoreMedia(it) }?.also {
runOnUiThread {
supportFragmentManager
.beginTransaction()
.replace(
R.id.navHostFragment, MediaViewerFragment.newInstance(
- null, null, MediaUri(uri, uriType, dataType)
+ it, it.bucketId, null
)
)
.commit()
}
- }
+ } ?: handleView(intent)
}
+ /**
+ * Given a [MediaStore] [Uri], parse its information and get a [Media] object.
+ * Must be executed on [ioScope].
+ * @param uri The [MediaStore] [Uri]
+ */
+ private fun getMediaStoreMedia(uri: Uri) = runCatching {
+ contentResolver.query(
+ uri,
+ arrayOf(
+ MediaStore.MediaColumns._ID,
+ MediaStore.MediaColumns.BUCKET_ID,
+ MediaStore.MediaColumns.DISPLAY_NAME,
+ MediaStore.MediaColumns.IS_FAVORITE,
+ MediaStore.MediaColumns.IS_TRASHED,
+ MediaStore.MediaColumns.MIME_TYPE,
+ MediaStore.MediaColumns.DATE_ADDED,
+ MediaStore.MediaColumns.DATE_MODIFIED,
+ MediaStore.MediaColumns.WIDTH,
+ MediaStore.MediaColumns.HEIGHT,
+ MediaStore.MediaColumns.ORIENTATION,
+ ),
+ bundleOf(),
+ null,
+ )?.use {
+ val idIndex = it.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
+ val bucketIdIndex = it.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID)
+ val displayNameIndex = it.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
+ val isFavoriteIndex = it.getColumnIndexOrThrow(MediaStore.MediaColumns.IS_FAVORITE)
+ val isTrashedIndex = it.getColumnIndexOrThrow(MediaStore.MediaColumns.IS_TRASHED)
+ val mimeTypeIndex = it.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE)
+ val dateAddedIndex = it.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
+ val dateModifiedIndex =
+ it.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
+ val widthIndex = it.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
+ val heightIndex = it.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
+ val orientationIndex =
+ it.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
+
+ if (it.count != 1) {
+ return@use null
+ }
+
+ it.moveToFirst()
+
+ val id = it.getLong(idIndex)
+ val bucketId = it.getInt(bucketIdIndex)
+ val displayName = it.getString(displayNameIndex)
+ val isFavorite = it.getInt(isFavoriteIndex)
+ val isTrashed = it.getInt(isTrashedIndex)
+ val mediaType = contentResolver.getType(uri)?.let { type ->
+ MediaType.fromMimeType(type)
+ } ?: return@use null
+ val mimeType = it.getString(mimeTypeIndex)
+ val dateAdded = it.getLong(dateAddedIndex)
+ val dateModified = it.getLong(dateModifiedIndex)
+ val width = it.getInt(widthIndex)
+ val height = it.getInt(heightIndex)
+ val orientation = it.getInt(orientationIndex)
+
+ Media.fromMediaStore(
+ id,
+ bucketId,
+ displayName,
+ isFavorite,
+ isTrashed,
+ mediaType.mediaStoreValue,
+ mimeType,
+ dateAdded,
+ dateModified,
+ width,
+ height,
+ orientation,
+ )
+ }
+ }.getOrNull()
+
private fun getContentType(uri: Uri) = when (uri.scheme) {
"content" -> contentResolver.getType(uri)
diff --git a/app/src/main/java/org/lineageos/glimpse/models/MediaType.kt b/app/src/main/java/org/lineageos/glimpse/models/MediaType.kt
index c116795..d6d8836 100644
--- a/app/src/main/java/org/lineageos/glimpse/models/MediaType.kt
+++ b/app/src/main/java/org/lineageos/glimpse/models/MediaType.kt
@@ -10,15 +10,20 @@
enum class MediaType(
val externalContentUri: Uri,
+ val mediaStoreValue: Int,
) {
- IMAGE(MediaStore.Images.Media.EXTERNAL_CONTENT_URI),
- VIDEO(MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
+ IMAGE(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE,
+ ),
+ VIDEO(
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO,
+ );
companion object {
- fun fromMediaStoreValue(value: Int) = when (value) {
- MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> IMAGE
- MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> VIDEO
- else -> throw Exception("Unknown value $value")
+ fun fromMediaStoreValue(value: Int) = values().first {
+ value == it.mediaStoreValue
}
fun fromMimeType(mimeType: String) = when {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c7eba06..d2d5067 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -76,6 +76,7 @@
<string name="media_info_location_open_with">View the location with</string>
<!-- View activity -->
+ <string name="intent_action_not_supported">Action not supported</string>
<string name="intent_media_not_found">Media not found</string>
<string name="intent_media_type_not_found">Media type not found</string>
<string name="intent_media_type_not_supported">Media type not supported</string>