feat: support android 14 partial file access API

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2023-10-21 15:10:57 +02:00
parent 1eec2499ee
commit c3994ed9c5
4 changed files with 175 additions and 14 deletions

View File

@ -5,8 +5,11 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Android 13+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Android 14+ -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />

View File

@ -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<String>,
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<Boolean?>(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<Media>() }
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()),

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32.604dp"
android:height="25.573dp"
android:viewportWidth="32.604"
android:viewportHeight="25.573">
<path
android:fillColor="@color/foreground"
android:pathData="m7.88,7.088 l-6.274,1.681c-1.161,0.311 -1.842,1.491 -1.531,2.652l3.362,12.547a2.165,2.165 0,0 0,2.652 1.531l16.73,-4.483a2.165,2.165 0,0 0,1.531 -2.652l-2.802,-10.456c-0.311,-1.161 -1.501,-1.839 -2.652,-1.531l-8.365,2.241z"
android:strokeWidth="1.08249"/>
<path
android:pathData="m30.724,0 l1.88,0.186 -1.027,10.339 -1.88,-0.186zM31.297,13.346 L31.11,15.226 29.23,15.04 29.417,13.16z"
android:strokeWidth="0.944576"
android:fillColor="#db4e5b"/>
</vector>

View File

@ -340,6 +340,9 @@
<string name="file_picker_permission_request_header">We need your permission to access photos and videos</string>
<string name="file_picker_permission_request_body">You will be able to attach photos and videos to your messages afterwards.</string>
<string name="file_picker_permission_request_cta">Allow access</string>
<string name="file_picker_permission_unpartialise_request_header">You have granted partial access to photos and videos</string>
<string name="file_picker_permission_unpartialise_request_body">To attach all your photos and videos, we need your permission</string>
<string name="file_picker_permission_unpartialise_request_cta">Allow full access</string>
<string name="file_picker_chip_documents">Attach a file</string>
<string name="file_picker_chip_camera">Take a photo</string>