diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index f770db0a4..430126a0e 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,6 +4,7 @@ updates:
directory: "/"
schedule:
interval: "monthly"
+ open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
schedule:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 88be81258..2fef06ac9 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -12,17 +12,17 @@ jobs:
timeout-minutes: 30
steps:
- name: Check out source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Build project
run: ./build.sh build
- 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
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: build
path: |
diff --git a/.gitignore b/.gitignore
index 66057054e..8dac68273 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ node_modules
*xcuserdata*
*.sketch
crowdin.yml
+kotlin-js-store
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bbc344341..7b4887f77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,22 @@
# 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
### Fixed
- Fix bug that caused widget to enter checkmark on wrong date (@iSoron, #1541)
diff --git a/build.gradle.kts b/build.gradle.kts
index d5daa6c76..b4e6235bf 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,10 +1,10 @@
plugins {
- val kotlinVersion = "1.7.21"
- id("com.android.application") version "7.4.2" apply (false)
+ val kotlinVersion = "1.9.22"
+ id("com.android.application") version "8.1.4" apply (false)
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
- id("org.jlleitschuh.gradle.ktlint") version "11.4.2"
+ id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
}
apply {
diff --git a/build.sh b/build.sh
index 3a9937a58..863e8681e 100755
--- a/build.sh
+++ b/build.sh
@@ -182,7 +182,7 @@ android_test() {
OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt
OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt
FAILED_TESTS=""
- for i in {1..5}; do
+ for i in {1..10}; do
log_info "Running $size instrumented tests (attempt $i)..."
$ADB shell am instrument \
-r -e coverage true -e size "$size" $FAILED_TESTS \
diff --git a/gradle.properties b/gradle.properties
index b54c6c097..cfc7ec4b1 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,3 +3,6 @@ org.gradle.daemon=true
org.gradle.jvmargs=-Xms2048m -Xmx2048m
android.useAndroidX=true
android.enableJetifier=true
+android.defaults.buildfeatures.buildconfig=true
+android.nonTransitiveRClass=false
+android.nonFinalResIds=false
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 8049c684f..e411586a5 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists
diff --git a/uhabits-android/build.gradle.kts b/uhabits-android/build.gradle.kts
index d8f6b23b9..7dbd220d7 100644
--- a/uhabits-android/build.gradle.kts
+++ b/uhabits-android/build.gradle.kts
@@ -18,8 +18,8 @@
*/
plugins {
- id("com.github.triplet.play") version "3.7.0"
- id("com.android.application") version "7.4.2"
+ id("com.github.triplet.play") version "3.8.6"
+ id("com.android.application") version "8.1.4"
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt")
id("org.jlleitschuh.gradle.ktlint")
@@ -29,15 +29,27 @@ tasks.compileLint {
dependsOn("updateTranslators")
}
+/*
+Added on top of kotlinOptions to work around this issue:
+https://youtrack.jetbrains.com/issue/KTIJ-24311/task-current-target-is-17-and-kaptGenerateStubsProductionDebugKotlin-task-current-target-is-1.8-jvm-target-compatibility-should#focus=Comments-27-6798448.0-0
+Updating gradle might fix this, so try again in the future to remove this and run:
+./gradlew --rerun-tasks :uhabits-android:kaptGenerateStubsReleaseKotlin
+If this doesn't produce any warning, try to remove it.
+ */
+kotlin {
+ jvmToolchain(11)
+}
+
android {
- compileSdk = 32
+ namespace = "org.isoron.uhabits"
+ compileSdk = 34
defaultConfig {
versionCode = 20200
versionName = "2.2.0"
minSdk = 28
- targetSdk = 32
+ targetSdk = 34
applicationId = "org.isoron.uhabits"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -69,8 +81,11 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
- targetCompatibility(JavaVersion.VERSION_1_8)
- sourceCompatibility(JavaVersion.VERSION_1_8)
+ targetCompatibility(JavaVersion.VERSION_11)
+ sourceCompatibility(JavaVersion.VERSION_11)
+ }
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.toString()
}
buildFeatures {
@@ -84,9 +99,9 @@ android {
}
dependencies {
- val daggerVersion = "2.46"
- val kotlinVersion = "1.7.21"
- val kxCoroutinesVersion = "1.6.4"
+ val daggerVersion = "2.51.1"
+ val kotlinVersion = "1.9.22"
+ val kxCoroutinesVersion = "1.7.3"
val ktorVersion = "1.6.8"
val espressoVersion = "3.5.1"
@@ -96,17 +111,17 @@ dependencies {
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
- androidTestImplementation("androidx.annotation:annotation:1.5.0")
+ androidTestImplementation("androidx.annotation:annotation:1.7.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
androidTestImplementation("androidx.test:rules:1.5.0")
- androidTestImplementation("org.mockito.kotlin:mockito-kotlin:2.2.11")
+ androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
compileOnly("javax.annotation:jsr250-api:1.0")
- coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
- implementation("com.github.AppIntro:AppIntro:6.2.0")
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
+ implementation("com.github.AppIntro:AppIntro:6.3.1")
implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.google.dagger:dagger:$daggerVersion")
- implementation("com.google.guava:guava:31.1-android")
+ implementation("com.google.guava:guava:33.1.0-android")
implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
@@ -114,17 +129,18 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$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-support-v4:1.0.0")
- implementation("com.google.android.material:material:1.8.0")
- implementation("com.opencsv:opencsv:5.7.1")
+ implementation("com.google.android.material:material:1.11.0")
+ implementation("com.opencsv:opencsv:5.9")
+ implementation("nl.dionsegijn:konfetti-xml:2.0.2")
implementation(project(":uhabits-core"))
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
testImplementation("com.google.dagger:dagger:$daggerVersion")
testImplementation("junit:junit:4.13.2")
- testImplementation("org.mockito.kotlin:mockito-kotlin:2.2.11")
+ testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
}
kapt {
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..8c7758439 100644
--- a/uhabits-android/src/main/AndroidManifest.xml
+++ b/uhabits-android/src/main/AndroidManifest.xml
@@ -16,12 +16,13 @@
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see .
-->
-
+
+
+
+
-
diff --git a/uhabits-android/src/main/java/com/android/datetimepicker/date/DayPickerView.java b/uhabits-android/src/main/java/com/android/datetimepicker/date/DayPickerView.java
index b18c358b0..1433f849f 100644
--- a/uhabits-android/src/main/java/com/android/datetimepicker/date/DayPickerView.java
+++ b/uhabits-android/src/main/java/com/android/datetimepicker/date/DayPickerView.java
@@ -22,7 +22,6 @@ import java.util.Locale;
import android.annotation.SuppressLint;
import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.AttributeSet;
diff --git a/uhabits-android/src/main/java/com/android/datetimepicker/date/MonthView.java b/uhabits-android/src/main/java/com/android/datetimepicker/date/MonthView.java
index b9df53cce..c3052fdde 100644
--- a/uhabits-android/src/main/java/com/android/datetimepicker/date/MonthView.java
+++ b/uhabits-android/src/main/java/com/android/datetimepicker/date/MonthView.java
@@ -23,14 +23,13 @@ import android.graphics.Paint.*;
import android.os.*;
import androidx.core.view.*;
import androidx.core.view.accessibility.*;
-import androidx.core.widget.*;
+
import android.text.format.*;
import android.view.*;
import android.view.accessibility.*;
import androidx.customview.widget.ExploreByTouchHelper;
-import com.android.*;
import com.android.datetimepicker.*;
import com.android.datetimepicker.date.MonthAdapter.*;
diff --git a/uhabits-android/src/main/java/com/android/datetimepicker/time/AmPmCirclesView.java b/uhabits-android/src/main/java/com/android/datetimepicker/time/AmPmCirclesView.java
index ad9f6ce2e..2cebe7cfa 100644
--- a/uhabits-android/src/main/java/com/android/datetimepicker/time/AmPmCirclesView.java
+++ b/uhabits-android/src/main/java/com/android/datetimepicker/time/AmPmCirclesView.java
@@ -23,7 +23,6 @@ import android.graphics.Paint.*;
import android.util.*;
import android.view.*;
-import com.android.*;
import com.android.datetimepicker.*;
import org.isoron.uhabits.R;
diff --git a/uhabits-android/src/main/java/com/android/datetimepicker/time/RadialPickerLayout.java b/uhabits-android/src/main/java/com/android/datetimepicker/time/RadialPickerLayout.java
index e18ee1143..bbd171a07 100644
--- a/uhabits-android/src/main/java/com/android/datetimepicker/time/RadialPickerLayout.java
+++ b/uhabits-android/src/main/java/com/android/datetimepicker/time/RadialPickerLayout.java
@@ -28,7 +28,6 @@ import android.view.View.*;
import android.view.accessibility.*;
import android.widget.*;
-import com.android.*;
import com.android.datetimepicker.*;
import org.isoron.uhabits.R;
diff --git a/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidDataView.kt b/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidDataView.kt
index 990314a06..ff5f518ff 100644
--- a/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidDataView.kt
+++ b/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidDataView.kt
@@ -44,21 +44,21 @@ class AndroidDataView(
addUpdateListener(this@AndroidDataView)
}
- override fun onTouchEvent(event: MotionEvent?) = detector.onTouchEvent(event)
- override fun onDown(e: MotionEvent?) = true
- override fun onShowPress(e: MotionEvent?) = Unit
+ override fun onTouchEvent(event: MotionEvent) = detector.onTouchEvent(event)
+ override fun onDown(e: MotionEvent) = true
+ override fun onShowPress(e: MotionEvent) = Unit
- override fun onSingleTapUp(e: MotionEvent?): Boolean {
+ override fun onSingleTapUp(e: MotionEvent): Boolean {
return handleClick(e, true)
}
- override fun onLongPress(e: MotionEvent?) {
+ override fun onLongPress(e: MotionEvent) {
handleClick(e)
}
override fun onScroll(
e1: MotionEvent?,
- e2: MotionEvent?,
+ e2: MotionEvent,
dx: Float,
dy: Float
): Boolean {
@@ -80,7 +80,7 @@ class AndroidDataView(
override fun onFling(
e1: MotionEvent?,
- e2: MotionEvent?,
+ e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
@@ -100,7 +100,7 @@ class AndroidDataView(
return false
}
- override fun onAnimationUpdate(animation: ValueAnimator?) {
+ override fun onAnimationUpdate(animation: ValueAnimator) {
if (!scroller.isFinished) {
scroller.computeScrollOffset()
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 y: Float
try {
- val pointerId = e!!.getPointerId(0)
+ val pointerId = e.getPointerId(0)
x = e.getX(pointerId)
y = e.getY(pointerId)
} catch (ex: RuntimeException) {
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkDialog.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkDialog.kt
index 5df8ffd7b..13e5f55f6 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkDialog.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/CheckmarkDialog.kt
@@ -33,17 +33,19 @@ import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
+import org.isoron.uhabits.utils.getCenter
import org.isoron.uhabits.utils.sres
class CheckmarkDialog : AppCompatDialogFragment() {
- var onToggle: (Int, String) -> Unit = { _, _ -> }
+ var onToggle: (Int, String, Float, Float) -> Unit = { _, _, _, _ -> }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val appComponent = (requireActivity().application as HabitsApplication).component
val prefs = appComponent.preferences
val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
+ val color = requireArguments().getInt("color")
arrayOf(view.yesBtn, view.skipBtn).forEach {
- it.setTextColor(requireArguments().getInt("color"))
+ it.setTextColor(color)
}
arrayOf(view.noBtn, view.unknownBtn).forEach {
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
@@ -62,7 +64,8 @@ class CheckmarkDialog : AppCompatDialogFragment() {
}
fun onClick(v: Int) {
val notes = view.notes.text.toString().trim()
- onToggle(v, notes)
+ val location = view.yesBtn.getCenter()
+ onToggle(v, notes, location.x, location.y)
requireDialog().dismiss()
}
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberDialog.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberDialog.kt
index 7e10baa51..d8ba1ed59 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberDialog.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/NumberDialog.kt
@@ -2,17 +2,20 @@ package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.os.Bundle
+import android.provider.Settings
import android.text.method.DigitsKeyListener
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
+import android.view.inputmethod.EditorInfo
import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.InterfaceUtils
+import org.isoron.uhabits.utils.getCenter
import org.isoron.uhabits.utils.requestFocusWithKeyboard
import org.isoron.uhabits.utils.sres
import java.text.DecimalFormat
@@ -22,7 +25,7 @@ import java.text.ParseException
class NumberDialog : AppCompatDialogFragment() {
- var onToggle: (Double, String) -> Unit = { _, _ -> }
+ var onToggle: (Double, String, Float, Float) -> Unit = { _, _, _, _ -> }
var onDismiss: () -> Unit = {}
private var originalNotes: String = ""
@@ -65,7 +68,7 @@ class NumberDialog : AppCompatDialogFragment() {
save()
}
view.skipBtnNumber.setOnClickListener {
- view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
+ view.value.setText(DecimalFormat("#.###").format((Entry.SKIP.toDouble() / 1000)))
save()
}
view.notes.setOnEditorActionListener { v, actionId, event ->
@@ -86,6 +89,15 @@ class NumberDialog : AppCompatDialogFragment() {
// https://stackoverflow.com/a/34256139
val separator = DecimalFormatSymbols.getInstance().decimalSeparator
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() {
@@ -93,12 +105,17 @@ class NumberDialog : AppCompatDialogFragment() {
try {
val numberFormat = NumberFormat.getInstance()
val valueStr = view.value.text.toString()
- value = numberFormat.parse(valueStr)!!.toDouble()
+ value = if (valueStr.isNotEmpty()) {
+ numberFormat.parse(valueStr)!!.toDouble()
+ } else {
+ Entry.UNKNOWN.toDouble() / 1000
+ }
} catch (e: ParseException) {
// NOP
}
val notes = view.notes.text.toString()
- onToggle(value, notes)
+ val location = view.saveBtn.getCenter()
+ onToggle(value, notes, location.x, location.y)
requireDialog().dismiss()
}
}
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt
index 870097800..59225dd2f 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/RingView.kt
@@ -58,6 +58,7 @@ class RingView : View {
private var em = 0f
private var text: String?
private var textSize: Float
+ private var isStrokedTextEnabled: Boolean = false
private var enableFontAwesome = false
private var internalDrawingCache: Bitmap? = null
private var cacheCanvas: Canvas? = null
@@ -131,6 +132,10 @@ class RingView : View {
invalidate()
}
+ fun setIsStrokedTextEnabled(isStroked: Boolean) {
+ this.isStrokedTextEnabled = isStroked
+ }
+
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val activeCanvas: Canvas?
@@ -159,6 +164,12 @@ class RingView : View {
pRing!!.xfermode = null
pRing!!.color = color
pRing!!.textSize = textSize
+
+ if (isStrokedTextEnabled) {
+ pRing!!.style = Paint.Style.STROKE
+ pRing!!.strokeWidth = textSize / 15f
+ }
+
if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context)
activeCanvas.drawText(
text!!,
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt
index 509acc067..e0d311a0f 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt
@@ -65,7 +65,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat
}
override fun onFling(
- e1: MotionEvent,
+ e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
@@ -116,7 +116,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat
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
if (scrollerBucketSize == 0) return false
if (abs(dx) > 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 b7ca3235f..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
@@ -127,6 +165,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
return menu.onItemSelected(item)
}
+ @Deprecated("Deprecated in Java")
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
super.onActivityResult(request, result, data)
screen.onResult(request, result, data)
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt
index 62fe3f00c..f0a542a0d 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.kt
@@ -23,6 +23,7 @@ import android.content.Context
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import android.widget.RelativeLayout
+import nl.dionsegijn.konfetti.xml.KonfettiView
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.ScrollableChart
import org.isoron.uhabits.activities.common.views.TaskProgressBar
@@ -69,6 +70,9 @@ class ListHabitsRootView @Inject constructor(
val listView: HabitCardListView = habitCardListViewFactory.create()
val llEmpty = EmptyListView(context)
val tbar = buildToolbar()
+ val konfettiView = KonfettiView(context).apply {
+ translationZ = 10f
+ }
val progressBar = TaskProgressBar(context, runner)
val hintView: HintView
val header = HeaderView(context, preferences, midnightTimer)
@@ -80,6 +84,7 @@ class ListHabitsRootView @Inject constructor(
val rootView = RelativeLayout(context).apply {
background = sres.getDrawable(R.attr.windowBackgroundColor)
+ addAtTop(konfettiView)
addAtTop(tbar)
addBelow(header, tbar)
addBelow(listView, header, height = MATCH_PARENT)
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt
index c525e624a..e1f0418a3 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.kt
@@ -25,6 +25,9 @@ import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy
+import nl.dionsegijn.konfetti.core.Party
+import nl.dionsegijn.konfetti.core.Position
+import nl.dionsegijn.konfetti.core.emitter.Emitter
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
@@ -63,6 +66,7 @@ import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.tasks.ExportDBTaskFactory
import org.isoron.uhabits.tasks.ImportDataTask
import org.isoron.uhabits.tasks.ImportDataTaskFactory
+import org.isoron.uhabits.utils.ColorUtils
import org.isoron.uhabits.utils.copyTo
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dismissCurrentAndShow
@@ -72,6 +76,7 @@ import org.isoron.uhabits.utils.showSendEmailScreen
import org.isoron.uhabits.utils.showSendFileScreen
import java.io.File
import java.io.IOException
+import java.util.concurrent.TimeUnit
import javax.inject.Inject
const val RESULT_IMPORT_DATA = 101
@@ -218,6 +223,28 @@ class ListHabitsScreen
activity.showSendFileScreen(filename)
}
+ override fun showConfetti(color: PaletteColor, x: Float, y: Float) {
+ val baseColor = themeSwitcher.currentTheme!!.color(color).toInt()
+ rootView.get().konfettiView.start(
+ Party(
+ speed = 0f,
+ maxSpeed = 16f,
+ damping = 0.9f,
+ spread = 360,
+ angle = 0,
+ colors = listOf(
+ ColorUtils.changeHue(baseColor, 180f),
+ ColorUtils.changeHue(baseColor, 20f),
+ ColorUtils.changeHue(baseColor, -20f),
+ baseColor
+ ),
+ position = Position.Absolute(x, y),
+ emitter = Emitter(duration = 25, TimeUnit.MILLISECONDS).max(25),
+ timeToLive = 0
+ )
+ )
+ }
+
override fun showSettingsScreen() {
val intent = intentFactory.startSettingsActivity(activity)
activity.startActivityForResult(intent, REQUEST_SETTINGS)
@@ -240,7 +267,7 @@ class ListHabitsScreen
putDouble("value", value)
putString("notes", notes)
}
- dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
+ dialog.onToggle = { v, n, x, y -> callback.onNumberPicked(v, n, x, y) }
dialog.dismissCurrentAndShow(fm, "numberDialog")
}
@@ -258,7 +285,7 @@ class ListHabitsScreen
putInt("value", selectedValue)
putString("notes", notes)
}
- dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
+ dialog.onToggle = { v, n, x, y -> callback.onNotesSaved(v, n, x, y) }
dialog.dismissCurrentAndShow(fm, "checkmarkDialog")
}
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt
index 8e5e9d21e..84bd001fc 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt
@@ -20,6 +20,7 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.Context
+import android.graphics.PointF
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
import android.os.Build
import android.os.Build.VERSION.SDK_INT
@@ -154,7 +155,17 @@ class HabitCardView(
checkmarkPanel = checkmarkPanelFactory.create().apply {
onToggle = { timestamp, value, notes ->
triggerRipple(timestamp)
- habit?.let { behavior.onToggle(it, timestamp, value, notes) }
+ val location = getAbsoluteButtonLocation(timestamp)
+ habit?.let {
+ behavior.onToggle(
+ it,
+ timestamp,
+ value,
+ notes,
+ location.x,
+ location.y
+ )
+ }
}
onEdit = { timestamp ->
triggerRipple(timestamp)
@@ -206,12 +217,27 @@ class HabitCardView(
}
fun triggerRipple(timestamp: Timestamp) {
+ val location = getRelativeButtonLocation(timestamp)
+ triggerRipple(location.x, location.y)
+ }
+
+ private fun getRelativeButtonLocation(timestamp: Timestamp): PointF {
val today = DateUtils.getTodayWithOffset()
val offset = timestamp.daysUntil(today) - dataOffset
val button = checkmarkPanel.buttons[offset]
val y = button.height / 2.0f
val x = checkmarkPanel.x + button.x + (button.width / 2).toFloat()
- triggerRipple(x, y)
+ return PointF(x, y)
+ }
+
+ private fun getAbsoluteButtonLocation(timestamp: Timestamp): PointF {
+ val containerLocation = IntArray(2)
+ this.getLocationOnScreen(containerLocation)
+ val relButtonLocation = getRelativeButtonLocation(timestamp)
+ return PointF(
+ containerLocation[0].toFloat() + relButtonLocation.x,
+ containerLocation[1].toFloat() - relButtonLocation.y
+ )
}
override fun onAttachedToWindow() {
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt
index 21289a0bd..14baee148 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.kt
@@ -179,7 +179,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
putDouble("value", value)
putString("notes", notes)
}
- dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
+ dialog.onToggle = { v, n, x, y -> callback.onNumberPicked(v, n, x, y) }
dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog")
}
@@ -196,7 +196,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
putInt("value", selectedValue)
putString("notes", notes)
}
- dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
+ dialog.onToggle = { v, n, x, y -> callback.onNotesSaved(v, n, x, y) }
dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog")
}
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt
index 980f6c1d1..f9e857261 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt
@@ -38,7 +38,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
fun setState(state: BarCardState) {
val androidColor = state.theme.color(state.color).toInt()
- binding.chart.view = BarChart(state.theme, JavaLocalDateFormatter(Locale.US)).apply {
+ binding.chart.view = BarChart(state.theme, JavaLocalDateFormatter(Locale.getDefault())).apply {
series = mutableListOf(state.entries.map { it.value / 1000.0 })
colors = mutableListOf(theme.color(state.color.paletteIndex))
axis = state.entries.map { it.timestamp.toLocalDate() }
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt
index e67f6a567..0fc1f43c8 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt
@@ -53,6 +53,8 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
private var ringtoneManager: RingtoneManager? = null
private lateinit var prefs: Preferences
private var widgetUpdater: WidgetUpdater? = null
+
+ @Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == RINGTONE_REQUEST_CODE) {
ringtoneManager!!.update(data)
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt
index 089af4af2..047474d67 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentScheduler.kt
@@ -25,6 +25,7 @@ import android.app.AlarmManager.RTC_WAKEUP
import android.app.PendingIntent
import android.content.Context
import android.content.Context.ALARM_SERVICE
+import android.os.Build
import android.util.Log
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit
@@ -56,6 +57,10 @@ class IntentScheduler
)
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)
return SchedulerResult.OK
}
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt
index 38ba3a1b4..4937dbc74 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt
@@ -66,7 +66,13 @@ class ReminderController @Inject constructor(
}
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) {
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt
index aa96604c2..70abc8f7f 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt
@@ -60,12 +60,14 @@ class AndroidTaskRunner : TaskRunner {
publishProgress(progress)
}
+ @Deprecated("Deprecated in Java")
override fun doInBackground(vararg params: Void?): Void? {
if (isCancelled) return null
task.doInBackground()
return null
}
+ @Deprecated("Deprecated in Java")
override fun onPostExecute(aVoid: Void?) {
if (isCancelled) return
task.onPostExecute()
@@ -74,6 +76,7 @@ class AndroidTaskRunner : TaskRunner {
for (l in listeners) l.onTaskFinished(task)
}
+ @Deprecated("Deprecated in Java")
override fun onPreExecute() {
if (isCancelled) return
for (l in listeners) l.onTaskStarted(task)
@@ -82,6 +85,7 @@ class AndroidTaskRunner : TaskRunner {
task.onPreExecute()
}
+ @Deprecated("Deprecated in Java")
override fun onProgressUpdate(vararg values: Int?) {
values[0]?.let { task.onProgressUpdate(it) }
}
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/utils/ColorUtils.kt b/uhabits-android/src/main/java/org/isoron/uhabits/utils/ColorUtils.kt
index c3806f5c6..b993a5bc2 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/utils/ColorUtils.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/utils/ColorUtils.kt
@@ -36,6 +36,13 @@ object ColorUtils {
return a or r or g or b
}
+ fun changeHue(color: Int, delta: Float): Int {
+ val hsv = FloatArray(3)
+ Color.colorToHSV(color, hsv)
+ hsv[0] = (hsv[0] + delta).mod(360f)
+ return Color.HSVToColor(hsv)
+ }
+
@JvmStatic
fun setAlpha(color: Int, newAlpha: Float): Int {
val intAlpha = (newAlpha * 255).toInt()
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt b/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt
index abf98970c..3c92e7e1b 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt
@@ -26,6 +26,7 @@ import android.content.Intent
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
+import android.graphics.PointF
import android.graphics.drawable.ColorDrawable
import android.os.Handler
import android.os.SystemClock
@@ -135,7 +136,11 @@ fun Activity.startActivitySafely(intent: Intent) {
}
}
-fun Activity.showSendEmailScreen(@StringRes toId: Int, @StringRes subjectId: Int, content: String?) {
+fun Activity.showSendEmailScreen(
+ @StringRes toId: Int,
+ @StringRes subjectId: Int,
+ content: String?
+) {
val to = this.getString(toId)
val subject = this.getString(subjectId)
this.startActivity(
@@ -232,3 +237,11 @@ fun View.requestFocusWithKeyboard() {
dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, 0f, 0f, 0))
}, 250)
}
+
+fun View.getCenter(): PointF {
+ val viewLocation = IntArray(2)
+ this.getLocationOnScreen(viewLocation)
+ viewLocation[0] += this.width / 2
+ viewLocation[1] -= this.height / 2
+ return PointF(viewLocation[0].toFloat(), viewLocation[1].toFloat())
+}
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt
index 6515542b0..72c95da64 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.kt
@@ -68,13 +68,13 @@ class CheckmarkWidgetView : HabitWidgetView {
val fgColor: Int
setShadowAlpha(0x4f)
when (entryState) {
- YES_MANUAL, SKIP -> {
+ YES_MANUAL, SKIP, YES_AUTO -> {
bgColor = activeColor
fgColor = res.getColor(R.attr.contrast0)
backgroundPaint!!.color = bgColor
frame!!.setBackgroundDrawable(background)
}
- YES_AUTO, NO, UNKNOWN -> {
+ NO, UNKNOWN -> {
bgColor = res.getColor(R.attr.cardBgColor)
fgColor = res.getColor(R.attr.contrast60)
}
@@ -87,12 +87,23 @@ class CheckmarkWidgetView : HabitWidgetView {
ring.setColor(fgColor)
ring.setBackgroundColor(bgColor)
ring.setText(text)
+ ring.setIsStrokedTextEnabled(strokedTextEnabled)
label.text = name
label.setTextColor(fgColor)
requestLayout()
postInvalidate()
}
+ private val strokedTextEnabled: Boolean
+ get() = if (isNumerical) {
+ false
+ } else {
+ when (entryState) {
+ YES_AUTO -> true
+ else -> false
+ }
+ }
+
private val text: String
get() = if (isNumerical) {
(max(0, entryValue) / 1000.0).toShortString()
diff --git a/uhabits-android/src/main/res/layout/checkmark_popup.xml b/uhabits-android/src/main/res/layout/checkmark_popup.xml
index e21c2fb54..59f5c81a1 100644
--- a/uhabits-android/src/main/res/layout/checkmark_popup.xml
+++ b/uhabits-android/src/main/res/layout/checkmark_popup.xml
@@ -36,7 +36,7 @@
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
- android:inputType="textCapSentences"
+ android:inputType="textCapSentences|textMultiLine"
android:textSize="@dimen/smallTextSize"
android:padding="4dp"
android:background="@color/transparent"
diff --git a/uhabits-android/src/main/res/layout/widget_graph.xml b/uhabits-android/src/main/res/layout/widget_graph.xml
index 6ee9cfb8b..6349717bd 100644
--- a/uhabits-android/src/main/res/layout/widget_graph.xml
+++ b/uhabits-android/src/main/res/layout/widget_graph.xml
@@ -44,6 +44,7 @@
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="@dimen/smallTextSize"
+ android:maxLines="2"
android:textColor="@color/white"/>
diff --git a/uhabits-android/src/main/res/values-night/colors.xml b/uhabits-android/src/main/res/values-night/colors.xml
new file mode 100644
index 000000000..e98c74022
--- /dev/null
+++ b/uhabits-android/src/main/res/values-night/colors.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ @color/grey_900
+
\ No newline at end of file
diff --git a/uhabits-android/src/main/res/values/colors.xml b/uhabits-android/src/main/res/values/colors.xml
index 9476698d5..5a59d6681 100644
--- a/uhabits-android/src/main/res/values/colors.xml
+++ b/uhabits-android/src/main/res/values/colors.xml
@@ -89,4 +89,5 @@
#1976D2
+ @color/grey_200
\ No newline at end of file
diff --git a/uhabits-android/src/main/res/values/styles.xml b/uhabits-android/src/main/res/values/styles.xml
index a3b2be8ba..87d053fe9 100644
--- a/uhabits-android/src/main/res/values/styles.xml
+++ b/uhabits-android/src/main/res/values/styles.xml
@@ -61,6 +61,7 @@
- 0.25
- true
- @color/grey_200
+ - @color/color_background
- @color/grey_800
- false
- @color/white
diff --git a/uhabits-android/src/main/res/xml/locales_config.xml b/uhabits-android/src/main/res/xml/locales_config.xml
new file mode 100644
index 000000000..a0fefcb50
--- /dev/null
+++ b/uhabits-android/src/main/res/xml/locales_config.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/uhabits-core/build.gradle.kts b/uhabits-core/build.gradle.kts
index 26cece397..930b7819a 100644
--- a/uhabits-core/build.gradle.kts
+++ b/uhabits-core/build.gradle.kts
@@ -24,6 +24,7 @@ plugins {
kotlin {
jvm().withJava()
+ jvmToolchain(11)
sourceSets {
val commonMain by getting {
@@ -43,14 +44,14 @@ kotlin {
val jvmMain by getting {
dependencies {
implementation(kotlin("stdlib-jdk8"))
- compileOnly("com.google.dagger:dagger:2.46")
- implementation("com.google.guava:guava:31.1-android")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4")
- implementation("androidx.annotation:annotation:1.5.0")
+ compileOnly("com.google.dagger:dagger:2.51.1")
+ implementation("com.google.guava:guava:33.1.0-android")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3")
+ implementation("androidx.annotation:annotation:1.7.1")
implementation("com.google.code.findbugs:jsr305:3.0.2")
- implementation("com.opencsv:opencsv:5.7.1")
- implementation("commons-codec:commons-codec:1.15")
- implementation("org.apache.commons:commons-lang3:3.12.0")
+ implementation("com.opencsv:opencsv:5.9")
+ implementation("commons-codec:commons-codec:1.16.0")
+ implementation("org.apache.commons:commons-lang3:3.14.0")
}
}
@@ -58,11 +59,11 @@ kotlin {
dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-junit"))
- implementation("org.xerial:sqlite-jdbc:3.40.0.0")
+ implementation("org.xerial:sqlite-jdbc:3.45.1.0")
implementation("org.hamcrest:hamcrest:2.2")
implementation("org.apache.commons:commons-io:1.3.2")
- implementation("org.mockito.kotlin:mockito-kotlin:2.2.11")
- implementation("org.junit.jupiter:junit-jupiter:5.8.1")
+ implementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
+ implementation("org.junit.jupiter:junit-jupiter:5.10.1")
}
}
}
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt
index abbfc4c48..a06d01ec9 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt
@@ -61,7 +61,7 @@ data class Habit(
return if (isNumerical) {
when (targetType) {
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
- NumericalHabitType.AT_MOST -> value / 1000.0 <= targetValue
+ NumericalHabitType.AT_MOST -> value != Entry.UNKNOWN && value / 1000.0 <= targetValue
}
} else {
value != Entry.NO && value != Entry.UNKNOWN
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)
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt
index 5239ed44b..da5bb018a 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt
@@ -89,6 +89,12 @@ class NotificationTray @Inject constructor(
}
}
+ fun reshow(habit: Habit) {
+ active[habit]?.let {
+ taskRunner.execute(ShowNotificationTask(habit, it))
+ }
+ }
+
interface SystemTray {
fun removeNotification(notificationId: Int)
fun showNotification(
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt
index 3db3cdbaf..b66b08be6 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt
@@ -20,9 +20,12 @@ package org.isoron.uhabits.core.ui.screens.habits.list
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
+import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitType
+import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
+import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences
@@ -52,8 +55,16 @@ open class ListHabitsBehavior @Inject constructor(
val entry = habit.computedEntries.get(timestamp!!)
if (habit.type == HabitType.NUMERICAL) {
val oldValue = entry.value.toDouble() / 1000
- screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String ->
+ screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String, x: Float, y: Float ->
val value = (newValue * 1000).roundToInt()
+ if (newValue != oldValue) {
+ if (
+ (habit.targetType == AT_LEAST && newValue >= habit.targetValue) ||
+ (habit.targetType == AT_MOST && newValue <= habit.targetValue)
+ ) {
+ screen.showConfetti(habit.color, x, y)
+ }
+ }
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
}
} else {
@@ -61,7 +72,8 @@ open class ListHabitsBehavior @Inject constructor(
entry.value,
entry.notes,
habit.color
- ) { newValue, newNotes ->
+ ) { newValue: Int, newNotes: String, x: Float, y: Float ->
+ if (newValue != entry.value && newValue == YES_MANUAL) screen.showConfetti(habit.color, x, y)
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes))
}
}
@@ -117,10 +129,11 @@ open class ListHabitsBehavior @Inject constructor(
if (prefs.isFirstRun) onFirstRun()
}
- fun onToggle(habit: Habit, timestamp: Timestamp, value: Int, notes: String) {
+ fun onToggle(habit: Habit, timestamp: Timestamp, value: Int, notes: String, x: Float, y: Float) {
commandRunner.run(
CreateRepetitionCommand(habitList, habit, timestamp, value, notes)
)
+ if (value == YES_MANUAL) screen.showConfetti(habit.color, x, y)
}
enum class Message {
@@ -144,12 +157,22 @@ open class ListHabitsBehavior @Inject constructor(
}
fun interface NumberPickerCallback {
- fun onNumberPicked(newValue: Double, notes: String)
+ fun onNumberPicked(
+ newValue: Double,
+ notes: String,
+ x: Float,
+ y: Float
+ )
fun onNumberPickerDismissed() {}
}
fun interface CheckMarkDialogCallback {
- fun onNotesSaved(value: Int, notes: String)
+ fun onNotesSaved(
+ value: Int,
+ notes: String,
+ x: Float,
+ y: Float
+ )
fun onNotesDismissed() {}
}
@@ -170,5 +193,6 @@ open class ListHabitsBehavior @Inject constructor(
)
fun showSendBugReportToDeveloperScreen(log: String)
fun showSendFileScreen(filename: String)
+ fun showConfetti(color: PaletteColor, x: Float, y: Float)
}
}
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt
index cfbe5e0f3..0a28c801f 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt
@@ -98,7 +98,7 @@ class HistoryCardPresenter(
entry.value,
entry.notes,
habit.color
- ) { newValue, newNotes ->
+ ) { newValue, newNotes, _: Float, _: Float ->
commandRunner.run(
CreateRepetitionCommand(
habitList,
@@ -135,7 +135,7 @@ class HistoryCardPresenter(
screen.showNumberPopup(
value = oldValue / 1000.0,
notes = entry.notes
- ) { newValue: Double, newNotes: String ->
+ ) { newValue: Double, newNotes: String, _: Float, _: Float ->
val thousands = (newValue * 1000).roundToInt()
commandRunner.run(
CreateRepetitionCommand(
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/Themes.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/Themes.kt
index 1b1d989e4..eef7f26d0 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/Themes.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/Themes.kt
@@ -125,4 +125,30 @@ class WidgetTheme : LightTheme() {
override val highContrastTextColor = Color.WHITE
override val mediumContrastTextColor = Color.WHITE.withAlpha(0.50)
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)
+ }
+ }
}
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateUtils.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateUtils.kt
index b91b0e33a..aec974228 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateUtils.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateUtils.kt
@@ -227,7 +227,7 @@ abstract class DateUtils {
fun getStartOfTodayWithOffset(): Long = getStartOfDayWithOffset(getLocalTime())
@JvmStatic
- fun millisecondsUntilTomorrowWithOffset(): Long = getStartOfTomorrowWithOffset() - getLocalTime()
+ fun millisecondsUntilTomorrowWithOffset(): Long = getStartOfTomorrowWithOffset() - applyTimezone(getLocalTime())
@JvmStatic
fun getStartOfTodayCalendar(): GregorianCalendar = getCalendar(getStartOfToday())
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/MidnightTimer.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/MidnightTimer.kt
index 903099293..b63569533 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/MidnightTimer.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/MidnightTimer.kt
@@ -19,6 +19,7 @@
package org.isoron.uhabits.core.utils
import org.isoron.uhabits.core.AppScope
+import org.isoron.uhabits.core.io.Logging
import java.util.LinkedList
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
@@ -29,9 +30,10 @@ import javax.inject.Inject
* A class that emits events when a new day starts.
*/
@AppScope
-open class MidnightTimer @Inject constructor() {
+open class MidnightTimer @Inject constructor(logging: Logging) {
private val listeners: MutableList = LinkedList()
private lateinit var executor: ScheduledExecutorService
+ private val logger = logging.getLogger("MidnightTimer")
@Synchronized
fun addListener(listener: MidnightListener) {
@@ -39,7 +41,10 @@ open class MidnightTimer @Inject constructor() {
}
@Synchronized
- fun onPause(): MutableList? = executor.shutdownNow()
+ fun onPause(): MutableList? {
+ logger.info("Pausing timer")
+ return executor.shutdownNow()
+ }
@Synchronized
fun onResume(
@@ -47,9 +52,11 @@ open class MidnightTimer @Inject constructor() {
testExecutor: ScheduledExecutorService? = null
) {
executor = testExecutor ?: Executors.newSingleThreadScheduledExecutor()
+ val initialDelay = DateUtils.millisecondsUntilTomorrowWithOffset() + delayOffsetInMillis
+ logger.info("Scheduling refresh for $initialDelay ms from now")
executor.scheduleAtFixedRate(
{ notifyListeners() },
- DateUtils.millisecondsUntilTomorrowWithOffset() + delayOffsetInMillis,
+ initialDelay,
DateUtils.DAY_LENGTH,
TimeUnit.MILLISECONDS
)
@@ -60,6 +67,7 @@ open class MidnightTimer @Inject constructor() {
@Synchronized
private fun notifyListeners() {
+ logger.info("Midnight refresh")
for (l in listeners) {
l.atMidnight()
}
diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt
index ead82c1e9..26dd82df8 100644
--- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt
+++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt
@@ -84,7 +84,7 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
eq(""),
picker.capture()
)
- picker.lastValue.onNumberPicked(100.0, "")
+ picker.lastValue.onNumberPicked(100.0, "", 0f, 0f)
val today = getTodayWithOffset()
assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
}
@@ -168,7 +168,9 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
habit = habit1,
timestamp = getToday(),
value = Entry.NO,
- notes = ""
+ notes = "",
+ x = 0f,
+ y = 0f
)
assertFalse(habit1.isCompletedToday())
}
diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.kt
index 27a1c8cba..e173cdb5e 100644
--- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.kt
+++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.kt
@@ -32,7 +32,7 @@ import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.reset
import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyZeroInteractions
+import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever
class WidgetBehaviorTest : BaseUnitTest() {
@@ -61,7 +61,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
CreateRepetitionCommand(habitList, habit, today, Entry.YES_MANUAL, "")
)
verify(notificationTray).cancel(habit)
- verifyZeroInteractions(preferences)
+ verifyNoInteractions(preferences)
}
@Test
@@ -71,7 +71,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
CreateRepetitionCommand(habitList, habit, today, Entry.NO, "")
)
verify(notificationTray).cancel(habit)
- verifyZeroInteractions(preferences)
+ verifyNoInteractions(preferences)
}
@Test
@@ -113,7 +113,7 @@ class WidgetBehaviorTest : BaseUnitTest() {
CreateRepetitionCommand(habitList, habit, today, 600, "")
)
verify(notificationTray).cancel(habit)
- verifyZeroInteractions(preferences)
+ verifyNoInteractions(preferences)
}
@Test
@@ -126,6 +126,6 @@ class WidgetBehaviorTest : BaseUnitTest() {
CreateRepetitionCommand(habitList, habit, today, 400, "")
)
verify(notificationTray).cancel(habit)
- verifyZeroInteractions(preferences)
+ verifyNoInteractions(preferences)
}
}
diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/MidnightTimerTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/MidnightTimerTest.kt
index b92507da0..c0eb3fe2d 100644
--- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/MidnightTimerTest.kt
+++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/MidnightTimerTest.kt
@@ -4,6 +4,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.isoron.uhabits.core.BaseUnitTest
+import org.isoron.uhabits.core.io.StandardLogging
import org.junit.Test
import java.util.Calendar
import java.util.TimeZone
@@ -34,7 +35,7 @@ class MidnightTimerTest : BaseUnitTest() {
)
val suspendedListener = suspendCoroutine { continuation ->
- MidnightTimer().apply {
+ MidnightTimer(StandardLogging()).apply {
addListener { continuation.resume(true) }
// When
onResume(1, executor)
diff --git a/uhabits-server/build.gradle.kts b/uhabits-server/build.gradle.kts
index a96d02131..9f2fda06a 100644
--- a/uhabits-server/build.gradle.kts
+++ b/uhabits-server/build.gradle.kts
@@ -22,7 +22,11 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
application
id("kotlin")
- id("com.github.johnrengelman.shadow") version "7.1.2"
+ id("com.github.johnrengelman.shadow") version "8.1.1"
+}
+
+kotlin {
+ jvmToolchain(17)
}
@@ -34,11 +38,9 @@ application {
dependencies {
val ktorVersion = "1.6.8"
- val kotlinVersion = "1.7.21"
- val logbackVersion = "1.4.5"
- implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.22")
implementation("io.ktor:ktor-server-netty:$ktorVersion")
- implementation("ch.qos.logback:logback-classic:$logbackVersion")
+ implementation("ch.qos.logback:logback-classic:1.4.14")
implementation("io.ktor:ktor-server-core:$ktorVersion")
implementation("io.ktor:ktor-html-builder:$ktorVersion")
implementation("io.ktor:ktor-jackson:$ktorVersion")
@@ -47,7 +49,7 @@ dependencies {
implementation("io.prometheus:simpleclient_httpserver:0.16.0")
implementation("io.prometheus:simpleclient_hotspot:0.16.0")
testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
- testImplementation("org.mockito.kotlin:mockito-kotlin:2.2.11")
+ testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
testImplementation(kotlin("test"))
testImplementation(kotlin("test-junit"))
}
@@ -57,3 +59,4 @@ tasks.withType {
archiveClassifier.set("")
archiveVersion.set("")
}
+