Compare commits

...

14 Commits

20 changed files with 87 additions and 10 deletions

View File

@@ -17,7 +17,7 @@ jobs:
run: ./build.sh build run: ./build.sh build
- name: Run Android tests - name: Run Android tests
run: ./build.sh android-tests-parallel 23 24 25 26 28 30 31 run: ./build.sh android-tests-parallel 24 25 26 28 30 31
- name: Upload artifacts - name: Upload artifacts
if: always() if: always()

View File

@@ -1,5 +1,28 @@
# Changelog # Changelog
## [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)
- Fix widget corners on Android 12 (@iSoron)
- Fix bug that caused notes to be lost when editing a checkmark (@iSoron, #1566)
- Prevent soft keyboard from covering entry popup (@iSoron)
- Accept comma (instead of dot) in certain locales (@iSoron)
### Changed
- Remove update delay after entering a checkmark (@iSoron)
### Removed
- Remove stack widgets (@iSoron)
## [2.1.1] -- 2022-09-24 ## [2.1.1] -- 2022-09-24
### Fixed ### Fixed
- Fix Tasker plugin (@iSoron, #1503) - Fix Tasker plugin (@iSoron, #1503)

View File

@@ -35,8 +35,8 @@ android {
compileSdk = 31 compileSdk = 31
defaultConfig { defaultConfig {
versionCode = 20102 versionCode = 20103
versionName = "2.1.2" versionName = "2.1.3"
minSdk = 23 minSdk = 23
targetSdk = 31 targetSdk = 31
applicationId = "org.isoron.uhabits" applicationId = "org.isoron.uhabits"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -36,6 +36,7 @@ class CheckmarkWidgetViewTest : BaseViewTest() {
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
similarityCutoff = 0.00025
setTheme(R.style.WidgetTheme) setTheme(R.style.WidgetTheme)
val habit = fixtures.createShortHabit() val habit = fixtures.createShortHabit()
val computedEntries = habit.computedEntries val computedEntries = habit.computedEntries

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

@@ -101,6 +101,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
screen.onAttached() screen.onAttached()
rootView.postInvalidate() rootView.postInvalidate()
midnightTimer.onResume() midnightTimer.onResume()
appComponent.reminderScheduler.scheduleAll()
taskRunner.run { taskRunner.run {
try { try {
AutoBackup(this@ListHabitsActivity).run() AutoBackup(this@ListHabitsActivity).run()

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

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

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