Last active
May 1, 2021 09:58
-
-
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://…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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