diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt index eb6e7179b..a0c50015e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsActivity.kt @@ -4,64 +4,17 @@ package at.bitfire.davdroid.ui.account -import android.Manifest import android.accounts.Account -import android.app.Application import android.content.Intent -import android.content.IntentFilter -import android.location.LocationManager import android.os.Build import android.os.Bundle +import android.provider.Settings import androidx.activity.compose.setContent -import androidx.activity.viewModels -import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedButton -import androidx.compose.material.Scaffold -import androidx.compose.material.Switch -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.Help -import androidx.compose.material.icons.filled.CloudOff -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.core.app.TaskStackBuilder -import androidx.core.content.getSystemService -import androidx.core.location.LocationManagerCompat -import androidx.lifecycle.ViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewModelScope -import at.bitfire.davdroid.Constants -import at.bitfire.davdroid.Constants.withStatParams import at.bitfire.davdroid.R -import at.bitfire.davdroid.ui.M2Theme -import at.bitfire.davdroid.ui.composable.PermissionSwitchRow -import at.bitfire.davdroid.util.PermissionUtils -import at.bitfire.davdroid.util.broadcastReceiverFlow import dagger.hilt.android.AndroidEntryPoint -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @AndroidEntryPoint class WifiPermissionsActivity: AppCompatActivity() { @@ -71,207 +24,31 @@ class WifiPermissionsActivity: AppCompatActivity() { } private val account by lazy { intent.getParcelableExtra(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set") } - private val model by viewModels() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - M2Theme { - Scaffold( - topBar = { - TopAppBar( - navigationIcon = { - IconButton(onClick = { onSupportNavigateUp() }) { - Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.navigate_up)) - } - }, - title = { Text(stringResource(R.string.wifi_permissions_label)) }, - actions = { - val uriHandler = LocalUriHandler.current - IconButton(onClick = { - uriHandler.openUri(Constants.HOMEPAGE_URL.buildUpon() - .appendPath(Constants.HOMEPAGE_PATH_FAQ) - .appendPath(Constants.HOMEPAGE_PATH_FAQ_LOCATION_PERMISSION) - .withStatParams("WifiPermissionsActivity") - .build().toString()) - }) { - Icon(Icons.AutoMirrored.Default.Help, stringResource(R.string.help)) - } - } - ) - } - ) { padding -> - Box(modifier = Modifier.padding(padding)) { - Content( - backgroundPermissionOptionLabel = - if (Build.VERSION.SDK_INT >= 30) - packageManager.backgroundPermissionOptionLabel.toString() - else - stringResource(R.string.wifi_permissions_background_location_permission_label), - locationServiceEnabled = model.locationEnabled.collectAsStateWithLifecycle().value - ) - } - } - } - } - } - - override fun supportShouldUpRecreateTask(targetIntent: Intent) = true - - override fun onPrepareSupportNavigateUpTaskStack(builder: TaskStackBuilder) { - builder.editIntentAt(builder.intentCount - 1)?.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account) - } - - - @Composable - fun Content( - backgroundPermissionOptionLabel: String, - locationServiceEnabled: Boolean - ) { - Column( - Modifier - .padding(8.dp) - .verticalScroll(rememberScrollState())) { - Text( - stringResource(R.string.wifi_permissions_intro), - style = MaterialTheme.typography.body1 - ) - - // Android 8.1+: location permission - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) - LocationPermission( - modifier = Modifier.padding(top = 16.dp) - ) - - // Android 10+: background location permission - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - BackgroundLocationPermission( - backgroundPermissionOptionLabel = backgroundPermissionOptionLabel, - modifier = Modifier.padding(top = 16.dp) - ) - - // Android 9+: location service - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - LocationService( - locationServiceEnabled = locationServiceEnabled, - modifier = Modifier.padding(top = 16.dp) - ) - - // If permissions have actively been denied - Text( - stringResource(R.string.permissions_app_settings_hint), - style = MaterialTheme.typography.body2, - modifier = Modifier.padding(top = 16.dp) - ) - val context = LocalContext.current - OutlinedButton( - onClick = { PermissionUtils.showAppSettings(context) } - ) { - Text(stringResource(R.string.permissions_app_settings).uppercase()) - } - - Divider(Modifier.padding(vertical = 16.dp)) - - // Disclaimer - Row { - Text( - stringResource(R.string.wifi_permissions_background_location_disclaimer, stringResource(R.string.app_name)), - style = MaterialTheme.typography.body2, - modifier = Modifier.weight(1f) - ) - Icon(Icons.Default.CloudOff, null, modifier = Modifier.padding(8.dp)) - } - } - } - - @Composable - fun LocationPermission( - modifier: Modifier = Modifier - ) { - val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - Manifest.permission.ACCESS_FINE_LOCATION // since Android 10, fine location is required - else - Manifest.permission.ACCESS_COARSE_LOCATION // Android 8+: coarse location is enough - - PermissionSwitchRow( - text = stringResource(R.string.wifi_permissions_location_permission), - permissions = listOf(permission), - summaryWhenGranted = stringResource(R.string.wifi_permissions_location_permission_on), - summaryWhenNotGranted = stringResource(R.string.wifi_permissions_location_permission_off), - modifier = modifier - ) - } - - @RequiresApi(Build.VERSION_CODES.Q) - @Composable - fun BackgroundLocationPermission( - backgroundPermissionOptionLabel: String, - modifier: Modifier = Modifier - ) { - PermissionSwitchRow( - text = stringResource(R.string.wifi_permissions_background_location_permission), - permissions = listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), - summaryWhenGranted = stringResource(R.string.wifi_permissions_background_location_permission_on, backgroundPermissionOptionLabel), - summaryWhenNotGranted = stringResource(R.string.wifi_permissions_background_location_permission_off, backgroundPermissionOptionLabel), - modifier = modifier - ) - } - - @Composable - fun LocationService( - locationServiceEnabled: Boolean, - modifier: Modifier = Modifier - ) { - Row(modifier.fillMaxWidth()) { - Column(Modifier.weight(1f)) { - Text( - stringResource(R.string.wifi_permissions_location_enabled), - style = MaterialTheme.typography.body1 - ) - Text( - stringResource( - if (locationServiceEnabled) - R.string.wifi_permissions_location_enabled_on - else - R.string.wifi_permissions_location_enabled_off - ), - style = MaterialTheme.typography.body2 - ) - } - Switch( - checked = locationServiceEnabled, - onCheckedChange = { - val intent = Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS) + WifiPermissionsScreen( + backgroundPermissionOptionLabel = + if (Build.VERSION.SDK_INT >= 30) + packageManager.backgroundPermissionOptionLabel.toString() + else + stringResource(R.string.wifi_permissions_background_location_permission_label), + onEnableLocationService = { + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) if (intent.resolveActivity(packageManager) != null) startActivity(intent) - } + }, + onNavUp = ::onSupportNavigateUp ) } } - @Composable - @Preview - fun Content_Preview() { - Content( - backgroundPermissionOptionLabel = stringResource(R.string.wifi_permissions_background_location_permission_label), - locationServiceEnabled = true - ) - } - - - @HiltViewModel - class Model @Inject constructor( - context: Application - ): ViewModel() { - - private val locationManager = context.getSystemService()!! - - val locationEnabled = broadcastReceiverFlow(context, IntentFilter(LocationManager.MODE_CHANGED_ACTION), immediate = true) - .map { LocationManagerCompat.isLocationEnabled(locationManager) } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + override fun supportShouldUpRecreateTask(targetIntent: Intent) = true + override fun onPrepareSupportNavigateUpTaskStack(builder: TaskStackBuilder) { + builder.editIntentAt(builder.intentCount - 1)?.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account) } } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsModel.kt new file mode 100644 index 000000000..03fd5815b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsModel.kt @@ -0,0 +1,24 @@ +package at.bitfire.davdroid.ui.account + +import android.app.Application +import android.content.IntentFilter +import android.location.LocationManager +import androidx.core.content.getSystemService +import androidx.core.location.LocationManagerCompat +import androidx.lifecycle.ViewModel +import at.bitfire.davdroid.util.broadcastReceiverFlow +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class WifiPermissionsModel @Inject constructor( + context: Application +): ViewModel() { + + private val locationManager = context.getSystemService()!! + + val locationEnabled = broadcastReceiverFlow(context, IntentFilter(LocationManager.MODE_CHANGED_ACTION), immediate = true) + .map { LocationManagerCompat.isLocationEnabled(locationManager) } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsScreen.kt new file mode 100644 index 000000000..20e59961b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/WifiPermissionsScreen.kt @@ -0,0 +1,248 @@ +package at.bitfire.davdroid.ui.account + +import android.Manifest +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.filled.CloudOff +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import at.bitfire.davdroid.Constants +import at.bitfire.davdroid.Constants.withStatParams +import at.bitfire.davdroid.R +import at.bitfire.davdroid.ui.AppTheme +import at.bitfire.davdroid.ui.composable.PermissionSwitchRow +import at.bitfire.davdroid.util.PermissionUtils + +@Composable +fun WifiPermissionsScreen( + model: WifiPermissionsModel = viewModel(), + backgroundPermissionOptionLabel: String, + onEnableLocationService: (Boolean) -> Unit, + onNavUp: () -> Unit +) { + val locationServiceEnabled by model.locationEnabled.collectAsStateWithLifecycle(false) + WifiPermissionsScreen( + backgroundPermissionOptionLabel = backgroundPermissionOptionLabel, + locationServiceEnabled = locationServiceEnabled, + onEnableLocationService = onEnableLocationService, + onNavUp = onNavUp + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WifiPermissionsScreen( + backgroundPermissionOptionLabel: String, + locationServiceEnabled: Boolean, + onEnableLocationService: (Boolean) -> Unit, + onNavUp: () -> Unit +) { + AppTheme { + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = onNavUp) { + Icon( + Icons.AutoMirrored.Default.ArrowBack, + stringResource(R.string.navigate_up) + ) + } + }, + title = { Text(stringResource(R.string.wifi_permissions_label)) }, + actions = { + val uriHandler = LocalUriHandler.current + IconButton(onClick = { + uriHandler.openUri( + Constants.HOMEPAGE_URL.buildUpon() + .appendPath(Constants.HOMEPAGE_PATH_FAQ) + .appendPath(Constants.HOMEPAGE_PATH_FAQ_LOCATION_PERMISSION) + .withStatParams("WifiPermissionsActivity") + .build().toString() + ) + }) { + Icon(Icons.AutoMirrored.Default.Help, stringResource(R.string.help)) + } + } + ) + } + ) { padding -> + Box(modifier = Modifier.padding(padding)) { + WifiPermissionsScreenContent( + backgroundPermissionOptionLabel = backgroundPermissionOptionLabel, + locationServiceEnabled = locationServiceEnabled, + onEnableLocationService = onEnableLocationService + ) + } + } + } +} + +@Composable +fun WifiPermissionsScreenContent( + backgroundPermissionOptionLabel: String, + locationServiceEnabled: Boolean, + onEnableLocationService: (Boolean) -> Unit +) { + Column( + Modifier + .padding(8.dp) + .verticalScroll(rememberScrollState())) { + Text( + stringResource(R.string.wifi_permissions_intro), + style = MaterialTheme.typography.bodyLarge + ) + + // Android 8.1+: location permission + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) + LocationPermission( + modifier = Modifier.padding(top = 16.dp) + ) + + // Android 10+: background location permission + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + BackgroundLocationPermission( + backgroundPermissionOptionLabel = backgroundPermissionOptionLabel, + modifier = Modifier.padding(top = 16.dp) + ) + + // Android 9+: location service + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + LocationService( + locationServiceEnabled = locationServiceEnabled, + modifier = Modifier.padding(top = 16.dp), + onEnableLocationService = onEnableLocationService + ) + + // If permissions have actively been denied + Text( + stringResource(R.string.permissions_app_settings_hint), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 16.dp) + ) + val context = LocalContext.current + OutlinedButton( + onClick = { PermissionUtils.showAppSettings(context) } + ) { + Text(stringResource(R.string.permissions_app_settings)) + } + + HorizontalDivider(Modifier.padding(vertical = 16.dp)) + + // Disclaimer + Row { + Text( + stringResource( + R.string.wifi_permissions_background_location_disclaimer, stringResource( + R.string.app_name) + ), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f) + ) + Icon(Icons.Default.CloudOff, null, modifier = Modifier.padding(8.dp)) + } + } +} + +@Composable +fun LocationPermission( + modifier: Modifier = Modifier +) { + val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + Manifest.permission.ACCESS_FINE_LOCATION // since Android 10, fine location is required + else + Manifest.permission.ACCESS_COARSE_LOCATION // Android 8+: coarse location is enough + + PermissionSwitchRow( + text = stringResource(R.string.wifi_permissions_location_permission), + permissions = listOf(permission), + summaryWhenGranted = stringResource(R.string.wifi_permissions_location_permission_on), + summaryWhenNotGranted = stringResource(R.string.wifi_permissions_location_permission_off), + modifier = modifier + ) +} + +@RequiresApi(Build.VERSION_CODES.Q) +@Composable +fun BackgroundLocationPermission( + backgroundPermissionOptionLabel: String, + modifier: Modifier = Modifier +) { + PermissionSwitchRow( + text = stringResource(R.string.wifi_permissions_background_location_permission), + permissions = listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), + summaryWhenGranted = stringResource(R.string.wifi_permissions_background_location_permission_on, backgroundPermissionOptionLabel), + summaryWhenNotGranted = stringResource(R.string.wifi_permissions_background_location_permission_off, backgroundPermissionOptionLabel), + modifier = modifier + ) +} + +@Composable +fun LocationService( + locationServiceEnabled: Boolean, + modifier: Modifier = Modifier, + onEnableLocationService: (Boolean) -> Unit +) { + Row(modifier.fillMaxWidth()) { + Column(Modifier.weight(1f)) { + Text( + stringResource(R.string.wifi_permissions_location_enabled), + style = MaterialTheme.typography.bodyLarge + ) + Text( + stringResource( + if (locationServiceEnabled) + R.string.wifi_permissions_location_enabled_on + else + R.string.wifi_permissions_location_enabled_off + ), + style = MaterialTheme.typography.bodyMedium + ) + } + Switch( + checked = locationServiceEnabled, + onCheckedChange = onEnableLocationService + ) + } +} + +@Composable +@Preview +fun WifiPermissionsScreen_Preview() { + AppTheme { + WifiPermissionsScreen( + backgroundPermissionOptionLabel = stringResource(R.string.wifi_permissions_background_location_permission_label), + locationServiceEnabled = true, + onEnableLocationService = {}, + onNavUp = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt index 49726f9fe..52c4e147d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/PermissionSwitchRow.kt @@ -40,12 +40,12 @@ fun PermissionSwitchRow( text = text, modifier = Modifier.fillMaxWidth(), fontWeight = fontWeight, - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyLarge ) Text( text = if (allPermissionsGranted) summaryWhenGranted else summaryWhenNotGranted, modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.bodyMedium ) } Switch(