From acda51c6798c22e914bc689d3e95e2f92c022baf Mon Sep 17 00:00:00 2001 From: Suhas Dissanayake Date: Thu, 25 Apr 2024 14:22:27 +0530 Subject: [PATCH] feat: permission request screen --- .../bnyro/clock/domain/model/Permission.kt | 73 +++++++++++++ .../clock/navigation/MainNavContainer.kt | 9 +- .../com/bnyro/clock/navigation/NavHost.kt | 20 +++- .../com/bnyro/clock/navigation/NavRoutes.kt | 2 + .../presentation/screens/alarm/AlarmScreen.kt | 18 ---- .../screens/permission/PermissionModel.kt | 19 ++++ .../screens/permission/PermissionScreen.kt | 52 +++++++++ .../permission/components/PermissionPage.kt | 101 ++++++++++++++++++ .../java/com/bnyro/clock/ui/MainActivity.kt | 38 +++---- .../java/com/bnyro/clock/util/AlarmHelper.kt | 12 +-- app/src/main/res/values/strings.xml | 6 ++ 11 files changed, 296 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/com/bnyro/clock/domain/model/Permission.kt create mode 100644 app/src/main/java/com/bnyro/clock/presentation/screens/permission/PermissionModel.kt create mode 100644 app/src/main/java/com/bnyro/clock/presentation/screens/permission/PermissionScreen.kt create mode 100644 app/src/main/java/com/bnyro/clock/presentation/screens/permission/components/PermissionPage.kt diff --git a/app/src/main/java/com/bnyro/clock/domain/model/Permission.kt b/app/src/main/java/com/bnyro/clock/domain/model/Permission.kt new file mode 100644 index 00000000..83b63da5 --- /dev/null +++ b/app/src/main/java/com/bnyro/clock/domain/model/Permission.kt @@ -0,0 +1,73 @@ +package com.bnyro.clock.domain.model + +import android.Manifest +import android.app.Activity +import android.app.AlarmManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.app.ActivityCompat +import androidx.core.net.toUri +import com.bnyro.clock.BuildConfig +import com.bnyro.clock.R + +sealed class Permission( + @StringRes + val titleRes: Int, + @StringRes + val descriptionRes: Int, + @DrawableRes + val iconRes: Int +) { + abstract fun hasPermission(context: Context): Boolean + abstract fun requestPermission(activity: Activity) + + object NotificationPermission : + Permission( + titleRes = R.string.notification_permission_title, + descriptionRes = R.string.notification_permission_description, + iconRes = R.drawable.ic_alarm + ) { + override fun hasPermission(context: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true + return ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } + + override fun requestPermission(activity: Activity) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return + ActivityCompat.requestPermissions( + activity, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + 1 + ) + } + } + + object AlarmPermission : Permission( + titleRes = R.string.alarm_permission_title, + descriptionRes = R.string.alarm_permission_description, + iconRes = R.drawable.ic_alarm + ) { + + override fun hasPermission(context: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + return alarmManager.canScheduleExactAlarms() + } + + override fun requestPermission(activity: Activity) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return + val intent = Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply { + data = "package:${BuildConfig.APPLICATION_ID}".toUri() + } + activity.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bnyro/clock/navigation/MainNavContainer.kt b/app/src/main/java/com/bnyro/clock/navigation/MainNavContainer.kt index 6d64603d..069b7f8e 100644 --- a/app/src/main/java/com/bnyro/clock/navigation/MainNavContainer.kt +++ b/app/src/main/java/com/bnyro/clock/navigation/MainNavContainer.kt @@ -8,10 +8,15 @@ import com.bnyro.clock.presentation.screens.settings.model.SettingsModel @Composable fun MainNavContainer( - settingsModel: SettingsModel, initialTab: HomeRoutes + settingsModel: SettingsModel, initialTab: HomeRoutes, + startDestination: String ) { val navController = rememberNavController() AppNavHost( - navController, settingsModel, initialTab = initialTab, modifier = Modifier.fillMaxSize() + navController, + settingsModel, + initialTab = initialTab, + startDestination = startDestination, + modifier = Modifier.fillMaxSize() ) } diff --git a/app/src/main/java/com/bnyro/clock/navigation/NavHost.kt b/app/src/main/java/com/bnyro/clock/navigation/NavHost.kt index 3b04e274..6e8ad9d3 100644 --- a/app/src/main/java/com/bnyro/clock/navigation/NavHost.kt +++ b/app/src/main/java/com/bnyro/clock/navigation/NavHost.kt @@ -12,6 +12,7 @@ import androidx.navigation.compose.composable import com.bnyro.clock.presentation.screens.alarm.model.AlarmModel import com.bnyro.clock.presentation.screens.alarmpicker.AlarmPickerScreen import com.bnyro.clock.presentation.screens.clock.model.ClockModel +import com.bnyro.clock.presentation.screens.permission.PermissionScreen import com.bnyro.clock.presentation.screens.settings.SettingsScreen import com.bnyro.clock.presentation.screens.settings.model.SettingsModel import com.bnyro.clock.presentation.screens.stopwatch.model.StopwatchModel @@ -22,6 +23,7 @@ fun AppNavHost( navController: NavHostController, settingsModel: SettingsModel, initialTab: HomeRoutes, + startDestination: String, modifier: Modifier = Modifier ) { val alarmModel: AlarmModel = viewModel() @@ -29,7 +31,7 @@ fun AppNavHost( val stopwatchModel: StopwatchModel = viewModel() val clockModel: ClockModel = viewModel() - NavHost(navController, startDestination = NavRoutes.Home.route, modifier = modifier) { + NavHost(navController, startDestination = startDestination, modifier = modifier) { composable(NavRoutes.Home.route, enterTransition = { slideIntoContainer( @@ -83,5 +85,21 @@ fun AppNavHost( navController.popBackStack() } } + + composable(NavRoutes.Permissions.route, + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Up, + initialOffset = { it / 4 }) + fadeIn() + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Down, + targetOffset = { it / 4 }) + fadeOut() + }) { + PermissionScreen { + navController.navigate(NavRoutes.Home.route) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/bnyro/clock/navigation/NavRoutes.kt b/app/src/main/java/com/bnyro/clock/navigation/NavRoutes.kt index b1d3818d..fff55a8d 100644 --- a/app/src/main/java/com/bnyro/clock/navigation/NavRoutes.kt +++ b/app/src/main/java/com/bnyro/clock/navigation/NavRoutes.kt @@ -13,4 +13,6 @@ sealed class NavRoutes( val routeWithArgs = "$route/{$alarmId}" val args = listOf(navArgument(alarmId) { NavType.LongType }) } + + object Permissions : NavRoutes("permissions") } \ No newline at end of file diff --git a/app/src/main/java/com/bnyro/clock/presentation/screens/alarm/AlarmScreen.kt b/app/src/main/java/com/bnyro/clock/presentation/screens/alarm/AlarmScreen.kt index 4a930c64..7da94acc 100644 --- a/app/src/main/java/com/bnyro/clock/presentation/screens/alarm/AlarmScreen.kt +++ b/app/src/main/java/com/bnyro/clock/presentation/screens/alarm/AlarmScreen.kt @@ -1,8 +1,5 @@ package com.bnyro.clock.presentation.screens.alarm -import android.content.Intent -import android.os.Build -import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height @@ -15,15 +12,12 @@ import androidx.compose.material.icons.rounded.Add import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import com.bnyro.clock.BuildConfig import com.bnyro.clock.R import com.bnyro.clock.navigation.TopBarScaffold import com.bnyro.clock.presentation.components.BlobIconBox @@ -31,7 +25,6 @@ import com.bnyro.clock.presentation.components.ClickableIcon import com.bnyro.clock.presentation.screens.alarm.components.AlarmFilterSection import com.bnyro.clock.presentation.screens.alarm.components.AlarmItem import com.bnyro.clock.presentation.screens.alarm.model.AlarmModel -import com.bnyro.clock.util.AlarmHelper @Composable fun AlarmScreen( @@ -43,17 +36,6 @@ fun AlarmScreen( val alarms by alarmModel.alarms.collectAsState() val filters by alarmModel.filters.collectAsState() - LaunchedEffect(Unit) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { - if (!AlarmHelper.hasPermission(context)) { - val intent = Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply { - data = "package:${BuildConfig.APPLICATION_ID}".toUri() - } - context.startActivity(intent) - } - } - } - TopBarScaffold(title = stringResource(R.string.alarm), onClickSettings, fab = { if (!alarmModel.showFilter) { FloatingActionButton( diff --git a/app/src/main/java/com/bnyro/clock/presentation/screens/permission/PermissionModel.kt b/app/src/main/java/com/bnyro/clock/presentation/screens/permission/PermissionModel.kt new file mode 100644 index 00000000..6ff1706f --- /dev/null +++ b/app/src/main/java/com/bnyro/clock/presentation/screens/permission/PermissionModel.kt @@ -0,0 +1,19 @@ +package com.bnyro.clock.presentation.screens.permission + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import com.bnyro.clock.domain.model.Permission + +class PermissionModel(application: Application) : + AndroidViewModel(application) { + val requiredPermissions = allPermissions.filter { + !it.hasPermission(application) + } + + companion object { + val allPermissions = listOf( + Permission.AlarmPermission, + Permission.NotificationPermission + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bnyro/clock/presentation/screens/permission/PermissionScreen.kt b/app/src/main/java/com/bnyro/clock/presentation/screens/permission/PermissionScreen.kt new file mode 100644 index 00000000..0cbc875f --- /dev/null +++ b/app/src/main/java/com/bnyro/clock/presentation/screens/permission/PermissionScreen.kt @@ -0,0 +1,52 @@ +package com.bnyro.clock.presentation.screens.permission + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.viewmodel.compose.viewModel +import com.bnyro.clock.presentation.screens.permission.components.PermissionRequestPage +import com.bnyro.clock.ui.MainActivity +import kotlinx.coroutines.launch + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun PermissionScreen(onClose: () -> Unit) { + val permissionModel: PermissionModel = viewModel() + val pagerState = rememberPagerState() { permissionModel.requiredPermissions.size } + val scope = rememberCoroutineScope() + val context = LocalContext.current + HorizontalPager( + state = pagerState, + ) { page -> + with(permissionModel.requiredPermissions[page]) { + PermissionRequestPage( + title = stringResource(id = titleRes), + subtitle = stringResource(id = descriptionRes), + onClickConfirm = { + requestPermission(context as MainActivity) + if (page + 1 < permissionModel.requiredPermissions.size) { + scope.launch { + pagerState.animateScrollToPage(page + 1) + } + } else { + onClose() + } + }, + onClickCancel = { + if (page + 1 < permissionModel.requiredPermissions.size) { + scope.launch { + pagerState.animateScrollToPage(page + 1) + } + } else { + onClose() + } + }, + icon = iconRes + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bnyro/clock/presentation/screens/permission/components/PermissionPage.kt b/app/src/main/java/com/bnyro/clock/presentation/screens/permission/components/PermissionPage.kt new file mode 100644 index 00000000..25878586 --- /dev/null +++ b/app/src/main/java/com/bnyro/clock/presentation/screens/permission/components/PermissionPage.kt @@ -0,0 +1,101 @@ +package com.bnyro.clock.presentation.screens.permission.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.bnyro.clock.R +import com.bnyro.clock.presentation.components.BlobIconBox + +@Composable +fun PermissionRequestPage( + title: String, + subtitle: String, + onClickConfirm: () -> Unit, + onClickCancel: () -> Unit, + @DrawableRes icon: Int, +) { + Column( + Modifier.fillMaxSize() + ) { + Column( + Modifier + .fillMaxWidth() + .weight(2f) + ) { + BlobIconBox(icon) + } + Column( + Modifier + .fillMaxWidth() + .padding(start = 8.dp, end = 8.dp, bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(48.dp)) + Button( + onClick = onClickConfirm, + contentPadding = PaddingValues(horizontal = 48.dp, vertical = 16.dp) + ) { + Text( + text = stringResource(R.string.allow), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + } + TextButton( + onClick = onClickCancel, colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + ) { + Text( + text = stringResource(R.string.maybe_later), + style = MaterialTheme.typography.bodyLarge + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun PermissionRequestPagePreview() { + PermissionRequestPage( + title = "Enable Alarm Permissions", + subtitle = "Alarm Permissions are required to schedule alarms", + onClickConfirm = {}, + onClickCancel = {}, + icon = R.drawable.ic_alarm + ) +} diff --git a/app/src/main/java/com/bnyro/clock/ui/MainActivity.kt b/app/src/main/java/com/bnyro/clock/ui/MainActivity.kt index e1d5d4f2..bfde7813 100644 --- a/app/src/main/java/com/bnyro/clock/ui/MainActivity.kt +++ b/app/src/main/java/com/bnyro/clock/ui/MainActivity.kt @@ -1,12 +1,9 @@ package com.bnyro.clock.ui -import android.Manifest import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection -import android.content.pm.PackageManager -import android.os.Build import android.os.Bundle import android.os.IBinder import android.provider.AlarmClock @@ -18,16 +15,16 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.core.app.ActivityCompat import androidx.lifecycle.viewmodel.compose.viewModel import com.bnyro.clock.domain.model.Alarm import com.bnyro.clock.navigation.HomeRoutes import com.bnyro.clock.navigation.MainNavContainer +import com.bnyro.clock.navigation.NavRoutes import com.bnyro.clock.navigation.homeRoutes import com.bnyro.clock.presentation.features.AlarmReceiverDialog import com.bnyro.clock.presentation.features.TimerReceiverDialog +import com.bnyro.clock.presentation.screens.permission.PermissionModel import com.bnyro.clock.presentation.screens.settings.model.SettingsModel import com.bnyro.clock.presentation.screens.stopwatch.model.StopwatchModel import com.bnyro.clock.presentation.screens.timer.model.TimerModel @@ -94,6 +91,16 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val allPermissions = PermissionModel.allPermissions + val requiredPermissions = allPermissions.any { + !it.hasPermission(this) + } + val startDestination = if (requiredPermissions) { + NavRoutes.Permissions.route + } else { + NavRoutes.Home.route + } + initialTab = when (intent?.action) { SHOW_STOPWATCH_ACTION -> HomeRoutes.Stopwatch AlarmClock.ACTION_SET_ALARM, AlarmClock.ACTION_SHOW_ALARMS -> HomeRoutes.Alarm @@ -133,13 +140,9 @@ class MainActivity : ComponentActivity() { getInitialTimer()?.let { TimerReceiverDialog(it) } - MainNavContainer(settingsModel, initialTab) + MainNavContainer(settingsModel, initialTab, startDestination) } } - - LaunchedEffect(Unit) { - requestNotificationPermissions() - } } } @@ -187,21 +190,6 @@ class MainActivity : ComponentActivity() { return intent.getIntExtra(AlarmClock.EXTRA_LENGTH, 0).takeIf { it > 0 } } - private fun requestNotificationPermissions() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return - if (ActivityCompat.checkSelfPermission( - this@MainActivity, - Manifest.permission.POST_NOTIFICATIONS - ) != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions( - this@MainActivity, - arrayOf(Manifest.permission.POST_NOTIFICATIONS), - 1 - ) - } - } - companion object { const val SHOW_STOPWATCH_ACTION = "com.bnyro.clock.SHOW_STOPWATCH_ACTION" } diff --git a/app/src/main/java/com/bnyro/clock/util/AlarmHelper.kt b/app/src/main/java/com/bnyro/clock/util/AlarmHelper.kt index a9b04c68..e86e3510 100644 --- a/app/src/main/java/com/bnyro/clock/util/AlarmHelper.kt +++ b/app/src/main/java/com/bnyro/clock/util/AlarmHelper.kt @@ -1,14 +1,14 @@ package com.bnyro.clock.util +import android.annotation.SuppressLint import android.app.AlarmManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.os.Build import android.util.Log -import androidx.annotation.RequiresApi import com.bnyro.clock.R import com.bnyro.clock.domain.model.Alarm +import com.bnyro.clock.domain.model.Permission import com.bnyro.clock.ui.MainActivity import com.bnyro.clock.util.receivers.AlarmReceiver import java.util.Calendar @@ -18,7 +18,9 @@ import java.util.GregorianCalendar object AlarmHelper { const val EXTRA_ID = "alarm_id" + @SuppressLint("ScheduleExactAlarm") fun enqueue(context: Context, alarm: Alarm) { + if (!Permission.AlarmPermission.hasPermission(context)) return cancel(context, alarm) if (!alarm.enabled) { return @@ -32,12 +34,6 @@ object AlarmHelper { alarmManager.setAlarmClock(alarmInfo, getPendingIntent(context, alarm)) } - @RequiresApi(Build.VERSION_CODES.S) - fun hasPermission(context: Context): Boolean { - val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - return alarmManager.canScheduleExactAlarms() - } - fun cancel(context: Context, alarm: Alarm) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager alarmManager.cancel(getPendingIntent(context, alarm)) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6542401f..5bcb4f2a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -121,4 +121,10 @@ Show Widget background Show Time Digital Clock Widget + Allow Notifications to ensure you never miss an alarm or timer. We\'ll only send alerts for your set alarms and timers. + Notification Permission + To wake you up on time, Clock You needs permission to schedule alarms. + Alarm Permission + Allow + Maybe later \ No newline at end of file