feat: support android 14 partial file access API
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
1eec2499ee
commit
c3994ed9c5
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue