diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7d40e590..1e316fdc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,8 +5,11 @@
+
+
+
diff --git a/app/src/main/java/chat/revolt/components/media/InbuiltMediaPicker.kt b/app/src/main/java/chat/revolt/components/media/InbuiltMediaPicker.kt
index 731b6ccc..8fd0935e 100644
--- a/app/src/main/java/chat/revolt/components/media/InbuiltMediaPicker.kt
+++ b/app/src/main/java/chat/revolt/components/media/InbuiltMediaPicker.kt
@@ -1,12 +1,16 @@
package chat.revolt.components.media
+import android.Manifest
import android.content.ContentUris
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.activity.compose.BackHandler
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState
@@ -44,7 +48,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -57,11 +63,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.core.content.ContextCompat
+import androidx.core.content.PermissionChecker
import chat.revolt.R
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
-import com.google.accompanist.permissions.ExperimentalPermissionsApi
-import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlin.time.Duration.Companion.milliseconds
data class Media(
@@ -87,7 +93,7 @@ private fun Long.formatAsLengthDuration(): String {
return components.joinToString(separator = "")
}
-@OptIn(ExperimentalPermissionsApi::class, ExperimentalGlideComposeApi::class)
+@OptIn(ExperimentalGlideComposeApi::class)
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
fun InbuiltMediaPicker(
@@ -98,23 +104,75 @@ fun InbuiltMediaPicker(
pendingMedia: List,
disabled: Boolean = false,
) {
- val mediaPermissionState = rememberMultiplePermissionsState(
- listOf(
- android.Manifest.permission.READ_MEDIA_IMAGES,
- android.Manifest.permission.READ_MEDIA_VIDEO
- )
- )
-
val context = LocalContext.current
+ var hasPhotoPermission by remember {
+ mutableStateOf(
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.READ_MEDIA_IMAGES
+ ) == PermissionChecker.PERMISSION_GRANTED
+ )
+ }
+ var hasVideoPermission by remember {
+ mutableStateOf(
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.READ_MEDIA_VIDEO
+ ) == PermissionChecker.PERMISSION_GRANTED
+ )
+ }
+ val hasMediaPermissions by remember {
+ derivedStateOf {
+ hasPhotoPermission && hasVideoPermission
+ }
+ }
+ var mediaPermissionIsPartial by remember { mutableStateOf(null) }
+
+ val canShowGallery by remember {
+ derivedStateOf {
+ hasMediaPermissions || (mediaPermissionIsPartial == true)
+ }
+ }
+
+ val permissionRequester = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestMultiplePermissions(),
+ onResult = { mediaPermissionState ->
+ hasPhotoPermission = mediaPermissionState[Manifest.permission.READ_MEDIA_IMAGES] == true
+ hasVideoPermission = mediaPermissionState[Manifest.permission.READ_MEDIA_VIDEO] == true
+ mediaPermissionIsPartial = if (!hasMediaPermissions
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+ && mediaPermissionState[Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED] == true
+ ) {
+ true
+ } else if (hasMediaPermissions) {
+ false
+ } else null
+ }
+ )
+
val images = remember { mutableStateListOf() }
BackHandler {
onClose()
}
- LaunchedEffect(mediaPermissionState.allPermissionsGranted) {
- if (mediaPermissionState.allPermissionsGranted) {
+ LaunchedEffect(hasMediaPermissions) {
+ mediaPermissionIsPartial = if (!hasMediaPermissions
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+ && ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
+ ) == PermissionChecker.PERMISSION_GRANTED
+ ) {
+ true
+ } else if (hasMediaPermissions) {
+ false
+ } else null
+ }
+
+ LaunchedEffect(hasMediaPermissions, mediaPermissionIsPartial) {
+ if (hasMediaPermissions || mediaPermissionIsPartial == true) {
val projection = arrayOf(
MediaStore.Images.ImageColumns._ID,
MediaStore.Images.ImageColumns.RESOLUTION,
@@ -197,7 +255,7 @@ fun InbuiltMediaPicker(
verticalArrangement = Arrangement.Center
) {
Crossfade(
- targetState = mediaPermissionState.allPermissionsGranted,
+ targetState = canShowGallery,
animationSpec = tween(
durationMillis = 300,
easing = FastOutSlowInEasing
@@ -241,7 +299,22 @@ fun InbuiltMediaPicker(
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
- mediaPermissionState.launchMultiplePermissionRequest()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ permissionRequester.launch(
+ arrayOf(
+ Manifest.permission.READ_MEDIA_IMAGES,
+ Manifest.permission.READ_MEDIA_VIDEO,
+ Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
+ )
+ )
+ } else {
+ permissionRequester.launch(
+ arrayOf(
+ Manifest.permission.READ_MEDIA_IMAGES,
+ Manifest.permission.READ_MEDIA_VIDEO,
+ )
+ )
+ }
}) {
Text(text = stringResource(id = R.string.file_picker_permission_request_cta))
}
@@ -253,6 +326,74 @@ fun InbuiltMediaPicker(
.fillMaxHeight()
.padding(horizontal = 16.dp),
) {
+ AnimatedVisibility(
+ mediaPermissionIsPartial == true
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.ux_file_unpartialise_request),
+ modifier = Modifier
+ .size(52.dp),
+ contentDescription = null // decorative
+ )
+
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ ) {
+ Text(
+ text = stringResource(id = R.string.file_picker_permission_unpartialise_request_header),
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = stringResource(id = R.string.file_picker_permission_unpartialise_request_body),
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = LocalContentColor.current.copy(
+ alpha = 0.5f
+ )
+ )
+ )
+ }
+ }
+
+ Button(onClick = {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ permissionRequester.launch(
+ arrayOf(
+ Manifest.permission.READ_MEDIA_IMAGES,
+ Manifest.permission.READ_MEDIA_VIDEO,
+ Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
+ )
+ )
+ } else {
+ permissionRequester.launch(
+ arrayOf(
+ Manifest.permission.READ_MEDIA_IMAGES,
+ Manifest.permission.READ_MEDIA_VIDEO,
+ )
+ )
+ }
+ }) {
+ Text(text = stringResource(id = R.string.file_picker_permission_unpartialise_request_cta))
+ }
+ }
+ }
+
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState()),
diff --git a/app/src/main/res/drawable/ux_file_unpartialise_request.xml b/app/src/main/res/drawable/ux_file_unpartialise_request.xml
new file mode 100644
index 00000000..f4734bb6
--- /dev/null
+++ b/app/src/main/res/drawable/ux_file_unpartialise_request.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c618fd4f..fd01563e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -340,6 +340,9 @@
We need your permission to access photos and videos
You will be able to attach photos and videos to your messages afterwards.
Allow access
+ You have granted partial access to photos and videos
+ To attach all your photos and videos, we need your permission
+ Allow full access
Attach a file
Take a photo