├── ComposePermission.kt ├── PermissionTestActivity.kt └── README.md /ComposePermission.kt: -------------------------------------------------------------------------------- 1 | package com.example.testproject 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.content.Intent 7 | import android.content.SharedPreferences 8 | import android.content.pm.PackageManager 9 | import android.net.Uri 10 | import android.os.Build 11 | import android.provider.Settings 12 | import androidx.activity.compose.ManagedActivityResultLauncher 13 | import androidx.activity.compose.rememberLauncherForActivityResult 14 | import androidx.activity.result.contract.ActivityResultContracts 15 | import androidx.annotation.ChecksSdkIntAtLeast 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.core.app.ActivityCompat 19 | 20 | 21 | enum class Status{ 22 | INITIAL,GRANTED_ALREADY,DENIED_WITH_RATIONALE,NOT_ASKED,DENIED_WITH_NEVER_ASK 23 | } 24 | 25 | @Composable 26 | fun requestMultiplePermission(permissions:List, onChangedStatus:(statusList:Map) -> Unit) 27 | : ManagedActivityResultLauncher, *> { 28 | val activity = LocalContext.current.requiredActivity() 29 | val allStatus = mutableMapOf() 30 | permissions.forEach { 31 | val status = if (shouldAskPermission(it, activity)) { 32 | if (ActivityCompat.shouldShowRequestPermissionRationale(activity, it)) { 33 | Status.DENIED_WITH_RATIONALE 34 | } else { 35 | if (PermissionPreferences(activity).isFirstTimeAsking(it)) { 36 | Status.NOT_ASKED 37 | } else { 38 | Status.DENIED_WITH_NEVER_ASK 39 | } 40 | } 41 | } else { 42 | Status.GRANTED_ALREADY 43 | } 44 | allStatus[it] = status 45 | } 46 | onChangedStatus(allStatus) 47 | 48 | return rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestMultiplePermissions(), onResult = { result -> 49 | val permissionsStatus = mutableMapOf() 50 | result.forEach { 51 | if (it.value.not()) { 52 | if (ActivityCompat.shouldShowRequestPermissionRationale(activity, it.key)) { 53 | permissionsStatus[it.key] = Status.DENIED_WITH_RATIONALE 54 | PermissionPreferences(activity).firstTimeAsking(it.key,false) 55 | } else { 56 | if (PermissionPreferences(activity).isFirstTimeAsking(it.key)){ 57 | permissionsStatus[it.key] = Status.NOT_ASKED 58 | }else{ 59 | permissionsStatus[it.key] = Status.DENIED_WITH_NEVER_ASK 60 | } 61 | 62 | } 63 | } else { 64 | permissionsStatus[it.key] = Status.GRANTED_ALREADY 65 | } 66 | } 67 | onChangedStatus(permissionsStatus) 68 | 69 | }) 70 | } 71 | 72 | @Composable 73 | fun requestPermission(permission: String, onChangedStatus:(status: Status) -> Unit) 74 | : ManagedActivityResultLauncher { 75 | val activity = LocalContext.current.requiredActivity() 76 | 77 | val initial = if (shouldAskPermission(permission, activity)) { 78 | if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { 79 | Status.DENIED_WITH_RATIONALE 80 | } else { 81 | if (PermissionPreferences(activity).isFirstTimeAsking(permission)) { 82 | Status.NOT_ASKED 83 | } else { 84 | Status.DENIED_WITH_NEVER_ASK 85 | } 86 | } 87 | } else { 88 | Status.GRANTED_ALREADY 89 | } 90 | onChangedStatus(initial) 91 | 92 | return rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission(), onResult = { isGranted-> 93 | 94 | if (isGranted.not()) { 95 | if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { 96 | onChangedStatus(Status.DENIED_WITH_RATIONALE) 97 | PermissionPreferences(activity).firstTimeAsking(permission,false) 98 | } else { 99 | if (PermissionPreferences(activity).isFirstTimeAsking(permission)){ 100 | onChangedStatus(Status.NOT_ASKED) 101 | }else{ 102 | onChangedStatus(Status.DENIED_WITH_NEVER_ASK) 103 | } 104 | 105 | } 106 | } else { 107 | onChangedStatus(Status.GRANTED_ALREADY) 108 | } 109 | }) 110 | } 111 | 112 | fun Map.allGranted():Boolean{ 113 | return this.values.all { it == Status.GRANTED_ALREADY } 114 | } 115 | 116 | fun Map.allDenied():Boolean{ 117 | return this.values.all { it != Status.GRANTED_ALREADY } 118 | } 119 | 120 | 121 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M) 122 | private fun shouldAskPermission(): Boolean { 123 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M 124 | } 125 | 126 | private fun shouldAskPermission(permission: String, activity: Activity): Boolean { 127 | if (shouldAskPermission()) { 128 | val permissionResult = ActivityCompat.checkSelfPermission(activity, permission) 129 | if (permissionResult != PackageManager.PERMISSION_GRANTED) { 130 | return true 131 | } 132 | } 133 | return false 134 | } 135 | 136 | 137 | 138 | 139 | private fun Context.requiredActivity(): Activity { 140 | var context = this 141 | while (context is ContextWrapper) { 142 | if (context is Activity) return context 143 | context = context.baseContext 144 | } 145 | throw IllegalStateException("Permissions should be called in the context of an Activity") 146 | } 147 | 148 | fun Context.openAppSystemSettings() { 149 | startActivity(Intent().apply { 150 | action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS 151 | data = Uri.fromParts("package", packageName, null) 152 | }) 153 | } 154 | 155 | 156 | 157 | 158 | class PermissionPreferences(context: Context) { 159 | private val sharedPreferences: SharedPreferences 160 | private var editor: SharedPreferences.Editor? = null 161 | private val preference = "permissions_Settings" 162 | fun firstTimeAsking(permission: String?, isFirstTime: Boolean) { 163 | doEdit() 164 | editor?.putBoolean(permission, isFirstTime) 165 | doCommit() 166 | } 167 | 168 | fun isFirstTimeAsking(permission: String?): Boolean { 169 | return sharedPreferences.getBoolean(permission, true) 170 | } 171 | 172 | private fun doEdit() { 173 | if (editor == null) { 174 | editor = sharedPreferences.edit() 175 | } 176 | } 177 | 178 | private fun doCommit() { 179 | if (editor != null) { 180 | editor?.commit() 181 | editor = null 182 | } 183 | } 184 | 185 | init { 186 | sharedPreferences = context.getSharedPreferences(preference, Context.MODE_PRIVATE) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /PermissionTestActivity.kt: -------------------------------------------------------------------------------- 1 | class PermissionTestActivity : ComponentActivity() { 2 | override fun onCreate(savedInstanceState: Bundle?) { 3 | super.onCreate(savedInstanceState) 4 | enableEdgeToEdge() 5 | setContent { 6 | TestProjectTheme { 7 | Greeting() 8 | 9 | } 10 | } 11 | } 12 | } 13 | 14 | @Composable 15 | fun Greeting() { 16 | 17 | val scope = rememberCoroutineScope() 18 | val context = LocalContext.current 19 | var permissionStatus by remember { 20 | mutableStateOf(Status.INITIAL) 21 | } 22 | 23 | var showDeniedDialog by remember { 24 | mutableStateOf(false) 25 | } 26 | var showGrantedDialog by remember { 27 | mutableStateOf(false) 28 | } 29 | 30 | var showNavigateSettingDialog by remember { 31 | mutableStateOf(false) 32 | } 33 | 34 | 35 | 36 | 37 | var showRequestDialog by remember { 38 | mutableStateOf(false) 39 | } 40 | 41 | val request = requestPermission(permission = Manifest.permission.CAMERA, 42 | onChangedStatus = { 43 | if (((permissionStatus == Status.NOT_ASKED) or (permissionStatus == Status.DENIED_WITH_RATIONALE)) //User granted the permission 44 | and (it == Status.GRANTED_ALREADY)){ 45 | showGrantedDialog = true 46 | }else if (((permissionStatus == Status.NOT_ASKED) and (it == Status.DENIED_WITH_RATIONALE)) //User denied the permission 47 | or ((permissionStatus == Status.DENIED_WITH_RATIONALE) and (it == Status.DENIED_WITH_NEVER_ASK))){ 48 | showDeniedDialog = true 49 | } 50 | permissionStatus = it 51 | }) 52 | 53 | 54 | 55 | Column(modifier = Modifier 56 | .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, 57 | verticalArrangement = Arrangement.Center) { 58 | 59 | 60 | SpecialDialog(showDialog = showRequestDialog, onChanged = {showRequestDialog = it}) { 61 | Box( 62 | modifier = Modifier 63 | .background( 64 | Color(0xFFFFFFFF), 65 | shape = RoundedCornerShape(12.dp) 66 | ), contentAlignment = Alignment.Center 67 | ){ 68 | 69 | Column(modifier = Modifier 70 | .fillMaxWidth(0.86f) 71 | .padding(vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { 72 | 73 | Icon(imageVector = Icons.Rounded.PhotoCamera, contentDescription = "", tint = Color(0xFF1A97F7)) 74 | 75 | 76 | Text(text = "We need to access your camera to take photo", fontSize = 16.sp, fontWeight = FontWeight.Medium, 77 | modifier = Modifier 78 | .padding(vertical = 32.dp) 79 | .padding(horizontal = 12.dp), textAlign = TextAlign.Center) 80 | 81 | 82 | Button(onClick = { 83 | showRequestDialog = false 84 | scope.launch { 85 | delay(500) 86 | request.launch(Manifest.permission.CAMERA) 87 | } 88 | }, modifier = Modifier 89 | .width(180.dp)) { 90 | Text(text = "Allow") 91 | } 92 | 93 | Button(onClick = { 94 | showRequestDialog = false 95 | }, modifier = Modifier 96 | .width(180.dp), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFFFFFF) 97 | )) { 98 | Text(text = "Close") 99 | } 100 | 101 | 102 | 103 | } 104 | } 105 | } 106 | SpecialDialog(showDialog = showDeniedDialog, onChanged = {showDeniedDialog = it}) { 107 | Box( 108 | modifier = Modifier 109 | .background( 110 | Color(0xFFFFFFFF), 111 | shape = RoundedCornerShape(12.dp) 112 | ), contentAlignment = Alignment.Center 113 | ){ 114 | 115 | Column(modifier = Modifier 116 | .fillMaxWidth(0.86f) 117 | .padding(vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { 118 | 119 | Icon(imageVector = Icons.Rounded.PhotoCamera, contentDescription = "", tint = Color(0xFF1A97F7)) 120 | 121 | 122 | Text(text = "You denied Camera permission!", fontSize = 16.sp, fontWeight = FontWeight.Medium, 123 | modifier = Modifier 124 | .padding(vertical = 32.dp) 125 | .padding(horizontal = 12.dp), textAlign = TextAlign.Center) 126 | 127 | 128 | 129 | Button(onClick = { 130 | showDeniedDialog = false 131 | }, modifier = Modifier 132 | .width(180.dp)) { 133 | Text(text = "Close") 134 | } 135 | 136 | 137 | 138 | } 139 | } 140 | } 141 | SpecialDialog(showDialog = showGrantedDialog, onChanged = {showGrantedDialog = it}) { 142 | Box( 143 | modifier = Modifier 144 | .background( 145 | Color(0xFFFFFFFF), 146 | shape = RoundedCornerShape(12.dp) 147 | ), contentAlignment = Alignment.Center 148 | ){ 149 | 150 | Column(modifier = Modifier 151 | .fillMaxWidth(0.86f) 152 | .padding(vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { 153 | 154 | Icon(imageVector = Icons.Rounded.PhotoCamera, contentDescription = "", tint = Color(0xFF1A97F7)) 155 | 156 | 157 | Text(text = "You have Granted Camera permission!", fontSize = 16.sp, fontWeight = FontWeight.Medium, 158 | modifier = Modifier 159 | .padding(vertical = 32.dp) 160 | .padding(horizontal = 12.dp), textAlign = TextAlign.Center) 161 | 162 | 163 | 164 | Button(onClick = { 165 | showGrantedDialog = false 166 | }, modifier = Modifier 167 | .width(180.dp)) { 168 | Text(text = "Close") 169 | } 170 | 171 | 172 | 173 | } 174 | } 175 | } 176 | SpecialDialog(showDialog = showNavigateSettingDialog, onChanged = {showNavigateSettingDialog = it}) { 177 | Box( 178 | modifier = Modifier 179 | .background( 180 | Color(0xFFFFFFFF), 181 | shape = RoundedCornerShape(12.dp) 182 | ), contentAlignment = Alignment.Center 183 | ){ 184 | 185 | Column(modifier = Modifier 186 | .fillMaxWidth(0.86f) 187 | .padding(vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { 188 | 189 | Icon(imageVector = Icons.Rounded.PhotoCamera, contentDescription = "", tint = Color(0xFF1A97F7)) 190 | 191 | 192 | Text(text = "You have disabled camera permission! You can navigate settings and enable Camera permission", fontSize = 16.sp, fontWeight = FontWeight.Medium, 193 | modifier = Modifier 194 | .padding(vertical = 32.dp) 195 | .padding(horizontal = 12.dp), textAlign = TextAlign.Center) 196 | 197 | 198 | 199 | Button(onClick = { 200 | showNavigateSettingDialog = false 201 | scope.launch { 202 | delay(500) 203 | context.openAppSystemSettings() 204 | } 205 | }, modifier = Modifier 206 | .width(180.dp)) { 207 | Text(text = "Settings") 208 | } 209 | 210 | 211 | 212 | } 213 | } 214 | } 215 | Button(onClick = { 216 | if (permissionStatus == Status.GRANTED_ALREADY){ 217 | val intent = Intent("android.media.action.IMAGE_CAPTURE") 218 | context.findActivity().startActivityForResult(intent, 0) 219 | 220 | }else if ((permissionStatus == Status.NOT_ASKED) or (permissionStatus == Status.DENIED_WITH_RATIONALE)){ 221 | showRequestDialog = showRequestDialog.not() 222 | }else if (permissionStatus == Status.DENIED_WITH_NEVER_ASK){ 223 | request.launch(Manifest.permission.CAMERA) 224 | scope.launch { 225 | delay(1000) 226 | if (context.findActivity().hasWindowFocus()){ //See below for why hasWindowFocus should be true 227 | showNavigateSettingDialog = true 228 | } 229 | } 230 | } 231 | 232 | }) { 233 | Text(text = "Open Camera ${permissionStatus.name}") 234 | } 235 | 236 | } 237 | } 238 | 239 | 240 | @Preview(showBackground = true) 241 | @Composable 242 | fun GreetingPreview7() { 243 | TestProjectTheme { 244 | Greeting() 245 | } 246 | } 247 | 248 | @Composable 249 | fun SpecialDialog(showDialog:Boolean, onChanged:(isShown:Boolean) -> Unit, properties: DialogProperties = DialogProperties(), 250 | content: @Composable () -> Unit){ 251 | 252 | val animation by remember { 253 | mutableStateOf(androidx.compose.animation.core.Animatable(initialValue = 1f)) 254 | } 255 | 256 | var show by remember { 257 | mutableStateOf(false) 258 | } 259 | 260 | 261 | LaunchedEffect(key1 = showDialog){ 262 | 263 | if (showDialog) show = true 264 | launch { 265 | if (showDialog.not() and show) { 266 | delay(350) 267 | show = false 268 | } 269 | } 270 | launch { 271 | delay(75) 272 | animation.animateTo(targetValue = 1.05f, 273 | animationSpec = spring(Spring.DampingRatioMediumBouncy,Spring.StiffnessMediumLow)) 274 | } 275 | 276 | launch { 277 | delay(175) 278 | animation.animateTo(targetValue = 1f, 279 | animationSpec = spring(Spring.DampingRatioMediumBouncy,Spring.StiffnessMediumLow)) 280 | } 281 | 282 | 283 | } 284 | 285 | 286 | if (show){ 287 | 288 | Dialog(onDismissRequest = { 289 | onChanged(false) 290 | 291 | }, properties = properties) { 292 | 293 | Box(modifier = Modifier.scale(animation.value)){ 294 | content() 295 | } 296 | } 297 | } 298 | 299 | } 300 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compose-Permissions 2 | * A library to handle permissions in Jetpack Compose 3 | 4 | 5 | ## Install 6 | 7 | * Add [`ComposePermission.kt`](https://github.com/akardas16/Compose-Permissions/blob/main/ComposePermission.kt) file to your project 8 | 9 | # Usage 10 | 11 |
12 | 13 | | Status | Description | 14 | | --- | --- | 15 | | GRANTED_ALREADY | User has already granted permission | 16 | | NOT_ASKED | User has never requested the permission (Possible to show permission dialog) | 17 | | DENIED_WITH_RATIONALE | User has denied the permission but still has a chance to request for permission dialog (Possible to show permission dialog) | 18 | | DENIED_WITH_NEVER_ASK | Not possible to show permission dialog. (navigate user to App Settings) | 19 | 20 |
21 | 22 | ## Single Permission Request 23 | 24 | * Track status of permission with `permissionStatus` 25 | ```kotlin 26 | var permissionStatus by remember { // 27 | mutableStateOf(Status.INITIAL) 28 | } 29 | val request = requestPermission(permission = android.Manifest.permission.CAMERA, 30 | onChangedStatus = { permissionStatus = it}) 31 | ``` 32 |
33 | 34 | * To request permission 35 | 36 | 37 | ```kotlin 38 | request.launch(android.Manifest.permission.CAMERA) 39 | ``` 40 |
41 |
42 | 43 | * See complete example below for single permission request 44 | 45 | ```kotlin 46 | @Composable 47 | fun Greeting() { 48 | 49 | val context = LocalContext.current 50 | val scope = rememberCoroutineScope() 51 | var permissionStatus by remember { // Track status of permission 52 | mutableStateOf(Status.INITIAL) 53 | } 54 | 55 | val request = requestPermission(permission = android.Manifest.permission.CAMERA, 56 | onChangedStatus = { permissionStatus = it}) 57 | 58 | Column(modifier = Modifier 59 | .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, 60 | verticalArrangement = Arrangement.Center) { 61 | 62 | //you can use if(permissionStatus) if you don't interest all states 63 | when(permissionStatus){ 64 | Status.GRANTED_ALREADY -> {//(No need to request permission) or (permission requested and granted already) 65 | Text(text = "Permission Has already Granted") 66 | } 67 | Status.NOT_ASKED -> {//a place to request permission 68 | Text(text = "No permission request has made") 69 | } 70 | Status.DENIED_WITH_RATIONALE -> { 71 | Text(text = "Permission has denied once but you have still have a chance to show permission popup") 72 | } 73 | Status.DENIED_WITH_NEVER_ASK -> {//call context.openAppSystemSettings() to navigate user to app settings 74 | 75 | Text(text = "Permission has denied navigate user to app settings") 76 | } 77 | else -> {}//Nothing 78 | } 79 | 80 | Button(onClick = { 81 | //Request permission 82 | request.launch(android.Manifest.permission.CAMERA) 83 | scope.launch { 84 | delay(1000) 85 | if (permissionStatus == Status.DENIED_WITH_NEVER_ASK 86 | && context.activity()?.hasWindowFocus() == true){ //See below for why hasWindowFocus should be true 87 | context.openAppSystemSettings() 88 | } 89 | } 90 | 91 | }) { Text("Request permission") } 92 | 93 | } 94 | } 95 | ``` 96 | ## Multiple Permissions Request 97 | 98 | * Track status of permissions with `permissionStatus` 99 | ```kotlin 100 | var permissionStatus by remember { 101 | mutableStateOf(mapOf()) 102 | } 103 | val request = requestMultiplePermission( 104 | permissions = listOf( 105 | Manifest.permission.CAMERA, Manifest.permission.POST_NOTIFICATIONS 106 | ), onChangedStatus = { permissionStatus = it} 107 | ) 108 | ``` 109 |
110 | 111 | * To request multiple permissions 112 | 113 | ```kotlin 114 | request.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.POST_NOTIFICATIONS)) 115 | ``` 116 |
117 |
118 | 119 | * See complete example below for multiple permissions request 120 | 121 | ```kotlin 122 | @Composable 123 | fun Greeting() { 124 | 125 | var permissionStatus by remember { 126 | mutableStateOf(mapOf()) 127 | } 128 | 129 | val request = requestMultiplePermission( 130 | permissions = listOf( 131 | Manifest.permission.CAMERA, 132 | Manifest.permission.POST_NOTIFICATIONS 133 | ), onChangedStatus = { permissionStatus = it} 134 | ) 135 | 136 | Column(modifier = Modifier 137 | .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, 138 | verticalArrangement = Arrangement.Center) { 139 | 140 | if (permissionStatus.allGranted()){ 141 | Text(text = "All Permissions have already Granted") 142 | } 143 | 144 | if (permissionStatus.allDenied()){ 145 | Text(text = "All Permissions have not Granted") 146 | } 147 | Row { 148 | Text(text = permissionStatus.keys.first().filter { it.isUpperCase() }, fontSize = 12.sp) 149 | Icon(imageVector = Icons.Default.ArrowForward, contentDescription = "") 150 | Text(text = permissionStatus.values.first().name, fontSize = 12.sp) 151 | } 152 | 153 | Row { 154 | Text(text = permissionStatus.keys.last().filter { it.isUpperCase() }, fontSize = 12.sp) 155 | Icon(imageVector = Icons.Default.ArrowForward, contentDescription = "") 156 | Text(text = permissionStatus.values.last().name, fontSize = 12.sp) 157 | } 158 | 159 | Button(onClick = { 160 | //Request permission 161 | request.launch(arrayOf(Manifest.permission.CAMERA, 162 | Manifest.permission.POST_NOTIFICATIONS)) 163 | 164 | }) { Text("Request permissions") } 165 | 166 | } 167 | } 168 | ``` 169 |
170 |
171 | 172 | * If all permissions have granted, `permissionStatus.allGranted()` will be true 173 | * If all permissions have not granted, `permissionStatus.allDenied()` will be true 174 | 175 | ```kotlin 176 | fun Map.allGranted():Boolean{ 177 | return this.values.all { it == Status.GRANTED_ALREADY } 178 | } 179 | fun Map.allDenied():Boolean{ 180 | return this.values.all { it != Status.GRANTED_ALREADY } 181 | } 182 | ``` 183 |
184 |
185 | 186 | * It is not possible to observe changes, if user has manually changed permission in app settings 187 | * If user has denied permission with never_ask and changed permission manually, it will be better to request permission and check any window popuped or not. 188 | 189 | ```kotlin 190 | request.launch(android.Manifest.permission.CAMERA) 191 | scope.launch { 192 | delay(1000) 193 | if (permissionStatus == Status.DENIED_WITH_NEVER_ASK 194 | && context.activity()?.hasWindowFocus() == true){ 195 | context.openAppSystemSettings() 196 | } 197 | } 198 | ``` 199 | 200 | 201 | --------------------------------------------------------------------------------