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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<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_IMAGES" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<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" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
package chat.revolt.components.media
|
package chat.revolt.components.media
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
|
@ -44,7 +48,9 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.PermissionChecker
|
||||||
import chat.revolt.R
|
import chat.revolt.R
|
||||||
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||||
import com.bumptech.glide.integration.compose.GlideImage
|
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
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
data class Media(
|
data class Media(
|
||||||
|
|
@ -87,7 +93,7 @@ private fun Long.formatAsLengthDuration(): String {
|
||||||
return components.joinToString(separator = "")
|
return components.joinToString(separator = "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class, ExperimentalGlideComposeApi::class)
|
@OptIn(ExperimentalGlideComposeApi::class)
|
||||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
@Composable
|
@Composable
|
||||||
fun InbuiltMediaPicker(
|
fun InbuiltMediaPicker(
|
||||||
|
|
@ -98,23 +104,75 @@ fun InbuiltMediaPicker(
|
||||||
pendingMedia: List<String>,
|
pendingMedia: List<String>,
|
||||||
disabled: Boolean = false,
|
disabled: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val mediaPermissionState = rememberMultiplePermissionsState(
|
|
||||||
listOf(
|
|
||||||
android.Manifest.permission.READ_MEDIA_IMAGES,
|
|
||||||
android.Manifest.permission.READ_MEDIA_VIDEO
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
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>() }
|
val images = remember { mutableStateListOf<Media>() }
|
||||||
|
|
||||||
BackHandler {
|
BackHandler {
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(mediaPermissionState.allPermissionsGranted) {
|
LaunchedEffect(hasMediaPermissions) {
|
||||||
if (mediaPermissionState.allPermissionsGranted) {
|
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(
|
val projection = arrayOf(
|
||||||
MediaStore.Images.ImageColumns._ID,
|
MediaStore.Images.ImageColumns._ID,
|
||||||
MediaStore.Images.ImageColumns.RESOLUTION,
|
MediaStore.Images.ImageColumns.RESOLUTION,
|
||||||
|
|
@ -197,7 +255,7 @@ fun InbuiltMediaPicker(
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Crossfade(
|
Crossfade(
|
||||||
targetState = mediaPermissionState.allPermissionsGranted,
|
targetState = canShowGallery,
|
||||||
animationSpec = tween(
|
animationSpec = tween(
|
||||||
durationMillis = 300,
|
durationMillis = 300,
|
||||||
easing = FastOutSlowInEasing
|
easing = FastOutSlowInEasing
|
||||||
|
|
@ -241,7 +299,22 @@ fun InbuiltMediaPicker(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Button(onClick = {
|
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))
|
Text(text = stringResource(id = R.string.file_picker_permission_request_cta))
|
||||||
}
|
}
|
||||||
|
|
@ -253,6 +326,74 @@ fun InbuiltMediaPicker(
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(horizontal = 16.dp),
|
.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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.horizontalScroll(rememberScrollState()),
|
.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_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_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_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_documents">Attach a file</string>
|
||||||
<string name="file_picker_chip_camera">Take a photo</string>
|
<string name="file_picker_chip_camera">Take a photo</string>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue