mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Implement runtime notification permission; bump targetSdk to 33
This commit is contained in:
@@ -42,13 +42,13 @@ kotlin {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
compileSdk = 32
|
compileSdk = 33
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionCode = 20200
|
versionCode = 20200
|
||||||
versionName = "2.2.0"
|
versionName = "2.2.0"
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 32
|
targetSdk = 33
|
||||||
applicationId = "org.isoron.uhabits"
|
applicationId = "org.isoron.uhabits"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.isoron.uhabits.activities.common.views
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import android.view.MotionEvent
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import org.isoron.uhabits.BaseViewTest
|
import org.isoron.uhabits.BaseViewTest
|
||||||
@@ -52,7 +53,8 @@ class FrequencyChartTest : BaseViewTest() {
|
|||||||
@Test
|
@Test
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
fun testRender_withDataOffset() {
|
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()
|
view.invalidate()
|
||||||
assertRenders(view, BASE_PATH + "renderDataOffset.png")
|
assertRenders(view, BASE_PATH + "renderDataOffset.png")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.isoron.uhabits.activities.common.views
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import android.view.MotionEvent
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import org.isoron.uhabits.BaseViewTest
|
import org.isoron.uhabits.BaseViewTest
|
||||||
@@ -63,7 +64,8 @@ class ScoreChartTest : BaseViewTest() {
|
|||||||
@Test
|
@Test
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
fun testRender_withDataOffset() {
|
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()
|
view.invalidate()
|
||||||
assertRenders(view, BASE_PATH + "renderDataOffset.png")
|
assertRenders(view, BASE_PATH + "renderDataOffset.png")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".HabitsApplication"
|
android:name=".HabitsApplication"
|
||||||
|
|||||||
@@ -44,21 +44,21 @@ class AndroidDataView(
|
|||||||
addUpdateListener(this@AndroidDataView)
|
addUpdateListener(this@AndroidDataView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent?) = detector.onTouchEvent(event)
|
override fun onTouchEvent(event: MotionEvent) = detector.onTouchEvent(event)
|
||||||
override fun onDown(e: MotionEvent?) = true
|
override fun onDown(e: MotionEvent) = true
|
||||||
override fun onShowPress(e: MotionEvent?) = Unit
|
override fun onShowPress(e: MotionEvent) = Unit
|
||||||
|
|
||||||
override fun onSingleTapUp(e: MotionEvent?): Boolean {
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||||
return handleClick(e, true)
|
return handleClick(e, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLongPress(e: MotionEvent?) {
|
override fun onLongPress(e: MotionEvent) {
|
||||||
handleClick(e)
|
handleClick(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent,
|
||||||
e2: MotionEvent?,
|
e2: MotionEvent,
|
||||||
dx: Float,
|
dx: Float,
|
||||||
dy: Float
|
dy: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
@@ -79,8 +79,8 @@ class AndroidDataView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent,
|
||||||
e2: MotionEvent?,
|
e2: MotionEvent,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
@@ -100,7 +100,7 @@ class AndroidDataView(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationUpdate(animation: ValueAnimator?) {
|
override fun onAnimationUpdate(animation: ValueAnimator) {
|
||||||
if (!scroller.isFinished) {
|
if (!scroller.isFinished) {
|
||||||
scroller.computeScrollOffset()
|
scroller.computeScrollOffset()
|
||||||
updateDataOffset()
|
updateDataOffset()
|
||||||
@@ -127,11 +127,11 @@ class AndroidDataView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleClick(e: MotionEvent?, isSingleTap: Boolean = false): Boolean {
|
private fun handleClick(e: MotionEvent, isSingleTap: Boolean = false): Boolean {
|
||||||
val x: Float
|
val x: Float
|
||||||
val y: Float
|
val y: Float
|
||||||
try {
|
try {
|
||||||
val pointerId = e!!.getPointerId(0)
|
val pointerId = e.getPointerId(0)
|
||||||
x = e.getX(pointerId)
|
x = e.getX(pointerId)
|
||||||
y = e.getY(pointerId)
|
y = e.getY(pointerId)
|
||||||
} catch (ex: RuntimeException) {
|
} catch (ex: RuntimeException) {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat
|
|||||||
return BundleSavedState(superState, bundle)
|
return BundleSavedState(superState, bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, dx: Float, dy: Float): Boolean {
|
override fun onScroll(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float): Boolean {
|
||||||
var dx = dx
|
var dx = dx
|
||||||
if (scrollerBucketSize == 0) return false
|
if (scrollerBucketSize == 0) return false
|
||||||
if (abs(dx) > abs(dy)) {
|
if (abs(dx) > abs(dy)) {
|
||||||
|
|||||||
@@ -19,12 +19,17 @@
|
|||||||
|
|
||||||
package org.isoron.uhabits.activities.habits.list
|
package org.isoron.uhabits.activities.habits.list
|
||||||
|
|
||||||
|
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat.checkSelfPermission
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.isoron.uhabits.BaseExceptionHandler
|
import org.isoron.uhabits.BaseExceptionHandler
|
||||||
@@ -56,6 +61,16 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
|||||||
lateinit var midnightTimer: MidnightTimer
|
lateinit var midnightTimer: MidnightTimer
|
||||||
private val scope = CoroutineScope(Dispatchers.Main)
|
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
|
private lateinit var menu: ListHabitsMenu
|
||||||
|
|
||||||
override fun onQuestionMarksChanged() {
|
override fun onQuestionMarksChanged() {
|
||||||
@@ -101,7 +116,26 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
|||||||
screen.onAttached()
|
screen.onAttached()
|
||||||
rootView.postInvalidate()
|
rootView.postInvalidate()
|
||||||
midnightTimer.onResume()
|
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 {
|
taskRunner.run {
|
||||||
try {
|
try {
|
||||||
AutoBackup(this@ListHabitsActivity).run()
|
AutoBackup(this@ListHabitsActivity).run()
|
||||||
@@ -117,6 +151,10 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun scheduleReminders() {
|
||||||
|
appComponent.reminderScheduler.scheduleAll()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(m: Menu): Boolean {
|
override fun onCreateOptionsMenu(m: Menu): Boolean {
|
||||||
menu.onCreate(menuInflater, m)
|
menu.onCreate(menuInflater, m)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ class ReminderScheduler @Inject constructor(
|
|||||||
for (habit in reminderHabits) schedule(habit)
|
for (habit in reminderHabits) schedule(habit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun hasHabitsWithReminders(): Boolean {
|
||||||
|
return !habitList.getFiltered(HabitMatcher.WITH_ALARM).isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun startListening() {
|
fun startListening() {
|
||||||
commandRunner.addListener(this)
|
commandRunner.addListener(this)
|
||||||
|
|||||||
Reference in New Issue
Block a user