From ed8c60e52f9de447a7877b6b6d78c4c9b179c3e1 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Sat, 25 Nov 2023 07:06:47 -0600 Subject: [PATCH] Implement runtime notification permission; bump targetSdk to 33 --- uhabits-android/build.gradle.kts | 4 +- .../common/views/FrequencyChartTest.kt | 4 +- .../activities/common/views/ScoreChartTest.kt | 4 +- uhabits-android/src/main/AndroidManifest.xml | 1 + .../isoron/platform/gui/AndroidDataView.kt | 24 +++++------ .../common/views/ScrollableChart.kt | 2 +- .../habits/list/ListHabitsActivity.kt | 40 ++++++++++++++++++- .../core/reminders/ReminderScheduler.kt | 5 +++ 8 files changed, 66 insertions(+), 18 deletions(-) diff --git a/uhabits-android/build.gradle.kts b/uhabits-android/build.gradle.kts index ac7b5cfaf..12c91c3f2 100644 --- a/uhabits-android/build.gradle.kts +++ b/uhabits-android/build.gradle.kts @@ -42,13 +42,13 @@ kotlin { android { - compileSdk = 32 + compileSdk = 33 defaultConfig { versionCode = 20200 versionName = "2.2.0" minSdk = 28 - targetSdk = 32 + targetSdk = 33 applicationId = "org.isoron.uhabits" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt index 0c4225f99..3fb97bb4c 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.activities.common.views +import android.view.MotionEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import org.isoron.uhabits.BaseViewTest @@ -52,7 +53,8 @@ class FrequencyChartTest : BaseViewTest() { @Test @Throws(Throwable::class) fun testRender_withDataOffset() { - view.onScroll(null, null, -dpToPixels(150), 0f) + val e = MotionEvent.obtain(0, 0, 0, 0f, 0f, 0) + view.onScroll(e, e, -dpToPixels(150), 0f) view.invalidate() assertRenders(view, BASE_PATH + "renderDataOffset.png") } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt index 4f504d8be..83d695ca4 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.activities.common.views +import android.view.MotionEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import org.isoron.uhabits.BaseViewTest @@ -63,7 +64,8 @@ class ScoreChartTest : BaseViewTest() { @Test @Throws(Throwable::class) fun testRender_withDataOffset() { - view.onScroll(null, null, -dpToPixels(150), 0f) + val e = MotionEvent.obtain(0, 0, 0, 0f, 0f, 0) + view.onScroll(e, e, -dpToPixels(150), 0f) view.invalidate() assertRenders(view, BASE_PATH + "renderDataOffset.png") } diff --git a/uhabits-android/src/main/AndroidManifest.xml b/uhabits-android/src/main/AndroidManifest.xml index 9377e42c0..dd170e10e 100644 --- a/uhabits-android/src/main/AndroidManifest.xml +++ b/uhabits-android/src/main/AndroidManifest.xml @@ -22,6 +22,7 @@ + abs(dy)) { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt index 918e28180..21936d4ea 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt @@ -19,12 +19,17 @@ package org.isoron.uhabits.activities.habits.list +import android.Manifest.permission.POST_NOTIFICATIONS import android.content.Intent +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.os.Build import android.os.Bundle import android.util.Log import android.view.Menu import android.view.MenuItem +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat.checkSelfPermission import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import org.isoron.uhabits.BaseExceptionHandler @@ -56,6 +61,16 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener { lateinit var midnightTimer: MidnightTimer private val scope = CoroutineScope(Dispatchers.Main) + private var permissionAlreadyRequested = false + private val permissionLauncher = + registerForActivityResult(RequestPermission()) { isGranted: Boolean -> + if (isGranted) { + scheduleReminders() + } else { + Log.i("ListHabitsActivity", "POST_NOTIFICATIONS denied") + } + } + private lateinit var menu: ListHabitsMenu override fun onQuestionMarksChanged() { @@ -101,7 +116,26 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener { screen.onAttached() rootView.postInvalidate() midnightTimer.onResume() - appComponent.reminderScheduler.scheduleAll() + + if (appComponent.reminderScheduler.hasHabitsWithReminders()) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + scheduleReminders() + } else { + if (checkSelfPermission(this, POST_NOTIFICATIONS) == PERMISSION_GRANTED) { + scheduleReminders() + } else { + // If we have not requested the permission yet, request it. Otherwide do + // nothing. This check is necessary to avoid an infinite onResume loop in case + // the user denies the permission. + if (!permissionAlreadyRequested) { + Log.i("ListHabitsActivity", "Requestion permission: POST_NOTIFICATIONS") + permissionLauncher.launch(POST_NOTIFICATIONS) + permissionAlreadyRequested = true + } + } + } + } + taskRunner.run { try { AutoBackup(this@ListHabitsActivity).run() @@ -117,6 +151,10 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener { super.onResume() } + private fun scheduleReminders() { + appComponent.reminderScheduler.scheduleAll() + } + override fun onCreateOptionsMenu(m: Menu): Boolean { menu.onCreate(menuInflater, m) return true diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt index 8318429f6..bae15bb00 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt @@ -115,6 +115,11 @@ class ReminderScheduler @Inject constructor( for (habit in reminderHabits) schedule(habit) } + @Synchronized + fun hasHabitsWithReminders(): Boolean { + return !habitList.getFiltered(HabitMatcher.WITH_ALARM).isEmpty + } + @Synchronized fun startListening() { commandRunner.addListener(this)