diff --git a/app/build.gradle b/app/build.gradle index 5195318..78fde1b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -177,6 +177,7 @@ dependencies { implementation "com.google.android.material:material:1.8.0" implementation 'androidx.compose.material:material:1.3.1' implementation 'androidx.activity:activity-compose:1.6.1' + implementation "com.google.accompanist:accompanist-flowlayout:0.30.0" implementation 'androidx.compose.material:material-icons-extended:1.3.1' implementation 'androidx.fragment:fragment-ktx:1.5.5' diff --git a/app/src/main/java/io/timelimit/android/ui/MainActivity.kt b/app/src/main/java/io/timelimit/android/ui/MainActivity.kt index dfecc60..44e6897 100644 --- a/app/src/main/java/io/timelimit/android/ui/MainActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/MainActivity.kt @@ -45,6 +45,7 @@ import io.timelimit.android.R import io.timelimit.android.data.IdGenerator import io.timelimit.android.data.model.UserType import io.timelimit.android.extensions.showSafe +import io.timelimit.android.integration.platform.SystemPermissionConfirmationLevel import io.timelimit.android.integration.platform.android.NotificationChannels import io.timelimit.android.livedata.ignoreUnchanged import io.timelimit.android.livedata.liveDataFromNullableValue @@ -141,6 +142,9 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De ActivityCommand.ShowCanNotAddDevicesInLocalModeDialogFragment -> CanNotAddDevicesInLocalModeDialogFragment().show(supportFragmentManager) ActivityCommand.ShowAuthenticationScreen -> showAuthenticationScreen() ActivityCommand.ShowMissingPremiumDialog -> RequiresPurchaseDialogFragment().show(supportFragmentManager) + is ActivityCommand.LaunchSystemSettings -> mainModel.logic.platformIntegration.openSystemPermissionScren( + this@MainActivity, message.permission, SystemPermissionConfirmationLevel.Suggestion + ) } } } diff --git a/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt b/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt index 6793081..2dc6189 100644 --- a/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt +++ b/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt @@ -19,9 +19,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.FragmentManager import io.timelimit.android.ui.diagnose.deviceowner.DeviceOwnerScreen +import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionScreen import io.timelimit.android.ui.manage.device.manage.user.ManageDeviceUserScreen import io.timelimit.android.ui.model.Screen import io.timelimit.android.ui.overview.overview.OverviewScreen +import io.timelimit.android.ui.setup.SetupDevicePermissionsScreen @Composable fun ScreenMultiplexer( @@ -36,5 +38,7 @@ fun ScreenMultiplexer( is Screen.OverviewScreen -> OverviewScreen(screen.content, modifier = modifier) is Screen.ManageDeviceUserScreen -> ManageDeviceUserScreen(screen.items, screen.actions, screen.overlay, modifier) is Screen.DeviceOwnerScreen -> DeviceOwnerScreen(screen.content, modifier = modifier) + is Screen.SetupDevicePermissionsScreen -> SetupDevicePermissionsScreen(screen.content, screen.next, modifier) + is Screen.ManageDevicePermissions -> ManageDevicePermissionScreen(screen.content, modifier) } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/ManageDevicePermissionScreen.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/ManageDevicePermissionScreen.kt new file mode 100644 index 0000000..484ba1a --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/ManageDevicePermissionScreen.kt @@ -0,0 +1,36 @@ +/* + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.manage.device.manage.permission + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ManageDevicePermissionScreen( + content: PermissionScreenContent, + modifier: Modifier = Modifier +) { + PermissionScreen( + content, + modifier + .verticalScroll(rememberScrollState()) + .padding(8.dp) + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/ManageDevicePermissionsFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/ManageDevicePermissionsFragment.kt index d1f5bc1..e9bed0b 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/ManageDevicePermissionsFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/ManageDevicePermissionsFragment.kt @@ -16,202 +16,40 @@ package io.timelimit.android.ui.manage.device.manage.permission import android.content.Context -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.map import io.timelimit.android.R import io.timelimit.android.data.model.Device -import io.timelimit.android.data.model.UserType -import io.timelimit.android.databinding.ManageDevicePermissionsFragmentBinding import io.timelimit.android.integration.platform.NewPermissionStatus import io.timelimit.android.integration.platform.ProtectionLevel import io.timelimit.android.integration.platform.RuntimePermissionStatus -import io.timelimit.android.integration.platform.SystemPermission -import io.timelimit.android.livedata.ignoreUnchanged -import io.timelimit.android.livedata.liveDataFromNonNullValue -import io.timelimit.android.logic.AppLogic -import io.timelimit.android.logic.DefaultAppLogic -import io.timelimit.android.ui.main.ActivityViewModel -import io.timelimit.android.ui.main.ActivityViewModelHolder -import io.timelimit.android.ui.main.AuthenticationFab -import io.timelimit.android.ui.main.FragmentWithCustomTitle -import io.timelimit.android.ui.model.UpdateStateCommand -import io.timelimit.android.ui.model.execute -class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle { - companion object { - fun getPreviewText(device: Device, context: Context): String { - val permissionLabels = mutableListOf() +object ManageDevicePermissionsFragment { + fun getPreviewText(device: Device, context: Context): String { + val permissionLabels = mutableListOf() - if (device.currentUsageStatsPermission == RuntimePermissionStatus.Granted) { - permissionLabels.add(context.getString(R.string.manage_device_permissions_usagestats_title_short)) - } - - if (device.currentNotificationAccessPermission == NewPermissionStatus.Granted) { - permissionLabels.add(context.getString(R.string.manage_device_permission_notification_access_title)) - } - - if (device.currentProtectionLevel != ProtectionLevel.None) { - permissionLabels.add(context.getString(R.string.manage_device_permission_device_admin_title)) - } - - if (device.currentOverlayPermission == RuntimePermissionStatus.Granted) { - permissionLabels.add(context.getString(R.string.manage_device_permissions_overlay_title)) - } - - if (device.accessibilityServiceEnabled) { - permissionLabels.add(context.getString(R.string.manage_device_permission_accessibility_title)) - } - - return if (permissionLabels.isEmpty()) { - context.getString(R.string.manage_device_permissions_summary_none) - } else { - permissionLabels.joinToString(", ") - } - } - } - - private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder } - private val logic: AppLogic by lazy { DefaultAppLogic.with(requireContext()) } - private val auth: ActivityViewModel by lazy { activity.getActivityViewModel() } - private val args: ManageDevicePermissionsFragmentArgs by lazy { ManageDevicePermissionsFragmentArgs.fromBundle(requireArguments()) } - private val deviceEntry: LiveData by lazy { - logic.database.device().getDeviceById(args.deviceId) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val binding = ManageDevicePermissionsFragmentBinding.inflate(inflater, container, false) - - // auth - AuthenticationFab.manageAuthenticationFab( - fab = binding.fab, - shouldHighlight = auth.shouldHighlightAuthenticationButton, - authenticatedUser = auth.authenticatedUser, - fragment = this, - doesSupportAuth = liveDataFromNonNullValue(true) - ) - - auth.authenticatedUser.map { it?.second?.type == UserType.Parent }.observe(this, Observer { - binding.isUserSignedIn = it - }) - - // handlers - binding.handlers = object: ManageDevicePermissionsFragmentHandlers { - override fun openUsageStatsSettings() { - if (binding.isThisDevice == true) { - logic.platformIntegration.openSystemPermissionScren( - requireActivity(), - SystemPermission.UsageStats - ) - } - } - - override fun openNotificationAccessSettings() { - if (binding.isThisDevice == true) { - logic.platformIntegration.openSystemPermissionScren( - requireActivity(), - SystemPermission.Notification - ) - } - } - - override fun openDrawOverOtherAppsScreen() { - if (binding.isThisDevice == true) { - logic.platformIntegration.openSystemPermissionScren( - requireActivity(), - SystemPermission.Overlay - ) - } - } - - override fun openAccessibilitySettings() { - if (binding.isThisDevice == true) { - logic.platformIntegration.openSystemPermissionScren( - requireActivity(), - SystemPermission.AccessibilityService - ) - } - } - - override fun manageDeviceAdmin() { - if (binding.isThisDevice == true) { - logic.platformIntegration.openSystemPermissionScren( - requireActivity(), - SystemPermission.DeviceAdmin - ) - } - } - - override fun showAuthenticationScreen() { - activity.showAuthenticationScreen() - } - - override fun helpUsageStatsAccess() { - PermissionInfoHelpDialog.show(requireActivity(), SystemPermission.UsageStats) - } - - override fun helpNotificationAccess() { - PermissionInfoHelpDialog.show(requireActivity(), SystemPermission.Notification) - } - - override fun helpDrawOverOtherApps() { - PermissionInfoHelpDialog.show(requireActivity(), SystemPermission.Overlay) - } - - override fun helpAccesibility() { - PermissionInfoHelpDialog.show(requireActivity(), SystemPermission.AccessibilityService) - } + if (device.currentUsageStatsPermission == RuntimePermissionStatus.Granted) { + permissionLabels.add(context.getString(R.string.manage_device_permissions_usagestats_title_short)) } - // is this device - val isThisDevice = logic.deviceId.map { ownDeviceId -> ownDeviceId == args.deviceId }.ignoreUnchanged() + if (device.currentNotificationAccessPermission == NewPermissionStatus.Granted) { + permissionLabels.add(context.getString(R.string.manage_device_permission_notification_access_title)) + } - isThisDevice.observe(this, Observer { - binding.isThisDevice = it - }) + if (device.currentProtectionLevel != ProtectionLevel.None) { + permissionLabels.add(context.getString(R.string.manage_device_permission_device_admin_title)) + } - // permissions - deviceEntry.observe(this, Observer { - device -> + if (device.currentOverlayPermission == RuntimePermissionStatus.Granted) { + permissionLabels.add(context.getString(R.string.manage_device_permissions_overlay_title)) + } - if (device == null) { - requireActivity().execute(UpdateStateCommand.ManageDevice.Leave) - } else { - binding.usageStatsAccess = device.currentUsageStatsPermission - binding.notificationAccessPermission = device.currentNotificationAccessPermission - binding.protectionLevel = device.currentProtectionLevel - binding.overlayPermission = device.currentOverlayPermission - binding.accessibilityServiceEnabled = device.accessibilityServiceEnabled - } - }) + if (device.accessibilityServiceEnabled) { + permissionLabels.add(context.getString(R.string.manage_device_permission_accessibility_title)) + } - - return binding.root + return if (permissionLabels.isEmpty()) { + context.getString(R.string.manage_device_permissions_summary_none) + } else { + permissionLabels.joinToString(", ") + } } - - override fun onResume() { - super.onResume() - - logic.backgroundTaskLogic.syncDeviceStatusAsync() - } - - override fun getCustomTitle(): LiveData = deviceEntry.map { "${getString(R.string.manage_device_card_permission_title)} < ${it?.name} < ${getString(R.string.main_tab_overview)}" } -} - -interface ManageDevicePermissionsFragmentHandlers { - fun openUsageStatsSettings() - fun openNotificationAccessSettings() - fun openDrawOverOtherAppsScreen() - fun openAccessibilitySettings() - fun manageDeviceAdmin() - fun showAuthenticationScreen() - fun helpUsageStatsAccess() - fun helpNotificationAccess() - fun helpDrawOverOtherApps() - fun helpAccesibility() -} +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionGoals.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionGoals.kt new file mode 100644 index 0000000..3595951 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionGoals.kt @@ -0,0 +1,150 @@ +/* + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.manage.device.manage.permission + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckBox +import androidx.compose.material.icons.filled.CheckBoxOutlineBlank +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.timelimit.android.R +import io.timelimit.android.integration.platform.NewPermissionStatus +import io.timelimit.android.integration.platform.ProtectionLevel +import io.timelimit.android.integration.platform.RuntimePermissionStatus +import io.timelimit.android.integration.platform.SystemPermission + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun PermissionGoals(status: PermissionScreenContent.Status) { + PermissionGoal( + stringResource(R.string.manage_device_permission_goal_limit_title), + status.usageStats != RuntimePermissionStatus.NotGranted && + (!status.isQOrLater || status.overlay == RuntimePermissionStatus.Granted || status.accessibility), + ) { + if (status.usageStats != RuntimePermissionStatus.NotRequired) FlowRow { + Text(stringResource(R.string.manage_device_permission_goal_needs)) + PermissionIcon(SystemPermission.UsageStats, status.usageStats == RuntimePermissionStatus.Granted) + } + + if (status.isQOrLater) FlowRow { + Text(stringResource(R.string.manage_device_permission_goal_needs)) + PermissionIcon(SystemPermission.Overlay, status.overlay == RuntimePermissionStatus.Granted) + Text(stringResource(R.string.manage_device_permission_goal_or)) + PermissionIcon(SystemPermission.AccessibilityService, status.accessibility) + } + + if (status.usageStats == RuntimePermissionStatus.NotRequired && !status.isQOrLater) + Text(stringResource(R.string.manage_device_permission_goal_reached_by_old_android)) + } + + PermissionGoal( + stringResource(R.string.manage_device_permission_goal_floating_window), + status.accessibility + ) { + FlowRow { + Text(stringResource(R.string.manage_device_permission_goal_eventually_needs)) + PermissionIcon(SystemPermission.AccessibilityService, status.accessibility) + } + } + + PermissionGoal( + stringResource(R.string.manage_device_permission_goal_background_audio), + status.notificationAccess == NewPermissionStatus.Granted + ) { + FlowRow { + Text(stringResource(R.string.manage_device_permission_goal_needs)) + PermissionIcon( + SystemPermission.Notification, + status.notificationAccess == NewPermissionStatus.Granted + ) + } + } + + PermissionGoal( + stringResource(R.string.manage_device_permission_goal_manipulation_protection), + status.protectionLevel == ProtectionLevel.DeviceOwner + ) { + Text(stringResource( + if (status.maxProtectionLevel == null) + R.string.manage_device_permission_goal_manipulation_protection_check_remotely + else if (status.maxProtectionLevel == ProtectionLevel.DeviceOwner) + R.string.manage_device_permission_goal_needs_device_owner + else + R.string.manage_device_permission_goal_manipulation_protection_unavailable + )) + } +} + +@Composable +fun PermissionGoal( + title: String, + checked: Boolean, + content: @Composable () -> Unit +) { + Card ( + elevation = 4.dp + ) { + Row ( + verticalAlignment = Alignment.CenterVertically + ) { + Column ( + Modifier + .weight(1f) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(title, style = MaterialTheme.typography.h6) + + content() + } + + Spacer(modifier = Modifier.width(8.dp)) + + if (checked) Icon( + Icons.Default.CheckBox, + stringResource(R.string.manage_device_permission_goal_reached), + tint = MaterialTheme.colors.primary + ) else Icon( + Icons.Default.CheckBoxOutlineBlank, + stringResource(R.string.manage_device_permission_goal_missed) + ) + + Spacer(modifier = Modifier.width(8.dp)) + } + } +} + +@Composable +fun PermissionIcon(permission: SystemPermission, checked: Boolean) { + val icon = PermissionVisualization.getIcon(permission) + val label = PermissionVisualization.getLabel(LocalContext.current, false, permission) + + val tint = + if (checked) MaterialTheme.colors.primary + else LocalContentColor.current + + Icon( + icon, + label, + tint = tint + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreen.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreen.kt new file mode 100644 index 0000000..e589c01 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreen.kt @@ -0,0 +1,38 @@ +/* + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.manage.device.manage.permission + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun PermissionScreen( + content: PermissionScreenContent, + modifier: Modifier = Modifier +) { + Column ( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + ) { + PermissionScreenPermissionList(content.status, content.showDetails) + PermissionGoals(content.status) + } + + if (content.dialog != null) PermissionScreenDialog(content.dialog, content.status) +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreenContent.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreenContent.kt new file mode 100644 index 0000000..ee518b0 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreenContent.kt @@ -0,0 +1,43 @@ +/* + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.manage.device.manage.permission + +import io.timelimit.android.integration.platform.NewPermissionStatus +import io.timelimit.android.integration.platform.ProtectionLevel +import io.timelimit.android.integration.platform.RuntimePermissionStatus +import io.timelimit.android.integration.platform.SystemPermission + +data class PermissionScreenContent( + val status: Status, + val dialog: Dialog?, + val showDetails: (SystemPermission) -> Unit +) { + data class Status( + val notificationAccess: NewPermissionStatus, + val protectionLevel: ProtectionLevel, + val maxProtectionLevel: ProtectionLevel?, + val usageStats: RuntimePermissionStatus, + val overlay: RuntimePermissionStatus, + val accessibility: Boolean, + val isQOrLater: Boolean + ) + + data class Dialog( + val permission: SystemPermission, + val launchSystemSettings: (() -> Unit)?, + val close: () -> Unit + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreenDialog.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreenDialog.kt new file mode 100644 index 0000000..dc9bd48 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreenDialog.kt @@ -0,0 +1,85 @@ +/* + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.manage.device.manage.permission + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.timelimit.android.R +import io.timelimit.android.integration.platform.ProtectionLevel +import io.timelimit.android.integration.platform.SystemPermission + +@Composable +fun PermissionScreenDialog( + dialog: PermissionScreenContent.Dialog, + status: PermissionScreenContent.Status +) { + val permissionIcon = PermissionVisualization.getIcon(dialog.permission) + val permissionTitle = PermissionVisualization.getLabel(LocalContext.current, false, dialog.permission) + val permissionDescription = PermissionVisualization.getDescription(LocalContext.current, dialog.permission) + val permissionStatus = PermissionVisualization.getStatusText(LocalContext.current, dialog.permission, status) + + AlertDialog( + onDismissRequest = dialog.close, + title = { + Row ( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(permissionIcon, permissionTitle) + Text(permissionTitle) + } + }, + text = { + Column ( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(permissionDescription, style = MaterialTheme.typography.body1) + + Text(permissionStatus, style = MaterialTheme.typography.body1) + + if (dialog.permission == SystemPermission.DeviceAdmin) { + val message = + if (status.maxProtectionLevel != ProtectionLevel.DeviceOwner && status.maxProtectionLevel != null) R.string.manage_device_permission_device_owner_unsupported + else if (status.protectionLevel != ProtectionLevel.DeviceOwner) R.string.manage_device_permission_device_owner_not_granted + else null + + if (message != null) Text(stringResource(message), style = MaterialTheme.typography.body1) + } + + Text(stringResource(R.string.manage_device_permission_link_only_info)) + } + }, + dismissButton = { + TextButton(onClick = dialog.close) { + Text(stringResource(R.string.generic_cancel)) + } + }, + confirmButton = { + if (dialog.launchSystemSettings != null) { + TextButton(onClick = dialog.launchSystemSettings) { + Text(stringResource(R.string.manage_device_permission_btn_modify)) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreenPermissionList.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreenPermissionList.kt new file mode 100644 index 0000000..6da9ebb --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionScreenPermissionList.kt @@ -0,0 +1,79 @@ +/* + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.manage.device.manage.permission + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import io.timelimit.android.integration.platform.SystemPermission + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun PermissionScreenPermissionList( + status: PermissionScreenContent.Status, + showDetails: (SystemPermission) -> Unit +) { + val permissions = listOf( + SystemPermission.UsageStats, + SystemPermission.DeviceAdmin, + SystemPermission.Notification, + SystemPermission.Overlay, + SystemPermission.AccessibilityService + ) + + FlowRow ( + horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally), + modifier = Modifier.fillMaxWidth(), + maxItemsInEachRow = 3 + ) { + for (permission in permissions) { + val permissionIcon = PermissionVisualization.getIcon(permission) + val permissionStatus = PermissionVisualization.getStatusColor(status, permission) + val permissionLabel = PermissionVisualization.getLabel(LocalContext.current, false, permission) + + Box ( + Modifier.padding(vertical = 8.dp) + ) { + Card( + modifier = Modifier.clickable( + onClick = { showDetails(permission) }, + onClickLabel = permissionLabel + ), + backgroundColor = when (permissionStatus) { + PermissionVisualization.Status.Neutral -> MaterialTheme.colors.surface + PermissionVisualization.Status.Good -> MaterialTheme.colors.primary + }, + elevation = 8.dp + ) { + Icon( + permissionIcon, + permissionLabel, + Modifier + .padding(16.dp) + .size(64.dp) + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionVisualization.kt b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionVisualization.kt new file mode 100644 index 0000000..3dd6fe6 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/manage/device/manage/permission/PermissionVisualization.kt @@ -0,0 +1,110 @@ +/* + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.manage.device.manage.permission + +import android.content.Context +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.ui.graphics.vector.ImageVector +import io.timelimit.android.R +import io.timelimit.android.integration.platform.NewPermissionStatus +import io.timelimit.android.integration.platform.ProtectionLevel +import io.timelimit.android.integration.platform.RuntimePermissionStatus +import io.timelimit.android.integration.platform.SystemPermission + +object PermissionVisualization { + fun getIcon(permission: SystemPermission): ImageVector = when (permission) { + SystemPermission.UsageStats -> Icons.Default.BarChart + SystemPermission.DeviceAdmin -> Icons.Default.Shield + SystemPermission.Notification -> Icons.Default.Notifications + SystemPermission.Overlay -> Icons.Default.Layers + SystemPermission.AccessibilityService -> Icons.Default.AccessibilityNew + } + + fun getLabel(context: Context, short: Boolean, permission: SystemPermission): String = context.getString(when (permission) { + SystemPermission.UsageStats -> + if (short) R.string.manage_device_permissions_usagestats_title_short + else R.string.manage_device_permissions_usagestats_title + SystemPermission.DeviceAdmin -> R.string.manage_device_permission_device_admin_title + SystemPermission.Notification -> R.string.manage_device_permission_notification_access_title + SystemPermission.Overlay -> R.string.manage_device_permissions_overlay_title + SystemPermission.AccessibilityService -> R.string.manage_device_permission_accessibility_title + }) + + fun getDescription(context: Context, permission: SystemPermission): String = context.getString(when (permission) { + SystemPermission.UsageStats -> R.string.manage_device_permissions_usagestats_text + SystemPermission.DeviceAdmin -> R.string.manage_device_permission_device_admin_text + SystemPermission.Notification -> R.string.manage_device_permission_notification_access_text + SystemPermission.Overlay -> R.string.manage_device_permissions_overlay_text + SystemPermission.AccessibilityService -> R.string.manage_device_permission_accessibility_text + }) + + fun getStatusColor(status: PermissionScreenContent.Status, permission: SystemPermission): Status = when (permission) { + SystemPermission.UsageStats -> + if (status.usageStats == RuntimePermissionStatus.Granted) Status.Good + else Status.Neutral + SystemPermission.DeviceAdmin -> + if (status.protectionLevel != ProtectionLevel.None) Status.Good + else Status.Neutral + SystemPermission.Notification -> + if (status.notificationAccess == NewPermissionStatus.Granted) Status.Good + else Status.Neutral + SystemPermission.Overlay -> + if (status.overlay == RuntimePermissionStatus.Granted) Status.Good + else Status.Neutral + SystemPermission.AccessibilityService -> + if (status.accessibility) Status.Good + else Status.Neutral + } + + fun getStatusText(context: Context, permission: SystemPermission, status: PermissionScreenContent.Status): String = when (permission) { + SystemPermission.UsageStats -> + context.getString(when (status.usageStats) { + RuntimePermissionStatus.Granted -> R.string.manage_device_permission_status_granted + RuntimePermissionStatus.NotGranted -> R.string.manage_device_permission_status_not_granted + RuntimePermissionStatus.NotRequired -> R.string.manage_device_permission_status_not_required + }) + SystemPermission.DeviceAdmin -> + context.getString(when (status.protectionLevel) { + ProtectionLevel.None -> R.string.manage_device_permission_device_admin_text_disabled + ProtectionLevel.SimpleDeviceAdmin -> R.string.manage_device_permission_device_admin_text_simple + ProtectionLevel.PasswordDeviceAdmin -> R.string.manage_device_permission_device_admin_text_password + ProtectionLevel.DeviceOwner -> R.string.manage_device_permission_device_admin_text_owner + }) + SystemPermission.Notification -> + context.getString(when (status.notificationAccess) { + NewPermissionStatus.Granted -> R.string.manage_device_permission_status_granted + NewPermissionStatus.NotGranted -> R.string.manage_device_permission_status_not_granted + NewPermissionStatus.NotSupported -> R.string.manage_device_permission_status_not_supported + }) + SystemPermission.Overlay -> + context.getString(when (status.overlay) { + RuntimePermissionStatus.Granted -> R.string.manage_device_permission_status_granted + RuntimePermissionStatus.NotGranted -> R.string.manage_device_permission_status_not_granted + RuntimePermissionStatus.NotRequired -> R.string.manage_device_permission_status_not_required + }) + SystemPermission.AccessibilityService -> + context.getString(when (status.accessibility) { + true -> R.string.manage_device_permission_status_granted + false -> R.string.manage_device_permission_status_not_granted + }) + } + + enum class Status { + Neutral, + Good, + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/model/ActivityCommand.kt b/app/src/main/java/io/timelimit/android/ui/model/ActivityCommand.kt index 2f66109..960759e 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/ActivityCommand.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/ActivityCommand.kt @@ -15,9 +15,12 @@ */ package io.timelimit.android.ui.model +import io.timelimit.android.integration.platform.SystemPermission + sealed class ActivityCommand { object ShowCanNotAddDevicesInLocalModeDialogFragment: ActivityCommand() object ShowAddDeviceFragment: ActivityCommand() object ShowAuthenticationScreen: ActivityCommand() object ShowMissingPremiumDialog: ActivityCommand() + class LaunchSystemSettings(val permission: SystemPermission): ActivityCommand() } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt b/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt index 665ac03..82b987e 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt @@ -30,6 +30,7 @@ import io.timelimit.android.ui.model.launch.LaunchHandling import io.timelimit.android.ui.model.main.OverviewHandling import io.timelimit.android.ui.model.managechild.ManageChildHandling import io.timelimit.android.ui.model.managedevice.ManageDeviceHandling +import io.timelimit.android.ui.model.setup.SetupLocalModePermissions import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.* @@ -57,8 +58,8 @@ class MainModel(application: Application): AndroidViewModel(application) { } val activityModel = ActivityViewModel(application) + val logic = DefaultAppLogic.with(application) - private val logic = DefaultAppLogic.with(application) private val activityCommandInternal = Channel() private val authenticationScreenClosed = MutableSharedFlow(extraBufferCapacity = 1) @@ -112,6 +113,7 @@ class MainModel(application: Application): AndroidViewModel(application) { Case.simple<_, _, State.ManageChild> { state -> ManageChildHandling.processState(logic, state, updateMethod(::updateState)) }, Case.simple<_, _, State.ManageDevice> { state -> ManageDeviceHandling.processState(logic, activityCommandInternal, authenticationModelApi, state, updateMethod(::updateState)) }, Case.simple<_, _, State.DiagnoseScreen.DeviceOwner> { DeviceOwnerHandling.processState(logic, scope, authenticationModelApi, state) }, + Case.simple<_, _, State.Setup.DevicePermissions> { state -> SetupLocalModePermissions.handle(logic, activityCommandInternal, state, updateMethod(::updateState)) }, Case.simple<_, _, FragmentState> { state -> state.transform { val containerId = it.containerId ?: run { diff --git a/app/src/main/java/io/timelimit/android/ui/model/Screen.kt b/app/src/main/java/io/timelimit/android/ui/model/Screen.kt index aea2526..7111ba3 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/Screen.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/Screen.kt @@ -19,6 +19,7 @@ import androidx.compose.material.SnackbarHostState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Info import io.timelimit.android.R +import io.timelimit.android.ui.manage.device.manage.permission.PermissionScreenContent import io.timelimit.android.ui.model.diagnose.DeviceOwnerHandling import io.timelimit.android.ui.model.main.OverviewHandling import io.timelimit.android.ui.model.managedevice.ManageDeviceUser @@ -177,12 +178,9 @@ sealed class Screen( class ManageDevicePermissions( state: State, - toolbarIcons: List, - toolbarOptions: List, - fragment: FragmentState, - containerId: Int, + val content: PermissionScreenContent, override val backStack: List - ): FragmentScreen(state, toolbarIcons, toolbarOptions, fragment, containerId), ScreenWithBackStack, ScreenWithTitle { + ): Screen(state), ScreenWithBackStack, ScreenWithTitle { override val title = Title.StringResource(R.string.manage_device_card_permission_title) } @@ -215,6 +213,12 @@ sealed class Screen( ): Screen(state), ScreenWithAuthenticationFab, ScreenWithSnackbar, ScreenWithTitle { override val title = Title.StringResource(R.string.diagnose_dom_title) } + + class SetupDevicePermissionsScreen( + state: State, + val content: PermissionScreenContent, + val next: () -> Unit + ): Screen(state) } interface ScreenWithAuthenticationFab diff --git a/app/src/main/java/io/timelimit/android/ui/model/State.kt b/app/src/main/java/io/timelimit/android/ui/model/State.kt index 7927b18..6b0151f 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/State.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/State.kt @@ -21,6 +21,7 @@ import androidx.compose.material.icons.filled.DirectionsBike import androidx.compose.material.icons.filled.Phone import androidx.fragment.app.Fragment import io.timelimit.android.R +import io.timelimit.android.integration.platform.SystemPermission import io.timelimit.android.ui.contacts.ContactsFragment import io.timelimit.android.ui.diagnose.* import io.timelimit.android.ui.diagnose.exitreason.DiagnoseExitReasonFragment @@ -35,8 +36,6 @@ import io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceAdvance import io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceAdvancedFragmentArgs import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragment import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragmentArgs -import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragment -import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragmentArgs import io.timelimit.android.ui.manage.parent.ManageParentFragment import io.timelimit.android.ui.manage.parent.ManageParentFragmentArgs import io.timelimit.android.ui.manage.parent.link.LinkParentMailFragment @@ -228,9 +227,7 @@ sealed class State (val previous: State?): Serializable { object AdjustDefaultUserTimeout: Overlay() } } - class Permissions(previousMain: Main): Sub(previousMain, ManageDevicePermissionsFragment::class.java) { - override val arguments: Bundle get() = ManageDevicePermissionsFragmentArgs(deviceId).toBundle() - } + data class Permissions(val previousMain: Main, val currentDialog: SystemPermission? = null): Sub(previousMain, Fragment::class.java) class Features(previousMain: Main): Sub(previousMain, ManageDeviceFeaturesFragment::class.java) { override val arguments: Bundle get() = ManageDeviceFeaturesFragmentArgs(deviceId).toBundle() } @@ -256,7 +253,10 @@ sealed class State (val previous: State?): Serializable { class SetupTerms: FragmentStateLegacy(previous = null, fragmentClass = SetupTermsFragment::class.java) class SetupHelpInfo(previous: SetupTerms): FragmentStateLegacy(previous = previous, fragmentClass = SetupHelpInfoFragment::class.java) class SelectMode(previous: SetupHelpInfo): FragmentStateLegacy(previous = previous, fragmentClass = SetupSelectModeFragment::class.java) - class DevicePermissions(previous: SelectMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupDevicePermissionsFragment::class.java) + data class DevicePermissions( + val previousSelectMode: SelectMode, + val currentDialog: SystemPermission? = null + ): FragmentStateLegacy(previous = previousSelectMode, fragmentClass = Fragment::class.java) class LocalMode(previous: DevicePermissions): FragmentStateLegacy(previous = previous, fragmentClass = SetupLocalModeFragment::class.java) class RemoteChild(previous: SelectMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupRemoteChildFragment::class.java) class ParentMode(previous: SelectMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupParentModeFragment::class.java) diff --git a/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceHandling.kt b/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceHandling.kt index c3c9ea2..0624f44 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceHandling.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDeviceHandling.kt @@ -118,11 +118,15 @@ object ManageDeviceHandling { updateMethod(updateState) ) }, - Case.simple<_, _, State.ManageDevice.Permissions> { - processPermissionsState( - it, + Case.simple<_, _, State.ManageDevice.Permissions> {state -> + ManageDevicePermissions.processState( + scope, + logic, + activityCommand, + state, subBackStackLive, - deviceLive + deviceLive, + updateMethod(updateState) ) }, Case.simple<_, _, State.ManageDevice.Features> { @@ -142,21 +146,6 @@ object ManageDeviceHandling { ) } - private fun processPermissionsState( - stateLive: Flow, - parentBackStackLive: Flow>, - deviceLive: Flow - ): Flow = combine(stateLive, deviceLive, parentBackStackLive) { state, device, backStack -> - Screen.ManageDevicePermissions( - state, - state.toolbarIcons, - state.toolbarOptions, - state, - R.id.fragment_manage_device_permissions, - backStack - ) - } - private fun processFeaturesState( stateLive: Flow, parentBackStackLive: Flow>, diff --git a/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDevicePermissions.kt b/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDevicePermissions.kt new file mode 100644 index 0000000..52a96ea --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/model/managedevice/ManageDevicePermissions.kt @@ -0,0 +1,88 @@ +/* + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.model.managedevice + +import io.timelimit.android.data.model.Device +import io.timelimit.android.logic.AppLogic +import io.timelimit.android.ui.manage.device.manage.permission.PermissionScreenContent +import io.timelimit.android.ui.model.ActivityCommand +import io.timelimit.android.ui.model.BackStackItem +import io.timelimit.android.ui.model.Screen +import io.timelimit.android.ui.model.State +import io.timelimit.android.ui.model.setup.SetupLocalModePermissions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.flow.* + +object ManageDevicePermissions { + fun processState( + scope: CoroutineScope, + logic: AppLogic, + activityCommand: SendChannel, + stateLive: Flow, + parentBackStackLive: Flow>, + deviceLive: SharedFlow, + updateState: ((State.ManageDevice.Permissions) -> State) -> Unit + ): Flow { + val isCurrentDeviceLive = isCurrentDevice(logic, deviceLive).shareIn(scope, SharingStarted.Lazily, 1) + val deviceStatusLive = isCurrentDeviceLive.transformLatest { isCurrentDevice -> + if (isCurrentDevice) emitAll(SetupLocalModePermissions.deviceStatus(logic.platformIntegration)) + else emitAll(statusFromDevice(deviceLive)) + } + + return combine( + stateLive, deviceStatusLive, isCurrentDeviceLive, parentBackStackLive + ) { state, deviceStatus, isCurrentDevice, parentBackStack -> + Screen.ManageDevicePermissions( + state, + PermissionScreenContent( + status = deviceStatus, + dialog = state.currentDialog?.let { dialog -> + PermissionScreenContent.Dialog( + permission = dialog, + launchSystemSettings = if (isCurrentDevice) ({ + activityCommand.trySend(ActivityCommand.LaunchSystemSettings(dialog)) + + updateState { it.copy(currentDialog = null) } + }) else null, + close = { updateState { it.copy(currentDialog = null) } } + ) + }, + showDetails = { permission -> updateState { it.copy(currentDialog = permission) } } + ), + parentBackStack + ) + } + } + + private fun isCurrentDevice(logic: AppLogic, deviceLive: Flow): Flow { + val ownDeviceIdLive = logic.database.config().getOwnDeviceIdFlow() + + return combine(ownDeviceIdLive, deviceLive) { deviceId, device -> device.id == deviceId } + } + + private fun statusFromDevice(deviceLive: Flow): Flow = deviceLive.map { device -> + PermissionScreenContent.Status( + notificationAccess = device.currentNotificationAccessPermission, + protectionLevel = device.currentProtectionLevel, + maxProtectionLevel = null, + usageStats = device.currentUsageStatsPermission, + overlay = device.currentOverlayPermission, + accessibility = device.accessibilityServiceEnabled, + isQOrLater = device.qOrLater + ) + }.distinctUntilChanged() +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/model/setup/SetupLocalModePermissions.kt b/app/src/main/java/io/timelimit/android/ui/model/setup/SetupLocalModePermissions.kt new file mode 100644 index 0000000..9113122 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/model/setup/SetupLocalModePermissions.kt @@ -0,0 +1,80 @@ +/* + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.model.setup + +import io.timelimit.android.integration.platform.PlatformIntegration +import io.timelimit.android.logic.AppLogic +import io.timelimit.android.ui.manage.device.manage.permission.PermissionScreenContent +import io.timelimit.android.ui.model.ActivityCommand +import io.timelimit.android.ui.model.Screen +import io.timelimit.android.ui.model.State +import io.timelimit.android.util.AndroidVersion +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow + +object SetupLocalModePermissions { + fun handle( + logic: AppLogic, + activityCommand: SendChannel, + stateLive: Flow, + updateState: ((State.Setup.DevicePermissions) -> State) -> Unit + ): Flow { + val deviceStatusLive = deviceStatus(logic.platformIntegration) + + return combine(stateLive, deviceStatusLive) { state, deviceStatus -> + Screen.SetupDevicePermissionsScreen( + state, + PermissionScreenContent( + status = deviceStatus, + dialog = state.currentDialog?.let { dialog -> + PermissionScreenContent.Dialog( + permission = dialog, + launchSystemSettings = { + activityCommand.trySend(ActivityCommand.LaunchSystemSettings(dialog)) + + updateState { it.copy(currentDialog = null) } + }, + close = { updateState { it.copy(currentDialog = null) } } + ) + }, + showDetails = { permission -> updateState { it.copy(currentDialog = permission) } } + ) + ) { updateState { State.Setup.LocalMode(it) } } + } + } + + fun deviceStatus(platformIntegration: PlatformIntegration): Flow = flow { + while (true) { + emit( + PermissionScreenContent.Status( + notificationAccess = platformIntegration.getNotificationAccessPermissionStatus(), + protectionLevel = platformIntegration.getCurrentProtectionLevel(), + maxProtectionLevel = platformIntegration.maximumProtectionLevel, + usageStats = platformIntegration.getForegroundAppPermissionStatus(), + overlay = platformIntegration.getDrawOverOtherAppsPermissionStatus(true), + accessibility = platformIntegration.isAccessibilityServiceEnabled(), + isQOrLater = AndroidVersion.qOrLater + ) + ) + + delay(2000) + } + }.distinctUntilChanged() +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/setup/SetupDevicePermissionsFragment.kt b/app/src/main/java/io/timelimit/android/ui/setup/SetupDevicePermissionsFragment.kt deleted file mode 100644 index 09bd892..0000000 --- a/app/src/main/java/io/timelimit/android/ui/setup/SetupDevicePermissionsFragment.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * TimeLimit Copyright 2019 - 2023 Jonas Lochmann - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package io.timelimit.android.ui.setup - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import io.timelimit.android.async.Threads -import io.timelimit.android.databinding.FragmentSetupDevicePermissionsBinding -import io.timelimit.android.integration.platform.SystemPermission -import io.timelimit.android.logic.AppLogic -import io.timelimit.android.logic.DefaultAppLogic -import io.timelimit.android.ui.manage.device.manage.permission.PermissionInfoHelpDialog -import io.timelimit.android.ui.model.UpdateStateCommand -import io.timelimit.android.ui.model.execute - -class SetupDevicePermissionsFragment : Fragment() { - private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) } - private lateinit var binding: FragmentSetupDevicePermissionsBinding - - lateinit var refreshStatusRunnable: Runnable - - init { - refreshStatusRunnable = Runnable { - refreshStatus() - - Threads.mainThreadHandler.postDelayed(refreshStatusRunnable, 2000L) - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - binding = FragmentSetupDevicePermissionsBinding.inflate(inflater, container, false) - - binding.handlers = object: SetupDevicePermissionsHandlers { - override fun manageDeviceAdmin() { - logic.platformIntegration.openSystemPermissionScren(requireActivity(), SystemPermission.DeviceAdmin) - } - - override fun openUsageStatsSettings() { - logic.platformIntegration.openSystemPermissionScren(requireActivity(), SystemPermission.UsageStats) - } - - override fun openNotificationAccessSettings() { - logic.platformIntegration.openSystemPermissionScren(requireActivity(), SystemPermission.Notification) - } - - override fun openDrawOverOtherAppsScreen() { - logic.platformIntegration.openSystemPermissionScren(requireActivity(), SystemPermission.Overlay) - } - - override fun openAccessibilitySettings() { - logic.platformIntegration.openSystemPermissionScren(requireActivity(), SystemPermission.AccessibilityService) - } - - override fun gotoNextStep() { - requireActivity().execute(UpdateStateCommand.Setup.LocalMode) - } - - override fun helpUsageStatsAccess() { - PermissionInfoHelpDialog.show(requireActivity(), SystemPermission.UsageStats) - } - - override fun helpNotificationAccess() { - PermissionInfoHelpDialog.show(requireActivity(), SystemPermission.Notification) - } - - override fun helpDrawOverOtherApps() { - PermissionInfoHelpDialog.show(requireActivity(), SystemPermission.Overlay) - } - - override fun helpAccesibility() { - PermissionInfoHelpDialog.show(requireActivity(), SystemPermission.AccessibilityService) - } - } - - refreshStatus() - - return binding.root - } - - private fun refreshStatus() { - val platform = logic.platformIntegration - - binding.notificationAccessPermission = platform.getNotificationAccessPermissionStatus() - binding.protectionLevel = platform.getCurrentProtectionLevel() - binding.usageStatsAccess = platform.getForegroundAppPermissionStatus() - binding.overlayPermission = platform.getDrawOverOtherAppsPermissionStatus(true) - binding.accessibilityServiceEnabled = platform.isAccessibilityServiceEnabled() - } - - override fun onResume() { - super.onResume() - - // this additionally schedules it - refreshStatusRunnable.run() - } - - override fun onPause() { - super.onPause() - - Threads.mainThreadHandler.removeCallbacks(refreshStatusRunnable) - } -} - -interface SetupDevicePermissionsHandlers { - fun manageDeviceAdmin() - fun openUsageStatsSettings() - fun openNotificationAccessSettings() - fun openDrawOverOtherAppsScreen() - fun openAccessibilitySettings() - fun gotoNextStep() - fun helpUsageStatsAccess() - fun helpNotificationAccess() - fun helpDrawOverOtherApps() - fun helpAccesibility() -} diff --git a/app/src/main/java/io/timelimit/android/ui/setup/SetupDevicePermissionsScreen.kt b/app/src/main/java/io/timelimit/android/ui/setup/SetupDevicePermissionsScreen.kt new file mode 100644 index 0000000..cda800b --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/setup/SetupDevicePermissionsScreen.kt @@ -0,0 +1,65 @@ +/* + * TimeLimit Copyright 2019 - 2023 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package io.timelimit.android.ui.setup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.timelimit.android.R +import io.timelimit.android.integration.platform.RuntimePermissionStatus +import io.timelimit.android.ui.manage.device.manage.permission.PermissionScreen +import io.timelimit.android.ui.manage.device.manage.permission.PermissionScreenContent + +@Composable +fun SetupDevicePermissionsScreen( + content: PermissionScreenContent, + next: () -> Unit, + modifier: Modifier +) { + Column( + Modifier + .verticalScroll(rememberScrollState()) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + stringResource(R.string.setup_device_permissions_text_short), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + PermissionScreen(content) + + Button( + onClick = next, + modifier = Modifier.align(Alignment.End), + enabled = content.status.usageStats != RuntimePermissionStatus.NotGranted + ) { + Text(stringResource(R.string.wiazrd_next)) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_setup_device_permissions.xml b/app/src/main/res/layout/fragment_setup_device_permissions.xml deleted file mode 100644 index 5173c98..0000000 --- a/app/src/main/res/layout/fragment_setup_device_permissions.xml +++ /dev/null @@ -1,410 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -