Compare commits

..

34 Commits

Author SHA1 Message Date
d10538e720 Merge branch 'release/2.2.0' 2024-01-30 19:15:12 -06:00
02e9e2384e Update CHANGELOG 2024-01-30 19:09:22 -06:00
Serhii K
b627ff4413 Use colorBackground property instead of splash screen lib 2024-01-30 19:06:43 -06:00
Serhii K
0683ea43f4 Fix splash screen background color in dark mode 2024-01-30 19:06:43 -06:00
08f77a5cae Merge branch 'master' into release/2.2.0 2023-12-01 18:31:53 -06:00
27df792775 Update CHANGELOG 2023-12-01 18:29:34 -06:00
Leonard Dizon
800f92f255 Create locales_config.xml for per-app language 2023-12-01 18:14:06 -06:00
dependabot[bot]
e06ed3ed7d Bump ch.qos.logback:logback-classic from 1.4.11 to 1.4.13 (#1882)
Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.4.11 to 1.4.13.
- [Commits](https://github.com/qos-ch/logback/compare/v_1.4.11...v_1.4.13)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 18:08:31 -06:00
dependabot[bot]
3bb119c6ed Bump com.github.triplet.play from 3.8.4 to 3.8.6 (#1879)
Bumps com.github.triplet.play from 3.8.4 to 3.8.6.

---
updated-dependencies:
- dependency-name: com.github.triplet.play
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 18:08:05 -06:00
dependabot[bot]
0762699a86 Bump org.apache.commons:commons-lang3 from 3.13.0 to 3.14.0 (#1881)
Bumps org.apache.commons:commons-lang3 from 3.13.0 to 3.14.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 18:07:26 -06:00
dependabot[bot]
8837326d44 Bump com.opencsv:opencsv from 5.8 to 5.9 (#1880)
Bumps com.opencsv:opencsv from 5.8 to 5.9.

---
updated-dependencies:
- dependency-name: com.opencsv:opencsv
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 18:07:17 -06:00
dependabot[bot]
481a3d5784 Bump org.junit.jupiter:junit-jupiter from 5.10.0 to 5.10.1 (#1878)
Bumps [org.junit.jupiter:junit-jupiter](https://github.com/junit-team/junit5) from 5.10.0 to 5.10.1.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.0...r5.10.1)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 18:07:03 -06:00
8eff782f54 Update dependencies 2023-11-29 20:07:45 -06:00
c590734c42 Update dependencies 2023-11-29 19:54:00 -06:00
7e7f68282b Update Kotlin 2023-11-29 19:38:21 -06:00
4bd5cee17b Update libraries 2023-11-29 19:30:32 -06:00
b2421dc8b1 GH Actions: Increase number of attempts 2023-11-27 20:57:43 -06:00
93363bff96 GH Actions: Add API 34 2023-11-27 20:57:14 -06:00
d01044b203 GH Actions: Add API 34 2023-11-27 20:47:48 -06:00
70fe513e52 Bump targetSdk to 34 2023-11-27 20:43:34 -06:00
dee93fde8f Upgrade to AGP 8.1.4 2023-11-27 19:46:54 -06:00
f0ce05e06e API 34: Implement workaround to keep sticky notifications non-dismissible 2023-11-25 15:42:04 -06:00
4975ba2752 Add permission: USE_EXACT_ALARM 2023-11-25 15:30:29 -06:00
ed8c60e52f Implement runtime notification permission; bump targetSdk to 33 2023-11-25 07:12:16 -06:00
dependabot[bot]
7735247521 Bump daggerVersion from 2.48 to 2.48.1
Bumps `daggerVersion` from 2.48 to 2.48.1.

Updates `com.google.dagger:dagger` from 2.48 to 2.48.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.48...dagger-2.48.1)

Updates `com.google.dagger:dagger-compiler` from 2.48 to 2.48.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.48...dagger-2.48.1)

---
updated-dependencies:
- dependency-name: com.google.dagger:dagger
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.dagger:dagger-compiler
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 16:07:22 +01:00
6116ef9450 Merge branch 'hotfix/2.1.3' 2023-08-28 05:51:08 -05:00
8801960615 Update CHANGELOG 2023-08-28 05:43:12 -05:00
b0a4284b66 NumberDialog: Use text input on Samsung devices
Fixes #1719
2023-08-28 05:36:59 -05:00
88df8d2552 Format source code 2023-07-08 17:13:45 -05:00
d4f4f8b4a9 Prevent crash if exact alarm permission is revoked 2023-06-05 20:25:00 -05:00
9ca1aa911a Minor layout fixes 2023-06-05 20:11:50 -05:00
ba57ebad31 Fix an invisible color in widgets; adjust another color 2023-06-05 20:01:41 -05:00
8b55ffb147 Fix timezone bug in MidnightTimer; increase logging 2023-06-04 18:25:33 -05:00
727e88b7b1 Fix skip button in locales that use comma instead of dot
Fixes #1721
2023-06-04 17:30:38 -05:00
31 changed files with 259 additions and 54 deletions

View File

@@ -18,7 +18,7 @@ jobs:
run: ./build.sh build run: ./build.sh build
- name: Run Android tests - name: Run Android tests
run: ./build.sh android-tests-parallel 28 29 30 31 32 33 run: ./build.sh android-tests-parallel 28 29 30 32 33 34
- name: Upload artifacts - name: Upload artifacts
if: always() if: always()

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ node_modules
*xcuserdata* *xcuserdata*
*.sketch *.sketch
crowdin.yml crowdin.yml
kotlin-js-store

View File

@@ -1,5 +1,22 @@
# Changelog # Changelog
## [2.2.0] -- 2024-01-30
### Added
- Add support for Android 14 (@iSoron, @hiqua)
- Allow user to change app language (@leondzn)
### Fixed
- Implement workaround to make notifications non-dismissible in Android 14 (@iSoron, #1872)
- Fix splash screen background color in dark mode (@SIKV, #1888)
## [2.1.3] -- 2023-08-28
### Fixed
- Use text input on Samsung devices (@iSoron, #1719)
- Prevent crash if alarm permission is revoked (@iSoron)
- Adjust widget colors (@iSoron)
- Fix bug preventing screens from updating at midnight (@iSoron)
- Fix skip button in locales that use comma instead of dot (@iSoron, #1721)
## [2.1.2] -- 2023-05-26 ## [2.1.2] -- 2023-05-26
### Fixed ### Fixed
- Fix bug that caused widget to enter checkmark on wrong date (@iSoron, #1541) - Fix bug that caused widget to enter checkmark on wrong date (@iSoron, #1541)

View File

@@ -1,6 +1,6 @@
plugins { plugins {
val kotlinVersion = "1.8.20" val kotlinVersion = "1.9.21"
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)

View File

@@ -181,7 +181,7 @@ android_test() {
OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt
OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt
FAILED_TESTS="" FAILED_TESTS=""
for i in {1..5}; do for i in {1..10}; do
log_info "Running $size instrumented tests (attempt $i)..." log_info "Running $size instrumented tests (attempt $i)..."
$ADB shell am instrument \ $ADB shell am instrument \
-r -e coverage true -e size "$size" $FAILED_TESTS \ -r -e coverage true -e size "$size" $FAILED_TESTS \

View File

@@ -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

View File

@@ -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

View File

@@ -18,8 +18,8 @@
*/ */
plugins { plugins {
id("com.github.triplet.play") version "3.8.4" id("com.github.triplet.play") version "3.8.6"
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 = 34
defaultConfig { defaultConfig {
versionCode = 20200 versionCode = 20200
versionName = "2.2.0" versionName = "2.2.0"
minSdk = 28 minSdk = 28
targetSdk = 32 targetSdk = 34
applicationId = "org.isoron.uhabits" applicationId = "org.isoron.uhabits"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -93,8 +94,8 @@ android {
} }
dependencies { dependencies {
val daggerVersion = "2.48" val daggerVersion = "2.48.1"
val kotlinVersion = "1.8.20" val kotlinVersion = "1.9.21"
val kxCoroutinesVersion = "1.7.3" val kxCoroutinesVersion = "1.7.3"
val ktorVersion = "1.6.8" val ktorVersion = "1.6.8"
val espressoVersion = "3.5.1" val espressoVersion = "3.5.1"
@@ -111,11 +112,11 @@ dependencies {
androidTestImplementation("androidx.test:rules:1.5.0") androidTestImplementation("androidx.test:rules:1.5.0")
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0") androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
compileOnly("javax.annotation:jsr250-api:1.0") compileOnly("javax.annotation:jsr250-api:1.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
implementation("com.github.AppIntro:AppIntro:6.2.0") implementation("com.github.AppIntro:AppIntro:6.3.1")
implementation("com.google.code.findbugs:jsr305:3.0.2") implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.google.dagger:dagger:$daggerVersion") implementation("com.google.dagger:dagger:$daggerVersion")
implementation("com.google.guava:guava:32.1.2-android") implementation("com.google.guava:guava:32.1.3-android")
implementation("io.ktor:ktor-client-android:$ktorVersion") implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-jackson:$ktorVersion") implementation("io.ktor:ktor-client-jackson:$ktorVersion")
@@ -123,11 +124,11 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
implementation("androidx.appcompat:appcompat:1.5.1") implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.legacy:legacy-preference-v14:1.0.0") implementation("androidx.legacy:legacy-preference-v14:1.0.0")
implementation("androidx.legacy:legacy-support-v4:1.0.0") implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("com.google.android.material:material:1.9.0") implementation("com.google.android.material:material:1.10.0")
implementation("com.opencsv:opencsv:5.8") implementation("com.opencsv:opencsv:5.9")
implementation(project(":uhabits-core")) implementation(project(":uhabits-core"))
kapt("com.google.dagger:dagger-compiler:$daggerVersion") kapt("com.google.dagger:dagger-compiler:$daggerVersion")
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion") kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")

View File

@@ -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")
} }

View File

@@ -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")
} }

View File

@@ -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"
@@ -29,6 +30,7 @@
android:backupAgent=".HabitsBackupAgent" android:backupAgent=".HabitsBackupAgent"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/main_activity_title" android:label="@string/main_activity_title"
android:localeConfig="@xml/locales_config"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppBaseTheme"> android:theme="@style/AppBaseTheme">

View File

@@ -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 {
@@ -80,7 +80,7 @@ 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) {

View File

@@ -2,11 +2,13 @@ package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.text.method.DigitsKeyListener import android.text.method.DigitsKeyListener
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R import org.isoron.uhabits.R
@@ -65,7 +67,7 @@ class NumberDialog : AppCompatDialogFragment() {
save() save()
} }
view.skipBtnNumber.setOnClickListener { view.skipBtnNumber.setOnClickListener {
view.value.setText((Entry.SKIP.toDouble() / 1000).toString()) view.value.setText(DecimalFormat("#.###").format((Entry.SKIP.toDouble() / 1000)))
save() save()
} }
view.notes.setOnEditorActionListener { v, actionId, event -> view.notes.setOnEditorActionListener { v, actionId, event ->
@@ -86,6 +88,15 @@ class NumberDialog : AppCompatDialogFragment() {
// https://stackoverflow.com/a/34256139 // https://stackoverflow.com/a/34256139
val separator = DecimalFormatSymbols.getInstance().decimalSeparator val separator = DecimalFormatSymbols.getInstance().decimalSeparator
view.value.keyListener = DigitsKeyListener.getInstance("0123456789$separator") view.value.keyListener = DigitsKeyListener.getInstance("0123456789$separator")
// https://github.com/flutter/flutter/issues/61175
val currKeyboard = Settings.Secure.getString(
requireContext().contentResolver,
Settings.Secure.DEFAULT_INPUT_METHOD
)
if (currKeyboard.contains("swiftkey") || currKeyboard.contains("samsung")) {
view.value.inputType = EditorInfo.TYPE_CLASS_TEXT
}
} }
fun save() { fun save() {

View File

@@ -65,7 +65,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat
} }
override fun onFling( override fun onFling(
e1: MotionEvent, e1: MotionEvent?,
e2: MotionEvent, e2: MotionEvent,
velocityX: Float, velocityX: Float,
velocityY: Float velocityY: Float
@@ -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)) {

View File

@@ -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

View File

@@ -25,6 +25,7 @@ import android.app.AlarmManager.RTC_WAKEUP
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Context.ALARM_SERVICE import android.content.Context.ALARM_SERVICE
import android.os.Build
import android.util.Log import android.util.Log
import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
@@ -56,6 +57,10 @@ class IntentScheduler
) )
return SchedulerResult.IGNORED return SchedulerResult.IGNORED
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !manager.canScheduleExactAlarms()) {
Log.e("IntentScheduler", "No permission to schedule exact alarms")
return SchedulerResult.IGNORED
}
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent) manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent)
return SchedulerResult.OK return SchedulerResult.OK
} }

View File

@@ -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) {

View File

@@ -36,7 +36,7 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:gravity="center" android:gravity="center"
android:inputType="textCapSentences" android:inputType="textCapSentences|textMultiLine"
android:textSize="@dimen/smallTextSize" android:textSize="@dimen/smallTextSize"
android:padding="4dp" android:padding="4dp"
android:background="@color/transparent" android:background="@color/transparent"

View File

@@ -44,6 +44,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:textSize="@dimen/smallTextSize" android:textSize="@dimen/smallTextSize"
android:maxLines="2"
android:textColor="@color/white"/> android:textColor="@color/white"/>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
~
~ This file is part of Loop Habit Tracker.
~
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by the
~ Free Software Foundation, either version 3 of the License, or (at your
~ option) any later version.
~
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<color name="color_background">@color/grey_900</color>
</resources>

View File

@@ -89,4 +89,5 @@
</array> </array>
<color name="ic_launcher_background">#1976D2</color> <color name="ic_launcher_background">#1976D2</color>
<color name="color_background">@color/grey_200</color>
</resources> </resources>

View File

@@ -61,6 +61,7 @@
<item name="widgetShadowAlpha">0.25</item> <item name="widgetShadowAlpha">0.25</item>
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
<item name="windowBackgroundColor">@color/grey_200</item> <item name="windowBackgroundColor">@color/grey_200</item>
<item name="android:colorBackground">@color/color_background</item>
<item name="android:textColorAlertDialogListItem">@color/grey_800</item> <item name="android:textColorAlertDialogListItem">@color/grey_800</item>
<item name="singleLineTitle">false</item> <item name="singleLineTitle">false</item>
<item name="dialogFormLabelColor">@color/white</item> <item name="dialogFormLabelColor">@color/white</item>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en" /> <!-- English -->
<locale android:name="af-ZA" /> <!-- Afrikaans -->
<locale android:name="ar-SA" /> <!-- Arabic -->
<locale android:name="bg-BG" /> <!-- Bulgarian -->
<locale android:name="ca-ES" /> <!-- Catalan -->
<locale android:name="cs-CZ" /> <!-- Czech -->
<locale android:name="da-DK" /> <!-- Danish -->
<locale android:name="de-DE" /> <!-- German -->
<locale android:name="el-GR" /> <!-- Greek -->
<locale android:name="eo-UY" /> <!-- Esperanto -->
<locale android:name="es-ES" /> <!-- Spanish -->
<locale android:name="eu-ES" /> <!-- Basque -->
<locale android:name="fa-IR" /> <!-- Farsi -->
<locale android:name="fi-FI" /> <!-- Finnish -->
<locale android:name="fr-FR" /> <!-- French -->
<locale android:name="hi-IN" /> <!-- Hindi -->
<locale android:name="hr-HR" /> <!-- Croatian -->
<locale android:name="hu-HU" /> <!-- Hungarian -->
<locale android:name="hy-AM" /> <!-- Armenian -->
<locale android:name="in-ID" /> <!-- Indonesian -->
<locale android:name="it-IT" /> <!-- Italian -->
<locale android:name="iw-IL" /> <!-- Hebrew -->
<locale android:name="ja-JP" /> <!-- Japanese -->
<locale android:name="ko-KR" /> <!-- Korean -->
<locale android:name="nl-NL" /> <!-- Dutch -->
<locale android:name="no-NO" /> <!-- Norwegian -->
<locale android:name="pl-PL" /> <!-- Polish -->
<locale android:name="pt-BR" /> <!-- Portuguese (Brazil) -->
<locale android:name="pt-PT" /> <!-- Portuguese (Portugal) -->
<locale android:name="ro-RO" /> <!-- Romanian -->
<locale android:name="ru-RU" /> <!-- Russian -->
<locale android:name="sk-SK" /> <!-- Slovak -->
<locale android:name="sl-SL" /> <!-- Slovenian -->
<locale android:name="sr-CS" /> <!-- Serbian (Latin) -->
<locale android:name="sr-SP" /> <!-- Serbian (Cyrillic) -->
<locale android:name="sv-SE" /> <!-- Swedish -->
<locale android:name="ta-IN" /> <!-- Tamil -->
<locale android:name="te-IN" /> <!-- Telugu -->
<locale android:name="tr-TR" /> <!-- Turkish -->
<locale android:name="ug-CN" /> <!-- Uyghur -->
<locale android:name="uk-UA" /> <!-- Ukrainian -->
<locale android:name="vi-VN" /> <!-- Vietnamese -->
<locale android:name="zh-CN" /> <!-- Chinese (Simplified) -->
<locale android:name="zh-TW" /> <!-- Chinese (Traditional) -->
</locale-config>

View File

@@ -44,14 +44,14 @@ 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.3-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")
implementation("com.google.code.findbugs:jsr305:3.0.2") implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.opencsv:opencsv:5.8") implementation("com.opencsv:opencsv:5.9")
implementation("commons-codec:commons-codec:1.16.0") implementation("commons-codec:commons-codec:1.16.0")
implementation("org.apache.commons:commons-lang3:3.13.0") implementation("org.apache.commons:commons-lang3:3.14.0")
} }
} }
@@ -63,7 +63,7 @@ kotlin {
implementation("org.hamcrest:hamcrest:2.2") implementation("org.hamcrest:hamcrest:2.2")
implementation("org.apache.commons:commons-io:1.3.2") implementation("org.apache.commons:commons-io:1.3.2")
implementation("org.mockito.kotlin:mockito-kotlin:5.1.0") implementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
implementation("org.junit.jupiter:junit-jupiter:5.10.0") implementation("org.junit.jupiter:junit-jupiter:5.10.1")
} }
} }
} }

View File

@@ -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)

View File

@@ -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(

View File

@@ -125,4 +125,30 @@ class WidgetTheme : LightTheme() {
override val highContrastTextColor = Color.WHITE override val highContrastTextColor = Color.WHITE
override val mediumContrastTextColor = Color.WHITE.withAlpha(0.50) override val mediumContrastTextColor = Color.WHITE.withAlpha(0.50)
override val lowContrastTextColor = Color.WHITE.withAlpha(0.10) override val lowContrastTextColor = Color.WHITE.withAlpha(0.10)
override fun color(paletteIndex: Int): Color {
return when (paletteIndex) {
0 -> Color(0xD32F2F)
1 -> Color(0xE64A19)
2 -> Color(0xF57C00)
3 -> Color(0xFF8F00)
4 -> Color(0xF9A825)
5 -> Color(0xAFB42B)
6 -> Color(0x7CB342)
7 -> Color(0x388E3C)
8 -> Color(0x00897B)
9 -> Color(0x00ACC1)
10 -> Color(0x039BE5)
11 -> Color(0x1976D2)
12 -> Color(0x6275f0)
13 -> Color(0x5E35B1)
14 -> Color(0x8E24AA)
15 -> Color(0xD81B60)
16 -> Color(0x5D4037)
17 -> Color(0x757575)
18 -> Color(0x757575)
19 -> Color(0x9E9E9E)
else -> Color(0x000000)
}
}
} }

