Skip to content

Instantly share code, notes, and snippets.

@ugommirikwe
Last active May 1, 2021 09:58
Show Gist options
  • Save ugommirikwe/afff6d7f4075bd5e1161ec9e8970e2fb to your computer and use it in GitHub Desktop.
Save ugommirikwe/afff6d7f4075bd5e1161ec9e8970e2fb to your computer and use it in GitHub Desktop.
Here's how you would implement a permission request/validation in a Jetpack Compose app. Note the code was to intended for embedding a MapBox map, but there are some other code related to using the MapBox map that are not shown here. This code snippet is just for requesting permissions in a Compose UI app. This solution was inspired by: https://…
package io.ugommirikwe.app
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.provider.Settings
import androidx.activity.compose.LocalActivityResultRegistryOwner
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MyLocation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidView
import com.mapbox.mapboxsdk.location.modes.RenderMode
import com.mapbox.mapboxsdk.location.modes.CameraMode
import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions
import com.mapbox.mapboxsdk.location.LocationComponentOptions
import com.mapbox.mapboxsdk.maps.Style
import androidx.activity.compose.registerForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistryOwner
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import java.util.*
enum class PermissionRequestStatus {
GRANTED, DENIED, REQUIRES_RATIONALE
}
class MainActivity : AppCompatActivity() {
val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
loadHomeScreen(PermissionRequestStatus.GRANTED)
} else {
/*
Explain to the user that the feature is unavailable because the
features requires a permission that the user has denied. At the
same time, respect the user's decision. Don't link to system
settings in an effort to convince the user to change their
decision.
*/
loadHomeScreen(PermissionRequestStatus.DENIED)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED -> {
// You can use the API that requires the permission.
loadHomeScreen(PermissionRequestStatus.GRANTED)
}
shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) -> {
/*
In an educational UI, explain to the user why your app requires this
permission for a specific feature to behave as expected. In this UI,
include a "cancel" or "no thanks" button that allows the user to
continue using your app without granting the permission.
TODO: Show a Dialog/BottomSheet Composable
*/
loadHomeScreen(PermissionRequestStatus.REQUIRES_RATIONALE)
}
else -> {
// You can directly ask for the permission.
// The registered ActivityResultCallback gets the result of this request.
requestPermissionLauncher.launch(
Manifest.permission.ACCESS_FINE_LOCATION
)
}
}
}
private fun loadHomeScreen(permissionStatus: PermissionRequestStatus) {
setContent {
MaterialTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
HomeScreen(permissionStatus)
}
}
}
}
}
@Composable
fun HomeScreen(permissionStatus: PermissionRequestStatus) {
val context = LocalContext.current
val mapView = rememberMapViewWithLifecycle()
var permissionState by remember { mutableStateOf(permissionStatus) }
var shouldShowRationaleSnackBar by remember {
mutableStateOf(permissionStatus != PermissionRequestStatus.GRANTED)
}
var shouldShowSettingsSnackBar by remember(permissionState) {
mutableStateOf(
permissionState ==
PermissionRequestStatus.DENIED && !shouldShowRationaleSnackBar
)
}
val requestPermissionLauncher = registerForActivityResultV(
contract = ActivityResultContracts.RequestPermission()
) { granted ->
permissionState = if (granted) PermissionRequestStatus.GRANTED else {
PermissionRequestStatus.DENIED
}
}
Box(modifier = Modifier.fillMaxSize()) {
AndroidView({ mapView }) { map ->
map.getMapAsync { mapBoxMap ->
mapBoxMap.setStyle(Style.MAPBOX_STREETS) { style ->
if (permissionState != PermissionRequestStatus.GRANTED) return@setStyle
// Enable the most basic pulsing styling by ONLY using the `.pulseEnabled()` method
val customLocationComponentOptions =
LocationComponentOptions.builder(context)
.pulseEnabled(true)
.build()
// Get an instance of the component
val locationComponent = mapBoxMap.getLocationComponent()
// Activate with options
locationComponent.activateLocationComponent(
LocationComponentActivationOptions.builder(context, style)
.locationComponentOptions(customLocationComponentOptions)
.build()
)
// Enable to make component visible
locationComponent.isLocationComponentEnabled = true
// Set the component's camera mode
locationComponent.cameraMode = CameraMode.TRACKING_GPS
// Set the component's render mode
locationComponent.renderMode = RenderMode.GPS
}
}
}
if (permissionState.equals(PermissionRequestStatus.GRANTED)) {
Column(
Modifier
.align(Alignment.BottomEnd)
.padding(all = 30.dp)
) {
IconButton(onClick = {
}) {
Icon(Icons.Default.MyLocation, "My Location")
}
}
}
if (shouldShowRationaleSnackBar) {
Snackbar(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(all = 8.dp),
action = {
ClickableText(
AnnotatedString(
stringResource(R.string.permission_rationale_button_text_snackbar)
.toUpperCase()
),
modifier = Modifier
.padding(16.dp),
style = TextStyle(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.primary,
),
onClick = {
shouldShowRationaleSnackBar = false
requestPermissionLauncher.launch(
Manifest.permission.ACCESS_FINE_LOCATION
)
})
},
actionOnNewLine = true,
backgroundColor = MaterialTheme.colors.background,
contentColor = contentColorFor(backgroundColor = MaterialTheme.colors.background)
) {
Text(
text = stringResource(R.string.permission_rationale_message_snackbar).trim(),
)
}
}
if (shouldShowSettingsSnackBar) {
Snackbar(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(all = 8.dp),
action = {
ClickableText(
AnnotatedString(
stringResource(R.string.permission_settings_button_text_snackbar)
.toUpperCase()
),
modifier = Modifier.padding(16.dp),
style = TextStyle(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onPrimary,
),
onClick = {
shouldShowSettingsSnackBar = false
context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
})
},
actionOnNewLine = true
) {
Text(
text = stringResource(R.string.permission_settings_message_snackbar).trim(),
style = TextStyle(
color = MaterialTheme.colors.onBackground,
)
)
}
}
}
}
@Composable
fun <I, O> registerForActivityResultV(
contract: ActivityResultContract<I, O>,
onResult: (O) -> Unit
): ActivityResultLauncher<I> {
// First, find the ActivityResultRegistry by casting the Context
// (which is actually a ComponentActivity) to ActivityResultRegistryOwner
val activityResultRegistry = LocalActivityResultRegistryOwner.current.activityResultRegistry
// Keep track of the current onResult listener
val currentOnResult = rememberUpdatedState(onResult)
// It doesn't really matter what the key is, just that it is unique
// and consistent across configuration changes
val key = rememberSaveable { UUID.randomUUID().toString() }
// TODO a working layer of indirection would be great
val realLauncher = remember<ActivityResultLauncher<I>> {
activityResultRegistry.register(key, contract) {
currentOnResult.value(it)
}
}
onDispose {
realLauncher.unregister()
}
return realLauncher
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MaterialTheme {
HomeScreen(PermissionRequestStatus.GRANTED)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment