android/ui: prompt for permissions and show list of permissions with statuses
Updates #ENG-2948 Signed-off-by: Percy Wegmann <percy@tailscale.com>pull/251/head
parent
8e063051b6
commit
44ba20a24e
@ -0,0 +1,47 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.model
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import com.tailscale.ipn.R
|
||||
|
||||
object Permissions {
|
||||
/**
|
||||
* All permissions that Tailscale requires. MainView takes care of prompting for permissions, and
|
||||
* PermissionsView provides a list of permissions with corresponding statuses and a link to the
|
||||
* application settings.
|
||||
*
|
||||
* When new permissions are needed, just add them to this list and the necessary strings to
|
||||
* strings.xml and the rest should take care of itself.
|
||||
*/
|
||||
val all: List<Permission>
|
||||
get() {
|
||||
val result = mutableListOf<Permission>()
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
result.add(
|
||||
Permission(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
R.string.permission_write_external_storage,
|
||||
R.string.permission_write_external_storage_needed,
|
||||
R.string.permission_write_external_storage_granted,
|
||||
))
|
||||
} else {
|
||||
result.add(
|
||||
Permission(
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
R.string.permission_post_notifications,
|
||||
R.string.permission_post_notifications_needed,
|
||||
R.string.permission_post_notifications_granted))
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class Permission(
|
||||
val name: String,
|
||||
val title: Int,
|
||||
val neededDescription: Int,
|
||||
val grantedDescription: Int
|
||||
)
|
@ -0,0 +1,75 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.view
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.tailscale.ipn.R
|
||||
import com.tailscale.ipn.ui.model.Permissions
|
||||
import com.tailscale.ipn.ui.util.itemsWithDividers
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun PermissionsView(nav: BackNavigation, openApplicationSettings: () -> Unit) {
|
||||
Scaffold(topBar = { Header(title = R.string.permissions, onBack = nav.onBack) }) { innerPadding ->
|
||||
val permissions = Permissions.all
|
||||
val permissionStates =
|
||||
rememberMultiplePermissionsState(permissions = permissions.map { it.name })
|
||||
val permissionsWithStates = permissions.zip(permissionStates.permissions)
|
||||
|
||||
LazyColumn(modifier = Modifier.padding(innerPadding)) {
|
||||
itemsWithDividers(permissionsWithStates) { (permission, state) ->
|
||||
var modifier: Modifier = Modifier
|
||||
if (!state.status.isGranted) {
|
||||
modifier = modifier.clickable { openApplicationSettings() }
|
||||
}
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
leadingContent = {
|
||||
Icon(
|
||||
if (state.status.isGranted) Icons.Filled.CheckCircle else Icons.Filled.Warning,
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentDescription =
|
||||
stringResource(if (state.status.isGranted) R.string.ok else R.string.warning))
|
||||
},
|
||||
headlineContent = {
|
||||
Text(stringResource(permission.title), style = MaterialTheme.typography.titleMedium)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
stringResource(
|
||||
if (state.status.isGranted) permission.grantedDescription
|
||||
else permission.neededDescription))
|
||||
},
|
||||
trailingContent = {
|
||||
if (!state.status.isGranted) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.KeyboardArrowRight,
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentDescription = stringResource(R.string.more))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue