mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 01:08:50 -06:00
Merge branch 'master' into dev
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -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)
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
23
uhabits-android/src/main/res/values-night/colors.xml
Normal file
23
uhabits-android/src/main/res/values-night/colors.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user