View File

@@ -227,7 +227,7 @@ abstract class DateUtils {
fun getStartOfTodayWithOffset(): Long = getStartOfDayWithOffset(getLocalTime()) fun getStartOfTodayWithOffset(): Long = getStartOfDayWithOffset(getLocalTime())
@JvmStatic @JvmStatic
fun millisecondsUntilTomorrowWithOffset(): Long = getStartOfTomorrowWithOffset() - getLocalTime() fun millisecondsUntilTomorrowWithOffset(): Long = getStartOfTomorrowWithOffset() - applyTimezone(getLocalTime())
@JvmStatic @JvmStatic
fun getStartOfTodayCalendar(): GregorianCalendar = getCalendar(getStartOfToday()) fun getStartOfTodayCalendar(): GregorianCalendar = getCalendar(getStartOfToday())

View File

@@ -19,6 +19,7 @@
package org.isoron.uhabits.core.utils package org.isoron.uhabits.core.utils
import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.io.Logging
import java.util.LinkedList import java.util.LinkedList
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledExecutorService
@@ -29,9 +30,10 @@ import javax.inject.Inject
* A class that emits events when a new day starts. * A class that emits events when a new day starts.
*/ */
@AppScope @AppScope
open class MidnightTimer @Inject constructor() { open class MidnightTimer @Inject constructor(logging: Logging) {
private val listeners: MutableList<MidnightListener> = LinkedList() private val listeners: MutableList<MidnightListener> = LinkedList()
private lateinit var executor: ScheduledExecutorService private lateinit var executor: ScheduledExecutorService
private val logger = logging.getLogger("MidnightTimer")
@Synchronized @Synchronized
fun addListener(listener: MidnightListener) { fun addListener(listener: MidnightListener) {
@@ -39,7 +41,10 @@ open class MidnightTimer @Inject constructor() {
} }
@Synchronized @Synchronized
fun onPause(): MutableList<Runnable>? = executor.shutdownNow() fun onPause(): MutableList<Runnable>? {
logger.info("Pausing timer")
return executor.shutdownNow()
}
@Synchronized @Synchronized
fun onResume( fun onResume(
@@ -47,9 +52,11 @@ open class MidnightTimer @Inject constructor() {
testExecutor: ScheduledExecutorService? = null testExecutor: ScheduledExecutorService? = null
) { ) {
executor = testExecutor ?: Executors.newSingleThreadScheduledExecutor() executor = testExecutor ?: Executors.newSingleThreadScheduledExecutor()
val initialDelay = DateUtils.millisecondsUntilTomorrowWithOffset() + delayOffsetInMillis
logger.info("Scheduling refresh for $initialDelay ms from now")
executor.scheduleAtFixedRate( executor.scheduleAtFixedRate(
{ notifyListeners() }, { notifyListeners() },
DateUtils.millisecondsUntilTomorrowWithOffset() + delayOffsetInMillis, initialDelay,
DateUtils.DAY_LENGTH, DateUtils.DAY_LENGTH,
TimeUnit.MILLISECONDS TimeUnit.MILLISECONDS
) )
@@ -60,6 +67,7 @@ open class MidnightTimer @Inject constructor() {
@Synchronized @Synchronized
private fun notifyListeners() { private fun notifyListeners() {
logger.info("Midnight refresh")
for (l in listeners) { for (l in listeners) {
l.atMidnight() l.atMidnight()
} }

View File

@@ -4,6 +4,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.BaseUnitTest
import org.isoron.uhabits.core.io.StandardLogging
import org.junit.Test import org.junit.Test
import java.util.Calendar import java.util.Calendar
import java.util.TimeZone import java.util.TimeZone
@@ -34,7 +35,7 @@ class MidnightTimerTest : BaseUnitTest() {
) )
val suspendedListener = suspendCoroutine<Boolean> { continuation -> val suspendedListener = suspendCoroutine<Boolean> { continuation ->
MidnightTimer().apply { MidnightTimer(StandardLogging()).apply {
addListener { continuation.resume(true) } addListener { continuation.resume(true) }
// When // When
onResume(1, executor) onResume(1, executor)

View File

@@ -22,7 +22,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins { plugins {
application application
id("kotlin") id("kotlin")
id("com.github.johnrengelman.shadow") version "7.1.2" id("com.github.johnrengelman.shadow") version "8.1.1"
} }
kotlin { kotlin {
@@ -38,11 +38,9 @@ application {
dependencies { dependencies {
val ktorVersion = "1.6.8" val ktorVersion = "1.6.8"
val kotlinVersion = "1.8.20" implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21")
val logbackVersion = "1.4.9"
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("io.ktor:ktor-server-netty:$ktorVersion") implementation("io.ktor:ktor-server-netty:$ktorVersion")
implementation("ch.qos.logback:logback-classic:$logbackVersion") implementation("ch.qos.logback:logback-classic:1.4.13")
implementation("io.ktor:ktor-server-core:$ktorVersion") implementation("io.ktor:ktor-server-core:$ktorVersion")
implementation("io.ktor:ktor-html-builder:$ktorVersion") implementation("io.ktor:ktor-html-builder:$ktorVersion")
implementation("io.ktor:ktor-jackson:$ktorVersion") implementation("io.ktor:ktor-jackson:$ktorVersion")