Search code examples
android-jetpack-compose

How do we implement SAF in jetpack compose?


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() })
}
}

Solution

  • 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)
            }
    
        }
    
    
    }