mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-07 09:38:52 -06:00
Compare commits
6 Commits
f8a6de228a
...
7a8ce51487
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a8ce51487 | ||
|
dee93fde8f
|
|||
|
f0ce05e06e
|
|||
|
4975ba2752
|
|||
|
ed8c60e52f
|
|||
|
|
7735247521 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ node_modules
|
|||||||
*xcuserdata*
|
*xcuserdata*
|
||||||
*.sketch
|
*.sketch
|
||||||
crowdin.yml
|
crowdin.yml
|
||||||
|
kotlin-js-store
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
val kotlinVersion = "1.8.20"
|
val kotlinVersion = "1.8.20"
|
||||||
id("com.android.application") version "7.4.2" apply (false)
|
id("com.android.application") version "8.1.4" apply (false)
|
||||||
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
||||||
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
||||||
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ org.gradle.daemon=true
|
|||||||
org.gradle.jvmargs=-Xms2048m -Xmx2048m
|
org.gradle.jvmargs=-Xms2048m -Xmx2048m
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
|
android.nonTransitiveRClass=false
|
||||||
|
android.nonFinalResIds=false
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.github.triplet.play") version "3.8.4"
|
id("com.github.triplet.play") version "3.8.4"
|
||||||
id("com.android.application") version "7.4.2"
|
id("com.android.application") version "8.1.4"
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("org.jetbrains.kotlin.kapt")
|
id("org.jetbrains.kotlin.kapt")
|
||||||
id("org.jlleitschuh.gradle.ktlint")
|
id("org.jlleitschuh.gradle.ktlint")
|
||||||
@@ -42,13 +42,14 @@ kotlin {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
compileSdk = 32
|
namespace = "org.isoron.uhabits"
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
@@ -93,7 +94,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
val daggerVersion = "2.48"
|
val daggerVersion = "2.48.1"
|
||||||
val kotlinVersion = "1.8.20"
|
val kotlinVersion = "1.8.20"
|
||||||
val kxCoroutinesVersion = "1.7.3"
|
val kxCoroutinesVersion = "1.7.3"
|
||||||
val ktorVersion = "1.6.8"
|
val ktorVersion = "1.6.8"
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,13 @@
|
|||||||
~ You should have received a copy of the GNU General Public License along
|
~ You should have received a copy of the GNU General Public License along
|
||||||
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="org.isoron.uhabits">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<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.SCHEDULE_EXACT_ALARM" />
|
||||||
|
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
<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
|
||||||
|
|||||||
@@ -66,7 +66,13 @@ class ReminderController @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onDismiss(habit: Habit) {
|
fun onDismiss(habit: Habit) {
|
||||||
notificationTray.cancel(habit)
|
if (preferences.shouldMakeNotificationsSticky()) {
|
||||||
|
// This is a workaround to keep sticky notifications non-dismissible in Android 14+.
|
||||||
|
// If the notification is dismissed, we immediately reshow it.
|
||||||
|
notificationTray.reshow(habit)
|
||||||
|
} else {
|
||||||
|
notificationTray.cancel(habit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSnoozeDelayPicker(habit: Habit, context: Context) {
|
private fun showSnoozeDelayPicker(habit: Habit, context: Context) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ kotlin {
|
|||||||
val jvmMain by getting {
|
val jvmMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
compileOnly("com.google.dagger:dagger:2.48")
|
compileOnly("com.google.dagger:dagger:2.48.1")
|
||||||
implementation("com.google.guava:guava:32.1.2-android")
|
implementation("com.google.guava:guava:32.1.2-android")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3")
|
||||||
implementation("androidx.annotation:annotation:1.7.0")
|
implementation("androidx.annotation:annotation:1.7.0")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ class NotificationTray @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun reshow(habit: Habit) {
|
||||||
|
active[habit]?.let {
|
||||||
|
taskRunner.execute(ShowNotificationTask(habit, it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface SystemTray {
|
interface SystemTray {
|
||||||
fun removeNotification(notificationId: Int)
|
fun removeNotification(notificationId: Int)
|
||||||
fun showNotification(
|
fun showNotification(
|
||||||
|
|||||||
Reference in New Issue
Block a user