From 1fbc2b16617c2598e945c25bc3bff6da175b363d Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Sat, 25 Jun 2022 12:33:38 -0500 Subject: [PATCH] Android 13 runtime notification permissions --- app/build.gradle.kts | 3 +- app/src/main/AndroidManifest.xml | 2 + .../andlib/utility/AndroidUtilities.java | 4 + .../todoroo/astrid/ui/ReminderControlSet.kt | 115 +++++++++++------- .../astrid/ui/ReminderControlSetViewModel.kt | 25 ++++ .../org/tasks/compose/AddReminderDialog.kt | 53 +++----- app/src/main/res/values/strings.xml | 2 + buildSrc/src/main/kotlin/Versions.kt | 1 + deps_fdroid.txt | 21 +++- deps_googleplay.txt | 21 +++- 10 files changed, 154 insertions(+), 93 deletions(-) create mode 100644 app/src/main/java/com/todoroo/astrid/ui/ReminderControlSetViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2429feef4..d712bc27e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,7 +233,8 @@ dependencies { implementation("androidx.compose.material:material-icons-extended:${Versions.compose}") releaseCompileOnly("androidx.compose.ui:ui-tooling:${Versions.compose}") - implementation("com.google.accompanist:accompanist-flowlayout:0.24.11-rc") + implementation("com.google.accompanist:accompanist-flowlayout:${Versions.accompanist}") + implementation("com.google.accompanist:accompanist-permissions:${Versions.accompanist}") googleplayImplementation("com.google.firebase:firebase-crashlytics:${Versions.crashlytics}") googleplayImplementation("com.google.firebase:firebase-analytics:${Versions.analytics}") { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 56e5bb397..98e97937d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + diff --git a/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java b/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java index b8958dbc2..e06473a9c 100644 --- a/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java +++ b/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java @@ -163,6 +163,10 @@ public class AndroidUtilities { return VERSION.SDK_INT >= VERSION_CODES.S; } + public static boolean atLeastTiramisu() { + return VERSION.SDK_INT >= VERSION_CODES.TIRAMISU; + } + public static void assertMainThread() { if (BuildConfig.DEBUG && !isMainThread()) { throw new IllegalStateException("Should be called from main thread"); diff --git a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt index 346b27c58..f7dacb12b 100644 --- a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt @@ -17,11 +17,17 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.viewModels +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionStatus +import com.google.accompanist.permissions.rememberPermissionState import com.todoroo.andlib.utility.AndroidUtilities +import com.todoroo.andlib.utility.AndroidUtilities.atLeastTiramisu import dagger.hilt.android.AndroidEntryPoint import org.tasks.R import org.tasks.activities.DateAndTimePickerActivity @@ -49,13 +55,15 @@ class ReminderControlSet : TaskEditControlComposeFragment() { @Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var alarmToString: AlarmToString - private val showCustomDialog = mutableStateOf(false) - private val showRandomDialog = mutableStateOf(false) + data class ViewState( + val showCustomDialog: Boolean = false, + val showRandomDialog: Boolean = false, + ) + private val ringMode = mutableStateOf(0) + private val vm: ReminderControlSetViewModel by viewModels() override fun createView(savedInstanceState: Bundle?) { - showCustomDialog.value = savedInstanceState?.getBoolean(CUSTOM_DIALOG_VISIBLE) ?: false - showRandomDialog.value = savedInstanceState?.getBoolean(RANDOM_DIALOG_VISIBLE) ?: false when { viewModel.ringNonstop!! -> setRingMode(2) viewModel.ringFiveTimes!! -> setRingMode(1) @@ -63,13 +71,6 @@ class ReminderControlSet : TaskEditControlComposeFragment() { } } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - - outState.putBoolean(CUSTOM_DIALOG_VISIBLE, showCustomDialog.value) - outState.putBoolean(RANDOM_DIALOG_VISIBLE, showRandomDialog.value) - } - private fun onClickRingType() { val modes = resources.getStringArray(R.array.reminder_ring_modes) val ringMode = when { @@ -102,11 +103,11 @@ class ReminderControlSet : TaskEditControlComposeFragment() { getString(R.string.when_overdue) -> addAlarmRow(whenOverdue(id)) getString(R.string.randomly) -> - addRandomAlarm() + vm.showRandomDialog(visible = true) getString(R.string.pick_a_date_and_time) -> addNewAlarm() getString(R.string.repeat_option_custom) -> - addCustomAlarm() + vm.showCustomDialog(visible = true) } } @@ -125,11 +126,65 @@ class ReminderControlSet : TaskEditControlComposeFragment() { } } - @OptIn(ExperimentalComposeUiApi::class) + @OptIn(ExperimentalComposeUiApi::class, ExperimentalPermissionsApi::class) @Composable override fun Body() { - val alarms = viewModel.selectedAlarms.collectAsStateLifecycleAware() + val viewState = vm.viewState.collectAsStateLifecycleAware() + val current: ViewState = viewState.value + val notificationPermissions = if (atLeastTiramisu()) { + rememberPermissionState( + android.Manifest.permission.POST_NOTIFICATIONS + ) + } else { + null + } + when (notificationPermissions?.status ?: PermissionStatus.Granted) { + PermissionStatus.Granted -> + Alarms() + is PermissionStatus.Denied -> { + Column( + modifier = Modifier.clickable { + notificationPermissions?.launchPermissionRequest() + } + ) { + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(id = R.string.enable_reminders), + color = colorResource(id = R.color.red_500), + ) + Text( + text = stringResource(id = R.string.enable_reminders_description), + style = MaterialTheme.typography.caption, + color = colorResource(id = R.color.red_500), + ) + Spacer(modifier = Modifier.height(20.dp)) + } + } + } + + AddReminderDialog.AddCustomReminderDialog( + openDialog = current.showCustomDialog, + addAlarm = this::addAlarmRow, + closeDialog = { + vm.showCustomDialog(visible = false) + AndroidUtilities.hideKeyboard(activity) + } + ) + + AddReminderDialog.AddRandomReminderDialog( + openDialog = current.showRandomDialog, + addAlarm = this::addAlarmRow, + closeDialog = { + vm.showRandomDialog(visible = false) + AndroidUtilities.hideKeyboard(activity) + } + ) + } + + @Composable + fun Alarms() { Column { + val alarms = viewModel.selectedAlarms.collectAsStateLifecycleAware() Spacer(modifier = Modifier.height(8.dp)) alarms.value.forEach { alarm -> AlarmRow(alarmToString.toString(alarm)) { @@ -181,26 +236,6 @@ class ReminderControlSet : TaskEditControlComposeFragment() { } Spacer(modifier = Modifier.height(8.dp)) } - - val openCustomDialog = remember { showCustomDialog } - AddReminderDialog.AddCustomReminderDialog( - openCustomDialog, - addAlarm = this::addAlarmRow, - closeDialog = { - openCustomDialog.value = false - AndroidUtilities.hideKeyboard(activity) - } - ) - - val openRandomDialog = remember { showRandomDialog } - AddReminderDialog.AddRandomReminderDialog( - openRandomDialog, - addAlarm = this::addAlarmRow, - closeDialog = { - openRandomDialog.value = false - AndroidUtilities.hideKeyboard(activity) - } - ) } override val icon = R.drawable.ic_outline_notifications_24px @@ -235,14 +270,6 @@ class ReminderControlSet : TaskEditControlComposeFragment() { startActivityForResult(intent, REQUEST_NEW_ALARM) } - private fun addCustomAlarm() { - showCustomDialog.value = true - } - - private fun addRandomAlarm() { - showRandomDialog.value = true - } - private val options: List get() { val options: MutableList = ArrayList() @@ -264,8 +291,6 @@ class ReminderControlSet : TaskEditControlComposeFragment() { companion object { const val TAG = R.string.TEA_ctrl_reminders_pref private const val REQUEST_NEW_ALARM = 12152 - private const val CUSTOM_DIALOG_VISIBLE = "custom_dialog_visible" - private const val RANDOM_DIALOG_VISIBLE = "random_dialog_visible" } } diff --git a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSetViewModel.kt b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSetViewModel.kt new file mode 100644 index 000000000..c37760c04 --- /dev/null +++ b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSetViewModel.kt @@ -0,0 +1,25 @@ +package com.todoroo.astrid.ui + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class ReminderControlSetViewModel : ViewModel() { + private val _viewState = MutableStateFlow(ReminderControlSet.ViewState()) + + val viewState: StateFlow + get() = _viewState.asStateFlow() + + fun showCustomDialog(visible: Boolean) { + _viewState.value = _viewState.value.copy( + showCustomDialog = visible + ) + } + + fun showRandomDialog(visible: Boolean) { + _viewState.value = _viewState.value.copy( + showRandomDialog = visible + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/compose/AddReminderDialog.kt b/app/src/main/java/org/tasks/compose/AddReminderDialog.kt index be2c473ce..8e929cd9f 100644 --- a/app/src/main/java/org/tasks/compose/AddReminderDialog.kt +++ b/app/src/main/java/org/tasks/compose/AddReminderDialog.kt @@ -3,31 +3,14 @@ package org.tasks.compose import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.AlertDialog -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.RadioButton -import androidx.compose.material.Text -import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Autorenew -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.ExperimentalComposeUiApi @@ -56,13 +39,13 @@ import java.util.concurrent.TimeUnit object AddReminderDialog { @Composable fun AddRandomReminderDialog( - openDialog: MutableState, + openDialog: Boolean, addAlarm: (Alarm) -> Unit, closeDialog: () -> Unit, ) { val time = rememberSaveable { mutableStateOf(15) } val units = rememberSaveable { mutableStateOf(0) } - if (openDialog.value) { + if (openDialog) { AlertDialog( onDismissRequest = closeDialog, text = { AddRandomReminder(openDialog, time, units) }, @@ -89,7 +72,7 @@ object AddReminderDialog { @Composable fun AddCustomReminderDialog( - openDialog: MutableState, + openDialog: Boolean, addAlarm: (Alarm) -> Unit, closeDialog: () -> Unit, ) { @@ -99,7 +82,7 @@ object AddReminderDialog { val interval = rememberSaveable { mutableStateOf(0) } val recurringUnits = rememberSaveable { mutableStateOf(0) } val repeat = rememberSaveable { mutableStateOf(0) } - if (openDialog.value) { + if (openDialog) { if (!openRecurringDialog.value) { AlertDialog( onDismissRequest = closeDialog, @@ -179,7 +162,7 @@ object AddReminderDialog { onDismissRequest = closeDialog, text = { AddRecurringReminder( - openDialog, + openDialog.value, interval, units, repeat, @@ -209,7 +192,7 @@ object AddReminderDialog { @Composable fun AddRandomReminder( - visible: MutableState, + visible: Boolean, time: MutableState, units: MutableState, ) { @@ -237,7 +220,7 @@ object AddReminderDialog { @Composable fun AddCustomReminder( - visible: MutableState, + visible: Boolean, time: MutableState, units: MutableState, interval: MutableState, @@ -313,7 +296,7 @@ object AddReminderDialog { @Composable fun AddRecurringReminder( - openDialog: MutableState, + openDialog: Boolean, interval: MutableState, units: MutableState, repeat: MutableState @@ -374,7 +357,7 @@ object AddReminderDialog { @ExperimentalComposeUiApi @Composable -fun ShowKeyboard(visible: MutableState, focusRequester: FocusRequester) { +fun ShowKeyboard(visible: Boolean, focusRequester: FocusRequester) { val keyboardController = LocalSoftwareKeyboardController.current LaunchedEffect(visible) { focusRequester.freeFocus() @@ -484,7 +467,7 @@ fun BodyText(modifier: Modifier = Modifier, text: String) { fun AddCustomReminderOne() = MdcTheme { AddReminderDialog.AddCustomReminder( - visible = remember { mutableStateOf(true) }, + visible = true, time = remember { mutableStateOf(1) }, units = remember { mutableStateOf(0) }, interval = remember { mutableStateOf(0) }, @@ -501,7 +484,7 @@ fun AddCustomReminderOne() = fun AddCustomReminder() = MdcTheme { AddReminderDialog.AddCustomReminder( - visible = remember { mutableStateOf(true) }, + visible = true, time = remember { mutableStateOf(15) }, units = remember { mutableStateOf(1) }, interval = remember { mutableStateOf(0) }, @@ -518,7 +501,7 @@ fun AddCustomReminder() = fun AddRepeatingReminderOne() = MdcTheme { AddReminderDialog.AddRecurringReminder( - openDialog = remember { mutableStateOf(true) }, + openDialog = true, interval = remember { mutableStateOf(1) }, units = remember { mutableStateOf(0) }, repeat = remember { mutableStateOf(1) }, @@ -532,7 +515,7 @@ fun AddRepeatingReminderOne() = fun AddRepeatingReminder() = MdcTheme { AddReminderDialog.AddRecurringReminder( - openDialog = remember { mutableStateOf(true) }, + openDialog = true, interval = remember { mutableStateOf(15) }, units = remember { mutableStateOf(1) }, repeat = remember { mutableStateOf(4) }, @@ -546,7 +529,7 @@ fun AddRepeatingReminder() = fun AddRandomReminderOne() = MdcTheme { AddReminderDialog.AddRandomReminder( - visible = remember { mutableStateOf(true) }, + visible = true, time = remember { mutableStateOf(1) }, units = remember { mutableStateOf(0) } ) @@ -559,7 +542,7 @@ fun AddRandomReminderOne() = fun AddRandomReminder() = MdcTheme { AddReminderDialog.AddRandomReminder( - visible = remember { mutableStateOf(true) }, + visible = true, time = remember { mutableStateOf(15) }, units = remember { mutableStateOf(1) } ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e61904608..cd3ef4c7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -734,4 +734,6 @@ File %1$s contained %2$s.\n\n Dismiss Too much information? You can customize this screen by rearranging or removing fields + Enable reminders + Reminders are disabled in Android Settings diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 6b9c4b263..8ca7037ce 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -20,4 +20,5 @@ object Versions { const val markwon = "4.6.2" const val compose = "1.2.0-rc02" const val compose_theme_adapter = "1.1.11" + const val accompanist = "0.24.12-rc" } diff --git a/deps_fdroid.txt b/deps_fdroid.txt index 3c10ff104..81febf895 100644 --- a/deps_fdroid.txt +++ b/deps_fdroid.txt @@ -556,16 +556,25 @@ +| +--- androidx.compose.material:material:1.2.0-beta03 -> 1.2.0-rc02 (*) +| +--- com.google.android.material:material:1.7.0-alpha02 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.21 (*) -++--- androidx.activity:activity-compose:1.4.0 +++--- androidx.activity:activity-compose:1.4.0 -> 1.5.0-rc01 ++| +--- androidx.activity:activity-ktx:1.5.0-rc01 -> 1.6.0-alpha05 (*) +| +--- androidx.compose.runtime:runtime:1.0.1 -> 1.2.0-rc02 (*) +| +--- androidx.compose.runtime:runtime-saveable:1.0.1 -> 1.2.0-rc02 (*) -+| +--- androidx.activity:activity-ktx:1.4.0 -> 1.6.0-alpha05 (*) +| +--- androidx.compose.ui:ui:1.0.1 -> 1.2.0-rc02 (*) -+| \--- org.jetbrains.kotlin:kotlin-stdlib:1.5.31 -> 1.6.21 (*) ++| +--- androidx.lifecycle:lifecycle-common-java8:2.5.0-rc01 (*) ++| \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.21 (*) ++--- androidx.compose.material:material-icons-extended:1.2.0-rc02 +| +--- androidx.compose.material:material-icons-core:1.2.0-rc02 (*) +| +--- androidx.compose.runtime:runtime:1.2.0-rc02 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.6.21 -+\--- com.google.accompanist:accompanist-flowlayout:0.24.11-rc -+ +--- androidx.compose.foundation:foundation:1.2.0-rc01 -> 1.2.0-rc02 (*) -+ \--- androidx.compose.ui:ui-util:1.2.0-rc01 -> 1.2.0-rc02 (*) +++--- com.google.accompanist:accompanist-flowlayout:0.24.12-rc ++| +--- androidx.compose.foundation:foundation:1.2.0-rc02 (*) ++| \--- androidx.compose.ui:ui-util:1.2.0-rc02 (*) ++\--- com.google.accompanist:accompanist-permissions:0.24.12-rc ++ +--- androidx.activity:activity-compose:1.5.0-rc01 (*) ++ +--- androidx.compose.foundation:foundation:1.2.0-rc02 (*) ++ +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0 -> 1.6.1 (*) ++ \--- io.github.aakira:napier:1.4.1 ++ \--- io.github.aakira:napier-android:1.4.1 ++ +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.32 -> 1.6.21 (*) ++ \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.4.32 -> 1.6.21 diff --git a/deps_googleplay.txt b/deps_googleplay.txt index dafe3040c..315d84444 100644 --- a/deps_googleplay.txt +++ b/deps_googleplay.txt @@ -692,16 +692,25 @@ +| +--- androidx.compose.material:material:1.2.0-beta03 -> 1.2.0-rc02 (*) +| +--- com.google.android.material:material:1.7.0-alpha02 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.21 (*) -++--- androidx.activity:activity-compose:1.4.0 +++--- androidx.activity:activity-compose:1.4.0 -> 1.5.0-rc01 ++| +--- androidx.activity:activity-ktx:1.5.0-rc01 -> 1.6.0-alpha05 (*) +| +--- androidx.compose.runtime:runtime:1.0.1 -> 1.2.0-rc02 (*) +| +--- androidx.compose.runtime:runtime-saveable:1.0.1 -> 1.2.0-rc02 (*) -+| +--- androidx.activity:activity-ktx:1.4.0 -> 1.6.0-alpha05 (*) +| +--- androidx.compose.ui:ui:1.0.1 -> 1.2.0-rc02 (*) -+| \--- org.jetbrains.kotlin:kotlin-stdlib:1.5.31 -> 1.6.21 (*) ++| +--- androidx.lifecycle:lifecycle-common-java8:2.5.0-rc01 (*) ++| \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.21 (*) ++--- androidx.compose.material:material-icons-extended:1.2.0-rc02 +| +--- androidx.compose.material:material-icons-core:1.2.0-rc02 (*) +| +--- androidx.compose.runtime:runtime:1.2.0-rc02 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.6.21 -+\--- com.google.accompanist:accompanist-flowlayout:0.24.11-rc -+ +--- androidx.compose.foundation:foundation:1.2.0-rc01 -> 1.2.0-rc02 (*) -+ \--- androidx.compose.ui:ui-util:1.2.0-rc01 -> 1.2.0-rc02 (*) +++--- com.google.accompanist:accompanist-flowlayout:0.24.12-rc ++| +--- androidx.compose.foundation:foundation:1.2.0-rc02 (*) ++| \--- androidx.compose.ui:ui-util:1.2.0-rc02 (*) ++\--- com.google.accompanist:accompanist-permissions:0.24.12-rc ++ +--- androidx.activity:activity-compose:1.5.0-rc01 (*) ++ +--- androidx.compose.foundation:foundation:1.2.0-rc02 (*) ++ +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0 -> 1.6.1 (*) ++ \--- io.github.aakira:napier:1.4.1 ++ \--- io.github.aakira:napier-android:1.4.1 ++ +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.32 -> 1.6.21 (*) ++ \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.4.32 -> 1.6.21