I'm working on a project where i need to copy, delete files from a specific folder (Android/Media). But as per android SAF guidelines we need to prompt user so they can select the folder and allow to use it (e.g. USE THIS FOLDER). I want you help to know that how do we actually check if SAF is granted and if its granted how do we store the granted folder and further use it.
@Composable
fun HomeScreen() {
PermissionHandler {
MediaScreen()
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PermissionHandler(content: @Composable () -> Unit) {
val permissionsList = listOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
val uri = remember {
mutableStateOf<Uri?>(null)
}
val permission = rememberMultiplePermissionsState(permissions = permissionsList)
val safLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()){ Uri ->
}
if (permission.allPermissionsGranted) {
if (uri.value == null){
Button(onClick = { safLauncher.launch(Uri.parse("emulated/0/android/media")) }) {
Text(text = "Please Select folder")
}
} else {
content()
}
} else {
AccessInfo(
accessModel = permissions[0],
isPopupEnabled = false,
onDetailLinkClick = { /*TODO*/ },
onPopUpButtonClick = { /*TODO*/ },
onAllowButtonClick = { permission.launchMultiplePermissionRequest() })
}
}
Here is the code. There is a lot of things need after this you can play around and get it.
First of need custom contract for the activityResultLauncher so here I have made
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Parcelable
import android.os.storage.StorageManager
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.RequiresApi
import com.md.whatsapppath.Constants.whatsappUriForMatching
class WhatsappDocumentContract : ActivityResultContract<Uri?, Uri?>() {
private fun getRootFolderPath(): String {
return "Android%2Fmedia%2Fcom.whatsapp%2FWhatsApp%2FMedia%2F.Statuses"
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun createIntent(context: Context, input: Uri?): Intent {
val intent: Intent
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val yourFolderPath: String = getRootFolderPath()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent = storageManager.primaryStorageVolume.createOpenDocumentTreeIntent()
val replace =
(intent.getParcelableExtra(
"android.provider.extra.INITIAL_URI",
Parcelable::class.java
) as Uri).toString()
.replace("/root/", "/document/")
intent.putExtra(
"android.provider.extra.INITIAL_URI",
Uri.parse("$replace%3A$yourFolderPath")
)
whatsappUriForMatching = Uri.parse("$replace%3A$yourFolderPath")
} else {
//for our case this is useless because for P and Q we are using normal scenario
intent = Intent("android.intent.action.OPEN_DOCUMENT_TREE")
intent.putExtra(
"android.provider.extra.INITIAL_URI",
Uri.parse("content://com.android.externalstorage.documents/document/primary%3A$yourFolderPath")
)
whatsappUriForMatching =
Uri.parse("content://com.android.externalstorage.documents/document/primary%3A$yourFolderPath")
}
intent.putExtra("android.content.extra.SHOW_ADVANCED", true)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
if (resultCode != Activity.RESULT_OK) {
return null
}
return intent?.data
}
}
Other Required Classes
object Constants {
var whatsappUriForMatching: Uri? = null
}
object StorageUtil {
fun isWhatsappUriPermissionGranted(mContext: Context): Boolean {
return mContext.contentResolver.persistedUriPermissions.filter { it.uri.toString() == "content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia%2Fcom.whatsapp%2FWhatsApp%2FMedia%2F.Statuses" }
.isNotEmpty()
}
}
Use CustomDocumentContact
for permission and get the permission.
import android.content.Intent
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
@Composable
fun WhatsappPath(modifier: Modifier = Modifier) {
val context = LocalContext.current
val whatsappPermissionLauncher =
rememberLauncherForActivityResult(contract = WhatsappDocumentContract(),
onResult = { result ->
if (result != null) {
val strResult: String =
result.toString().substring(result.toString().lastIndexOf("/"))
val actualSelectedUri: String = Constants.whatsappUriForMatching.toString()
.substring(Constants.whatsappUriForMatching.toString().lastIndexOf("/"))
if (actualSelectedUri == strResult) {
val takeFlags =
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
context.contentResolver.takePersistableUriPermission(result, takeFlags)
//Permission Granted Save It In SharedPreference
Toast.makeText(context,"Permission Granted",Toast.LENGTH_SHORT).show()
} else {
//Selected Wrong Locations
Toast.makeText(
context, "Selected Wrong Whatsapp Locations", Toast.LENGTH_SHORT
).show()
}
}
})
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 30.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(modifier = Modifier
.padding(horizontal = 30.dp)
.clip(shape = RoundedCornerShape(12.dp))
.fillMaxWidth()
.height(45.dp)
.clickable {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (!StorageUtil.isWhatsappUriPermissionGranted(context)) {
whatsappPermissionLauncher.launch(null)
} else {
//Permission Given Do your work
Toast.makeText(context,"Permission Granted",Toast.LENGTH_SHORT).show()
}
} else {
//Request For Write External Storage Permission
}
}
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0xFFF4CDBC),
Color(0xFFCACFF9),
)
)
), contentAlignment = Alignment.Center) {
Text("Allow Permission", color = Color.Black)
}
}
}