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/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..423e90958 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,11 +2,13 @@ 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
@@ -65,7 +67,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 +88,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() {
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/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-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/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)