Merge branch 'iSoron:dev' into dev

pull/1780/head
Jakub Kalinowski 2 years ago committed by GitHub
commit 5ab1b01f9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,3 +4,7 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "monthly" interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

@ -9,9 +9,10 @@ on:
jobs: jobs:
Test: Test:
runs-on: self-hosted runs-on: self-hosted
timeout-minutes: 30
steps: steps:
- name: Check out source code - name: Check out source code
uses: actions/checkout@v1 uses: actions/checkout@v3
- name: Build project - name: Build project
run: ./build.sh build run: ./build.sh build
@ -21,7 +22,7 @@ jobs:
- name: Upload artifacts - name: Upload artifacts
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: build name: build
path: | path: |

@ -1,5 +1,20 @@
# Changelog # Changelog
## [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)

@ -17,8 +17,8 @@ show you how your habits improved over time. It is completely ad-free and open
source. source.
<p align="center"> <p align="center">
<a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge-border.png" height="75px"/></a> <a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80px"/></a>
<a href="https://f-droid.org/app/org.isoron.uhabits"><img alt="Get it on F-Droid" src="https://i.imgur.com/baSPE7X.png" height="75px"/></a> <a href="https://f-droid.org/app/org.isoron.uhabits"><img alt="Get it on F-Droid" src="https://f-droid.org/badge/get-it-on.png" height="80px"/></a>
</p> </p>
## Screenshots ## Screenshots

@ -1,11 +1,11 @@
plugins { plugins {
val kotlinVersion = "1.7.21" val kotlinVersion = "1.7.21"
id("com.android.application") version ("7.3.0-rc01") apply (false) id("com.android.application") version "7.4.2" apply (false)
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false) id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false) id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false) id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false) id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
id("org.jlleitschuh.gradle.ktlint") version "11.0.0" id("org.jlleitschuh.gradle.ktlint") version "11.4.2"
} }
apply { apply {

@ -217,20 +217,28 @@ android_test_parallel() {
( (
LOG=build/android-test-$API.log LOG=build/android-test-$API.log
log_info "API $API: Running tests..." log_info "API $API: Running tests..."
if android_test $API 1>$LOG 2>&1; then android_test $API 1>$LOG 2>&1
ret_code=$?
if [ $ret_code = 0 ]; then
log_info "API $API: Passed" log_info "API $API: Passed"
else else
log_error "API $API: Failed" log_error "API $API: Failed"
fi fi
pkill -9 -f ${AVD_PREFIX}${API} pkill -9 -f ${AVD_PREFIX}${API}
exit $ret_code
)& )&
PIDS+=" $!" PIDS+=" $!"
done done
# Check exit codes # Check exit codes
RET_CODE=0 success=0
for pid in $PIDS; do for pid in $PIDS; do
wait $pid || RET_CODE=1 wait $pid
ret_code=$?
if [ $ret_code != 0 ]; then
success=1
fi
echo pid=$pid ret_code=$ret_code success=$success
done done
# Print all logs # Print all logs
@ -240,7 +248,7 @@ android_test_parallel() {
echo "::endgroup::" echo "::endgroup::"
done done
return $RET_CODE return $success
} }
android_build() { android_build() {

@ -19,7 +19,7 @@
plugins { plugins {
id("com.github.triplet.play") version "3.7.0" id("com.github.triplet.play") version "3.7.0"
id("com.android.application") id("com.android.application") version "7.4.2"
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt") id("org.jetbrains.kotlin.kapt")
id("org.jetbrains.kotlin.android.extensions") id("org.jetbrains.kotlin.android.extensions")
@ -35,8 +35,8 @@ android {
compileSdk = 32 compileSdk = 32
defaultConfig { defaultConfig {
versionCode = 20101 versionCode = 20200
versionName = "2.1.1" versionName = "2.2.0"
minSdk = 28 minSdk = 28
targetSdk = 31 targetSdk = 31
applicationId = "org.isoron.uhabits" applicationId = "org.isoron.uhabits"
@ -80,11 +80,11 @@ android {
} }
dependencies { dependencies {
val daggerVersion = "2.43.2" val daggerVersion = "2.46"
val kotlinVersion = "1.7.21" val kotlinVersion = "1.7.21"
val kxCoroutinesVersion = "1.6.4" val kxCoroutinesVersion = "1.6.4"
val ktorVersion = "1.6.8" val ktorVersion = "1.6.8"
val espressoVersion = "3.4.0" val espressoVersion = "3.5.1"
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion") androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion") androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
@ -92,11 +92,11 @@ dependencies {
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3") androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion") androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion") androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
androidTestImplementation("androidx.annotation:annotation:1.4.0") androidTestImplementation("androidx.annotation:annotation:1.5.0")
androidTestImplementation("androidx.test.ext:junit:1.1.3") androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0") androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
androidTestImplementation("androidx.test:rules:1.4.0") androidTestImplementation("androidx.test:rules:1.5.0")
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") androidTestImplementation("org.mockito.kotlin:mockito-kotlin:2.2.11")
compileOnly("javax.annotation:jsr250-api:1.0") compileOnly("javax.annotation:jsr250-api:1.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
implementation("com.github.AppIntro:AppIntro:6.2.0") implementation("com.github.AppIntro:AppIntro:6.2.0")
@ -113,14 +113,14 @@ dependencies {
implementation("androidx.appcompat:appcompat:1.5.1") implementation("androidx.appcompat:appcompat:1.5.1")
implementation("androidx.legacy:legacy-preference-v14:1.0.0") implementation("androidx.legacy:legacy-preference-v14:1.0.0")
implementation("androidx.legacy:legacy-support-v4:1.0.0") implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("com.google.android.material:material:1.6.1") implementation("com.google.android.material:material:1.8.0")
implementation("com.opencsv:opencsv:5.7.1") implementation("com.opencsv:opencsv:5.7.1")
implementation(project(":uhabits-core")) implementation(project(":uhabits-core"))
kapt("com.google.dagger:dagger-compiler:$daggerVersion") kapt("com.google.dagger:dagger-compiler:$daggerVersion")
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion") kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
testImplementation("com.google.dagger:dagger:$daggerVersion") testImplementation("com.google.dagger:dagger:$daggerVersion")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") testImplementation("org.mockito.kotlin:mockito-kotlin:2.2.11")
} }
kapt { kapt {

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

@ -39,6 +39,7 @@ open class BaseUserInterfaceTest {
private lateinit var prefs: Preferences private lateinit var prefs: Preferences
private lateinit var fixtures: HabitFixtures private lateinit var fixtures: HabitFixtures
private lateinit var cache: HabitCardListCache private lateinit var cache: HabitCardListCache
@Before @Before
@Throws(Exception::class) @Throws(Exception::class)
fun setUp() { fun setUp() {

@ -153,18 +153,22 @@ open class BaseViewTest : BaseAndroidTest() {
var filename = filename var filename = filename
var dir = getSDCardDir("test-screenshots") var dir = getSDCardDir("test-screenshots")
if (dir == null) dir = AndroidDirFinder(targetContext).getFilesDir("test-screenshots") if (dir == null) dir = AndroidDirFinder(targetContext).getFilesDir("test-screenshots")
if (dir == null) throw RuntimeException( if (dir == null) {
throw RuntimeException(
"Could not find suitable dir for screenshots" "Could not find suitable dir for screenshots"
) )
}
filename = filename.replace("\\.png$".toRegex(), "$suffix.png") filename = filename.replace("\\.png$".toRegex(), "$suffix.png")
val absolutePath = String.format("%s/%s", dir.absolutePath, filename) val absolutePath = String.format("%s/%s", dir.absolutePath, filename)
val parent = File(absolutePath).parentFile val parent = File(absolutePath).parentFile
if (!parent.exists() && !parent.mkdirs()) throw RuntimeException( if (!parent.exists() && !parent.mkdirs()) {
throw RuntimeException(
String.format( String.format(
"Could not create dir: %s", "Could not create dir: %s",
parent.absolutePath parent.absolutePath
) )
) )
}
val out = FileOutputStream(absolutePath) val out = FileOutputStream(absolutePath)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
return absolutePath return absolutePath

@ -19,7 +19,6 @@
package org.isoron.uhabits package org.isoron.uhabits
import com.nhaarman.mockitokotlin2.mock
import dagger.Component import dagger.Component
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -35,6 +34,7 @@ import org.isoron.uhabits.inject.ActivityScope
import org.isoron.uhabits.inject.HabitModule import org.isoron.uhabits.inject.HabitModule
import org.isoron.uhabits.inject.HabitsActivityModule import org.isoron.uhabits.inject.HabitsActivityModule
import org.isoron.uhabits.inject.HabitsApplicationComponent import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.mockito.kotlin.mock
@Module @Module
class TestModule { class TestModule {

@ -44,7 +44,7 @@ class ScoreChartTest : BaseViewTest() {
habit = habit, habit = habit,
firstWeekday = prefs.firstWeekdayInt, firstWeekday = prefs.firstWeekdayInt,
spinnerPosition = 0, spinnerPosition = 0,
theme = LightTheme(), theme = LightTheme()
) )
view = ScoreChart(targetContext).apply { view = ScoreChart(targetContext).apply {
setScores(state.scores) setScores(state.scores)

@ -30,6 +30,7 @@ import org.junit.runner.RunWith
@MediumTest @MediumTest
class StreakChartTest : BaseViewTest() { class StreakChartTest : BaseViewTest() {
private lateinit var view: StreakChart private lateinit var view: StreakChart
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()

@ -44,7 +44,7 @@ class EntryButtonViewTest : BaseViewTest() {
view = component.getEntryButtonViewFactory().create().apply { view = component.getEntryButtonViewFactory().create().apply {
value = Entry.NO value = Entry.NO
color = PaletteUtils.getAndroidTestColor(5) color = PaletteUtils.getAndroidTestColor(5)
onToggle = { _, _, _ -> toggled = true } onToggle = { _, _ -> toggled = true }
onEdit = { edited = true } onEdit = { edited = true }
} }
measureView(view, dpToPixels(48), dpToPixels(48)) measureView(view, dpToPixels(48), dpToPixels(48))

@ -77,7 +77,7 @@ class EntryPanelViewTest : BaseViewTest() {
@Test @Test
fun testToggle() { fun testToggle() {
val timestamps = mutableListOf<Timestamp>() val timestamps = mutableListOf<Timestamp>()
view.onToggle = { t, _, _, _ -> timestamps.add(t) } view.onToggle = { t, _, _ -> timestamps.add(t) }
view.buttons[0].performLongClick() view.buttons[0].performLongClick()
view.buttons[2].performLongClick() view.buttons[2].performLongClick()
view.buttons[3].performLongClick() view.buttons[3].performLongClick()
@ -88,7 +88,7 @@ class EntryPanelViewTest : BaseViewTest() {
fun testToggle_withOffset() { fun testToggle_withOffset() {
val timestamps = mutableListOf<Timestamp>() val timestamps = mutableListOf<Timestamp>()
view.dataOffset = 3 view.dataOffset = 3
view.onToggle = { t, _, _, _ -> timestamps += t } view.onToggle = { t, _, _ -> timestamps += t }
view.buttons[0].performLongClick() view.buttons[0].performLongClick()
view.buttons[2].performLongClick() view.buttons[2].performLongClick()
view.buttons[3].performLongClick() view.buttons[3].performLongClick()

@ -20,15 +20,15 @@ package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.whenever
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest

@ -20,9 +20,6 @@ package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
@ -30,6 +27,9 @@ import org.isoron.uhabits.core.ui.screens.habits.list.HintList
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest

@ -48,7 +48,7 @@ class FrequencyCardViewTest : BaseViewTest() {
FrequencyCardPresenter.buildState( FrequencyCardPresenter.buildState(
habit = habit, habit = habit,
firstWeekday = 0, firstWeekday = 0,
theme = LightTheme(), theme = LightTheme()
) )
) )
measureView(view, 800f, 600f) measureView(view, 800f, 600f)

@ -49,7 +49,7 @@ class HistoryCardViewTest : BaseViewTest() {
HistoryCardPresenter.buildState( HistoryCardPresenter.buildState(
habit = habit, habit = habit,
firstWeekday = SUNDAY, firstWeekday = SUNDAY,
theme = LightTheme(), theme = LightTheme()
) )
) )
measureView(view, 800f, 600f) measureView(view, 800f, 600f)

@ -51,7 +51,7 @@ class OverviewCardViewTest : BaseViewTest() {
scoreYearDiff = 0.74f, scoreYearDiff = 0.74f,
totalCount = 44, totalCount = 44,
color = PaletteColor(7), color = PaletteColor(7),
theme = LightTheme(), theme = LightTheme()
) )
) )
measureView(view, 800f, 300f) measureView(view, 800f, 300f)

@ -49,7 +49,7 @@ class ScoreCardViewTest : BaseViewTest() {
habit = habit, habit = habit,
firstWeekday = 0, firstWeekday = 0,
spinnerPosition = 0, spinnerPosition = 0,
theme = LightTheme(), theme = LightTheme()
) )
) )
measureView(view, 800f, 600f) measureView(view, 800f, 600f)

@ -48,7 +48,7 @@ class StreakCardViewTest : BaseViewTest() {
StreakCardState( StreakCardState(
bestStreaks = habit.streaks.getBest(10), bestStreaks = habit.streaks.getBest(10),
color = habit.color, color = habit.color,
theme = LightTheme(), theme = LightTheme()
) )
) )
measureView(view, 800f, 600f) measureView(view, 800f, 600f)

@ -53,7 +53,7 @@ class SubtitleCardViewTest : BaseViewTest() {
isNumerical = false, isNumerical = false,
question = "Did you meditate this morning?", question = "Did you meditate this morning?",
reminder = Reminder(8, 30, EVERY_DAY), reminder = Reminder(8, 30, EVERY_DAY),
theme = LightTheme(), theme = LightTheme()
) )
) )
measureView(view, 800f, 200f) measureView(view, 800f, 200f)

@ -119,7 +119,7 @@ class ListHabitsRegressionTest : BaseUserInterfaceTest() {
"Wake up early", "Wake up early",
"Meditate", "Meditate",
"Read books", "Read books",
"Track time", "Track time"
) )
} }
} }

@ -33,9 +33,11 @@ import java.io.IOException
@MediumTest @MediumTest
class CheckmarkWidgetViewTest : BaseViewTest() { class CheckmarkWidgetViewTest : BaseViewTest() {
private lateinit var view: CheckmarkWidgetView private lateinit var view: CheckmarkWidgetView
@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

@ -19,7 +19,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.isoron.uhabits"> package="org.isoron.uhabits">
<uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

@ -60,7 +60,7 @@ class AndroidCanvas : Canvas {
y1.toDp(), y1.toDp(),
x2.toDp(), x2.toDp(),
y2.toDp(), y2.toDp(),
paint, paint
) )
} }
@ -69,7 +69,7 @@ class AndroidCanvas : Canvas {
text, text,
x.toDp(), x.toDp(),
y.toDp() + 0.6f * mHeight, y.toDp() + 0.6f * mHeight,
textPaint, textPaint
) )
} }
@ -83,7 +83,7 @@ class AndroidCanvas : Canvas {
y: Double, y: Double,
width: Double, width: Double,
height: Double, height: Double,
cornerRadius: Double, cornerRadius: Double
) { ) {
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
innerCanvas.drawRoundRect( innerCanvas.drawRoundRect(
@ -93,7 +93,7 @@ class AndroidCanvas : Canvas {
(y + height).toDp(), (y + height).toDp(),
cornerRadius.toDp(), cornerRadius.toDp(),
cornerRadius.toDp(), cornerRadius.toDp(),
paint, paint
) )
} }
@ -108,7 +108,7 @@ class AndroidCanvas : Canvas {
y.toDp(), y.toDp(),
(x + width).toDp(), (x + width).toDp(),
(y + height).toDp(), (y + height).toDp(),
paint, paint
) )
} }
@ -148,7 +148,7 @@ class AndroidCanvas : Canvas {
centerY: Double, centerY: Double,
radius: Double, radius: Double,
startAngle: Double, startAngle: Double,
swipeAngle: Double, swipeAngle: Double
) { ) {
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
innerCanvas.drawArc( innerCanvas.drawArc(
@ -159,14 +159,14 @@ class AndroidCanvas : Canvas {
-startAngle.toFloat(), -startAngle.toFloat(),
-swipeAngle.toFloat(), -swipeAngle.toFloat(),
true, true,
paint, paint
) )
} }
override fun fillCircle( override fun fillCircle(
centerX: Double, centerX: Double,
centerY: Double, centerY: Double,
radius: Double, radius: Double
) { ) {
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
innerCanvas.drawCircle(centerX.toDp(), centerY.toDp(), radius.toDp(), paint) innerCanvas.drawCircle(centerX.toDp(), centerY.toDp(), radius.toDp(), paint)

@ -33,7 +33,7 @@ import kotlin.math.max
*/ */
class AndroidDataView( class AndroidDataView(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null
) : AndroidView<DataView>(context, attrs), ) : AndroidView<DataView>(context, attrs),
GestureDetector.OnGestureListener, GestureDetector.OnGestureListener,
ValueAnimator.AnimatorUpdateListener { ValueAnimator.AnimatorUpdateListener {
@ -60,7 +60,7 @@ class AndroidDataView(
e1: MotionEvent?, e1: MotionEvent?,
e2: MotionEvent?, e2: MotionEvent?,
dx: Float, dx: Float,
dy: Float, dy: Float
): Boolean { ): Boolean {
if (abs(dx) > abs(dy)) { if (abs(dx) > abs(dy)) {
val parent = parent val parent = parent
@ -82,7 +82,7 @@ class AndroidDataView(
e1: MotionEvent?, e1: MotionEvent?,
e2: MotionEvent?, e2: MotionEvent?,
velocityX: Float, velocityX: Float,
velocityY: Float, velocityY: Float
): Boolean { ): Boolean {
scroller.fling( scroller.fling(
scroller.currX, scroller.currX,
@ -140,8 +140,11 @@ class AndroidDataView(
// e.getPointerId. // e.getPointerId.
return false return false
} }
if (isSingleTap) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity) if (isSingleTap) {
else view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
} else {
view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
}
return true return true
} }
} }

@ -47,6 +47,6 @@ fun Color.toInt(): Int {
(255 * this.alpha).roundToInt(), (255 * this.alpha).roundToInt(),
(255 * this.red).roundToInt(), (255 * this.red).roundToInt(),
(255 * this.green).roundToInt(), (255 * this.green).roundToInt(),
(255 * this.blue).roundToInt(), (255 * this.blue).roundToInt()
) )
} }

@ -24,7 +24,7 @@ import android.util.AttributeSet
open class AndroidView<T : View>( open class AndroidView<T : View>(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null
) : android.view.View(context, attrs) { ) : android.view.View(context, attrs) {
var view: T? = null var view: T? = null

@ -39,7 +39,7 @@ import org.isoron.uhabits.inject.ActivityScope
class AndroidThemeSwitcher class AndroidThemeSwitcher
constructor( constructor(
@ActivityContext val context: Context, @ActivityContext val context: Context,
preferences: Preferences, preferences: Preferences
) : ThemeSwitcher(preferences) { ) : ThemeSwitcher(preferences) {
override var currentTheme: Theme = LightTheme() override var currentTheme: Theme = LightTheme()

@ -27,7 +27,7 @@ import org.isoron.uhabits.utils.startActivitySafely
class AboutScreen( class AboutScreen(
private val activity: AboutActivity, private val activity: AboutActivity,
private val intents: IntentFactory, private val intents: IntentFactory,
private val prefs: Preferences, private val prefs: Preferences
) { ) {
private var developerCountdown = 5 private var developerCountdown = 5

@ -32,7 +32,7 @@ import org.isoron.uhabits.utils.setupToolbar
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
class AboutView( class AboutView(
context: Context, context: Context,
private val screen: AboutScreen, private val screen: AboutScreen
) : FrameLayout(context) { ) : FrameLayout(context) {
private var binding = AboutBinding.inflate(LayoutInflater.from(context)) private var binding = AboutBinding.inflate(LayoutInflater.from(context))
@ -43,7 +43,7 @@ class AboutView(
toolbar = binding.toolbar, toolbar = binding.toolbar,
color = PaletteColor(11), color = PaletteColor(11),
title = resources.getString(R.string.about), title = resources.getString(R.string.about),
theme = currentTheme(), theme = currentTheme()
) )
val version = resources.getString(R.string.version_n) val version = resources.getString(R.string.version_n)
binding.tvContributors.setOnClickListener { screen.showCodeContributorsWebsite() } binding.tvContributors.setOnClickListener { screen.showCodeContributorsWebsite() }

@ -20,109 +20,60 @@
package org.isoron.uhabits.activities.common.dialogs package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry.Companion.NO import org.isoron.uhabits.core.models.Entry.Companion.NO
import org.isoron.uhabits.core.models.Entry.Companion.SKIP import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.databinding.CheckmarkPopupBinding import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.utils.dimBehind
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
const val POPUP_WIDTH = 4 * 48f + 16f class CheckmarkDialog : AppCompatDialogFragment() {
const val POPUP_HEIGHT = 48f * 2.5f + 8f
class CheckmarkPopup(
private val context: Context,
private val color: Int,
private var notes: String,
private var value: Int,
private val prefs: Preferences,
private val anchor: View,
) {
var onToggle: (Int, String) -> Unit = { _, _ -> } var onToggle: (Int, String) -> Unit = { _, _ -> }
private lateinit var dialog: Dialog
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
// Required for round corners
container.clipToOutline = true
}
init {
view.booleanButtons.visibility = VISIBLE
initColors()
initTypefaces()
hideDisabledButtons()
populate()
}
private fun initColors() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val appComponent = (requireActivity().application as HabitsApplication).component
val prefs = appComponent.preferences
val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
arrayOf(view.yesBtn, view.skipBtn).forEach { arrayOf(view.yesBtn, view.skipBtn).forEach {
it.setTextColor(color) it.setTextColor(requireArguments().getInt("color"))
} }
arrayOf(view.noBtn, view.unknownBtn).forEach { arrayOf(view.noBtn, view.unknownBtn).forEach {
it.setTextColor(view.root.sres.getColor(R.attr.contrast60)) it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
} }
}
private fun initTypefaces() {
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach { arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
it.typeface = getFontAwesome(context) it.typeface = getFontAwesome(requireContext())
} }
} view.notes.setText(requireArguments().getString("notes")!!)
private fun hideDisabledButtons() {
if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE
if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE
} view.booleanButtons.visibility = VISIBLE
val dialog = Dialog(requireContext())
private fun populate() {
val selectedBtn = when (value) {
YES_MANUAL -> view.yesBtn
YES_AUTO -> view.noBtn
NO -> view.noBtn
UNKNOWN -> if (prefs.areQuestionMarksEnabled) view.unknownBtn else view.noBtn
SKIP -> if (prefs.isSkipEnabled) view.skipBtn else view.noBtn
else -> null
}
view.notes.setText(notes)
}
fun show() {
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
dialog.setContentView(view.root) dialog.setContentView(view.root)
dialog.window?.apply { dialog.window?.apply {
setLayout(
view.root.dp(POPUP_WIDTH).toInt(),
view.root.dp(POPUP_HEIGHT).toInt()
)
setBackgroundDrawableResource(android.R.color.transparent) setBackgroundDrawableResource(android.R.color.transparent)
} }
fun onClick(v: Int) { fun onClick(v: Int) {
this.value = v val notes = view.notes.text.toString().trim()
save() onToggle(v, notes)
requireDialog().dismiss()
} }
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) } view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
view.noBtn.setOnClickListener { onClick(NO) } view.noBtn.setOnClickListener { onClick(NO) }
view.skipBtn.setOnClickListener { onClick(SKIP) } view.skipBtn.setOnClickListener { onClick(SKIP) }
view.unknownBtn.setOnClickListener { onClick(UNKNOWN) } view.unknownBtn.setOnClickListener { onClick(UNKNOWN) }
dialog.setCanceledOnTouchOutside(true) view.notes.setOnEditorActionListener { v, actionId, event ->
dialog.dimBehind() onClick(requireArguments().getInt("value"))
dialog.dismissCurrentAndShow() true
} }
fun save() { return dialog
onToggle(value, view.notes.text.toString().trim())
dialog.dismiss()
} }
} }

@ -48,22 +48,22 @@ class FrequencyPickerDialog(
addBeforeAfterText( addBeforeAfterText(
this.getString(R.string.every_x_days), this.getString(R.string.every_x_days),
contentView.everyXDaysContainer, contentView.everyXDaysContainer
) )
addBeforeAfterText( addBeforeAfterText(
this.getString(R.string.x_times_per_week), this.getString(R.string.x_times_per_week),
contentView.xTimesPerWeekContainer, contentView.xTimesPerWeekContainer
) )
addBeforeAfterText( addBeforeAfterText(
this.getString(R.string.x_times_per_month), this.getString(R.string.x_times_per_month),
contentView.xTimesPerMonthContainer, contentView.xTimesPerMonthContainer
) )
addBeforeAfterText( addBeforeAfterText(
this.getString(R.string.x_times_per_y_days), this.getString(R.string.x_times_per_y_days),
contentView.xTimesPerYDaysContainer, contentView.xTimesPerYDaysContainer
) )
contentView.everyDayRadioButton.setOnClickListener { contentView.everyDayRadioButton.setOnClickListener {
@ -124,7 +124,8 @@ class FrequencyPickerDialog(
val parts = str.split("%d") val parts = str.split("%d")
for (i in parts.indices) { for (i in parts.indices) {
container.addView( container.addView(
TextView(activity).apply { text = parts[i].trim() }, 2 * i + 1, TextView(activity).apply { text = parts[i].trim() },
2 * i + 1
) )
} }
} }

@ -69,7 +69,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
theme = themeSwitcher.currentTheme, theme = themeSwitcher.currentTheme,
today = DateUtils.getTodayWithOffset().toLocalDate(), today = DateUtils.getTodayWithOffset().toLocalDate(),
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {}, onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
padding = 10.0, padding = 10.0
) )
dataView = AndroidDataView(requireContext(), null) dataView = AndroidDataView(requireContext(), null)
dataView.view = chart!! dataView.view = chart!!

@ -0,0 +1,104 @@
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.os.Bundle
import android.text.method.DigitsKeyListener
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
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.requestFocusWithKeyboard
import org.isoron.uhabits.utils.sres
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.text.NumberFormat
import java.text.ParseException
class NumberDialog : AppCompatDialogFragment() {
var onToggle: (Double, String) -> Unit = { _, _ -> }
var onDismiss: () -> Unit = {}
private var originalNotes: String = ""
private var originalValue: Double = 0.0
private lateinit var view: CheckmarkPopupBinding
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val appComponent = (requireActivity().application as HabitsApplication).component
val prefs = appComponent.preferences
view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
arrayOf(view.yesBtn, view.skipBtn).forEach {
it.setTextColor(requireArguments().getInt("color"))
}
arrayOf(view.noBtn, view.unknownBtn).forEach {
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
}
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
it.typeface = InterfaceUtils.getFontAwesome(requireContext())
}
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = View.GONE
view.numberButtons.visibility = View.VISIBLE
fixDecimalSeparator(view)
originalNotes = requireArguments().getString("notes")!!
originalValue = requireArguments().getDouble("value")
view.notes.setText(originalNotes)
view.value.setText(
when {
originalValue < 0.01 -> "0"
else -> DecimalFormat("#.##").format(originalValue)
}
)
view.value.setOnKeyListener { _, keyCode, event ->
if (event.action == MotionEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
save()
return@setOnKeyListener true
}
return@setOnKeyListener false
}
view.saveBtn.setOnClickListener {
save()
}
view.skipBtnNumber.setOnClickListener {
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
save()
}
view.notes.setOnEditorActionListener { v, actionId, event ->
save()
true
}
view.value.requestFocusWithKeyboard()
val dialog = Dialog(requireContext())
dialog.setContentView(view.root)
dialog.window?.apply {
setBackgroundDrawableResource(android.R.color.transparent)
}
dialog.setOnDismissListener { onDismiss() }
return dialog
}
private fun fixDecimalSeparator(view: CheckmarkPopupBinding) {
// https://stackoverflow.com/a/34256139
val separator = DecimalFormatSymbols.getInstance().decimalSeparator
view.value.keyListener = DigitsKeyListener.getInstance("0123456789$separator")
}
fun save() {
var value = originalValue
try {
val numberFormat = NumberFormat.getInstance()
val valueStr = view.value.text.toString()
value = numberFormat.parse(valueStr)!!.toDouble()
} catch (e: ParseException) {
// NOP
}
val notes = view.notes.text.toString()
onToggle(value, notes)
requireDialog().dismiss()
}
}

@ -1,116 +0,0 @@
/*
* 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/>.
*/
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.content.Context
import android.view.KeyEvent.KEYCODE_ENTER
import android.view.LayoutInflater
import android.view.MotionEvent.ACTION_DOWN
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.dimBehind
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.requestFocusWithKeyboard
import java.text.DecimalFormat
class NumberPopup(
private val context: Context,
private var notes: String,
private var value: Double,
private val prefs: Preferences,
private val anchor: View,
) {
var onToggle: (Double, String) -> Unit = { _, _ -> }
var onDismiss: () -> Unit = {}
private val originalValue = value
private lateinit var dialog: Dialog
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
// Required for round corners
container.clipToOutline = true
}
init {
view.numberButtons.visibility = VISIBLE
hideDisabledButtons()
populate()
}
private fun hideDisabledButtons() {
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = GONE
}
private fun populate() {
view.notes.setText(notes)
view.value.setText(
when {
value < 0.01 -> "0"
else -> DecimalFormat("#.##").format(value)
}
)
}
fun show() {
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
dialog.setContentView(view.root)
dialog.window?.apply {
setLayout(
view.root.dp(POPUP_WIDTH).toInt(),
view.root.dp(POPUP_HEIGHT).toInt()
)
setBackgroundDrawableResource(android.R.color.transparent)
}
dialog.setOnDismissListener {
onDismiss()
}
view.value.setOnKeyListener { _, keyCode, event ->
if (event.action == ACTION_DOWN && keyCode == KEYCODE_ENTER) {
save()
return@setOnKeyListener true
}
return@setOnKeyListener false
}
view.saveBtn.setOnClickListener {
save()
}
view.skipBtnNumber.setOnClickListener {
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
save()
}
view.value.requestFocusWithKeyboard()
dialog.setCanceledOnTouchOutside(true)
dialog.dimBehind()
dialog.dismissCurrentAndShow()
}
fun save() {
val value = view.value.text.toString().toDoubleOrNull() ?: originalValue
val notes = view.notes.text.toString()
onToggle(value, notes)
dialog.dismiss()
}
}

@ -181,8 +181,9 @@ class FrequencyChart : ScrollableChart {
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat() rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j) rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
val i = localeWeekdayList[j] % 7 val i = localeWeekdayList[j] % 7
if (values != null) if (values != null) {
drawMarker(canvas, rect, values[i], weekDaysInMonth[i]) drawMarker(canvas, rect, values[i], weekDaysInMonth[i])
}
rect.offset(0f, rowHeight) rect.offset(0f, rowHeight)
} }
drawFooter(canvas, rect, date) drawFooter(canvas, rect, date)
@ -196,13 +197,15 @@ class FrequencyChart : ScrollableChart {
rect.centerY() - 0.1f * em, rect.centerY() - 0.1f * em,
pText!! pText!!
) )
if (date[Calendar.MONTH] == 1) canvas.drawText( if (date[Calendar.MONTH] == 1) {
canvas.drawText(
dfYear!!.format(time), dfYear!!.format(time),
rect.centerX(), rect.centerX(),
rect.centerY() + 0.9f * em, rect.centerY() + 0.9f * em,
pText!! pText!!
) )
} }
}
private fun drawGrid(canvas: Canvas, rGrid: RectF?) { private fun drawGrid(canvas: Canvas, rGrid: RectF?) {
val nRows = 7 val nRows = 7

@ -148,8 +148,12 @@ class RingView : View {
pRing!!.color = inactiveColor!! pRing!!.color = inactiveColor!!
activeCanvas.drawArc(rect!!, angle - 90, 360 - angle, true, pRing!!) activeCanvas.drawArc(rect!!, angle - 90, 360 - angle, true, pRing!!)
if (thickness > 0) { if (thickness > 0) {
if (isTransparencyEnabled) pRing!!.xfermode = XFERMODE_CLEAR else pRing!!.color = if (isTransparencyEnabled) {
pRing!!.xfermode = XFERMODE_CLEAR
} else {
pRing!!.color =
backgroundColor!! backgroundColor!!
}
rect!!.inset(thickness, thickness) rect!!.inset(thickness, thickness)
activeCanvas.drawArc(rect!!, 0f, 360f, true, pRing!!) activeCanvas.drawArc(rect!!, 0f, 360f, true, pRing!!)
pRing!!.xfermode = null pRing!!.xfermode = null

@ -40,6 +40,7 @@ import org.isoron.uhabits.inject.ActivityContextModule
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
import org.isoron.uhabits.inject.HabitsActivityComponent import org.isoron.uhabits.inject.HabitsActivityComponent
import org.isoron.uhabits.inject.HabitsApplicationComponent import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.isoron.uhabits.utils.dismissCurrentDialog
import org.isoron.uhabits.utils.restartWithFade import org.isoron.uhabits.utils.restartWithFade
class ListHabitsActivity : AppCompatActivity(), Preferences.Listener { class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
@ -91,6 +92,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
midnightTimer.onPause() midnightTimer.onPause()
screen.onDetached() screen.onDetached()
adapter.cancelRefresh() adapter.cancelRefresh()
dismissCurrentDialog()
super.onPause() super.onPause()
} }
@ -99,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()

@ -94,7 +94,7 @@ class ListHabitsRootView @Inject constructor(
title = resources.getString(R.string.main_activity_title), title = resources.getString(R.string.main_activity_title),
color = PaletteColor(17), color = PaletteColor(17),
displayHomeAsUpEnabled = false, displayHomeAsUpEnabled = false,
theme = currentTheme(), theme = currentTheme()
) )
addView(rootView, MATCH_PARENT, MATCH_PARENT) addView(rootView, MATCH_PARENT, MATCH_PARENT)
listAdapter.setListView(listView) listAdapter.setListView(listView)

@ -22,14 +22,15 @@ package org.isoron.uhabits.activities.habits.list
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy import dagger.Lazy
import org.isoron.platform.gui.toInt import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPopup import org.isoron.uhabits.activities.common.dialogs.NumberDialog
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
@ -95,7 +96,7 @@ class ListHabitsScreen
private val colorPickerFactory: ColorPickerDialogFactory, private val colorPickerFactory: ColorPickerDialogFactory,
private val behavior: Lazy<ListHabitsBehavior>, private val behavior: Lazy<ListHabitsBehavior>,
private val preferences: Preferences, private val preferences: Preferences,
private val rootView: Lazy<ListHabitsRootView>, private val rootView: Lazy<ListHabitsRootView>
) : CommandRunner.Listener, ) : CommandRunner.Listener,
ListHabitsBehavior.Screen, ListHabitsBehavior.Screen,
ListHabitsMenuBehavior.Screen, ListHabitsMenuBehavior.Screen,
@ -233,17 +234,14 @@ class ListHabitsScreen
notes: String, notes: String,
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
) { ) {
val view = rootView.get() val fm = (context as AppCompatActivity).supportFragmentManager
NumberPopup( val dialog = NumberDialog()
context = context, dialog.arguments = Bundle().apply {
prefs = preferences, putDouble("value", value)
anchor = view, putString("notes", notes)
notes = notes,
value = value,
).apply {
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
show()
} }
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
dialog.dismissCurrentAndShow(fm, "numberDialog")
} }
override fun showCheckmarkPopup( override fun showCheckmarkPopup(
@ -252,18 +250,16 @@ class ListHabitsScreen
color: PaletteColor, color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback callback: ListHabitsBehavior.CheckMarkDialogCallback
) { ) {
val view = rootView.get() val theme = rootView.get().currentTheme()
CheckmarkPopup( val fm = (context as AppCompatActivity).supportFragmentManager
context = context, val dialog = CheckmarkDialog()
prefs = preferences, dialog.arguments = Bundle().apply {
anchor = view, putInt("color", theme.color(color).toInt())
color = view.currentTheme().color(color).toInt(), putInt("value", selectedValue)
notes = notes, putString("notes", notes)
value = selectedValue,
).apply {
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
show()
} }
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
dialog.dismissCurrentAndShow(fm, "checkmarkDialog")
} }
private fun getExecuteString(command: Command): String? { private fun getExecuteString(command: Command): String? {
@ -325,8 +321,11 @@ class ListHabitsScreen
private fun onExportDB() { private fun onExportDB() {
taskRunner.execute( taskRunner.execute(
exportDBFactory.create { filename -> exportDBFactory.create { filename ->
if (filename != null) activity.showSendFileScreen(filename) if (filename != null) {
else activity.showMessage(activity.resources.getString(R.string.could_not_export)) activity.showSendFileScreen(filename)
} else {
activity.showMessage(activity.resources.getString(R.string.could_not_export))
}
} }
) )
} }

@ -60,8 +60,11 @@ abstract class ButtonPanelView<T : View>(
repeat(buttonCount) { buttons.add(createButton()) } repeat(buttonCount) { buttons.add(createButton()) }
removeAllViews() removeAllViews()
if (reverse) buttons.reversed().forEach { addView(it) } if (reverse) {
else buttons.forEach { addView(it) } buttons.reversed().forEach { addView(it) }
} else {
buttons.forEach { addView(it) }
}
setupButtons() setupButtons()
requestLayout() requestLayout()
} }

@ -44,8 +44,6 @@ import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toMeasureSpec import org.isoron.uhabits.utils.toMeasureSpec
import javax.inject.Inject import javax.inject.Inject
const val TOGGLE_DELAY_MILLIS = 2000L
class CheckmarkButtonViewFactory class CheckmarkButtonViewFactory
@Inject constructor( @Inject constructor(
@ActivityContext val context: Context, @ActivityContext val context: Context,
@ -79,7 +77,7 @@ class CheckmarkButtonView(
invalidate() invalidate()
} }
var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> } var onToggle: (Int, String) -> Unit = { _, _ -> }
var onEdit: () -> Unit = { } var onEdit: () -> Unit = { }
@ -90,25 +88,31 @@ class CheckmarkButtonView(
setOnLongClickListener(this) setOnLongClickListener(this)
} }
fun performToggle(delay: Long) { fun performToggle() {
value = Entry.nextToggleValue( value = Entry.nextToggleValue(
value = value, value = value,
isSkipEnabled = preferences.isSkipEnabled, isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
) )
onToggle(value, notes, delay) onToggle(value, notes)
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
invalidate() invalidate()
} }
override fun onClick(v: View) { override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS) if (preferences.isShortToggleEnabled) {
else onEdit() performToggle()
} else {
onEdit()
}
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
if (preferences.isShortToggleEnabled) onEdit() if (preferences.isShortToggleEnabled) {
else performToggle(TOGGLE_DELAY_MILLIS) onEdit()
} else {
performToggle()
}
return true return true
} }
@ -142,8 +146,11 @@ class CheckmarkButtonView(
paint.color = when (value) { paint.color = when (value) {
YES_MANUAL, YES_AUTO, SKIP -> color YES_MANUAL, YES_AUTO, SKIP -> color
NO -> { NO -> {
if (preferences.areQuestionMarksEnabled) mediumContrastColor if (preferences.areQuestionMarksEnabled) {
else lowContrastColor mediumContrastColor
} else {
lowContrastColor
}
} }
else -> lowContrastColor else -> lowContrastColor
} }
@ -151,8 +158,11 @@ class CheckmarkButtonView(
SKIP -> R.string.fa_skipped SKIP -> R.string.fa_skipped
NO -> R.string.fa_times NO -> R.string.fa_times
UNKNOWN -> { UNKNOWN -> {
if (preferences.areQuestionMarksEnabled) R.string.fa_question if (preferences.areQuestionMarksEnabled) {
else R.string.fa_times R.string.fa_question
} else {
R.string.fa_times
}
} }
else -> R.string.fa_check else -> R.string.fa_check
} }

@ -60,7 +60,7 @@ class CheckmarkPanelView(
setupButtons() setupButtons()
} }
var onToggle: (Timestamp, Int, String, Long) -> Unit = { _, _, _, _ -> } var onToggle: (Timestamp, Int, String) -> Unit = { _, _, _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
@ -89,7 +89,7 @@ class CheckmarkPanelView(
else -> "" else -> ""
} }
button.color = color button.color = color
button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) } button.onToggle = { value, notes -> onToggle(timestamp, value, notes) }
button.onEdit = { onEdit(timestamp) } button.onEdit = { onEdit(timestamp) }
} }
} }

@ -156,10 +156,11 @@ class HabitCardListController @Inject constructor(
} }
private fun notifyListener() { private fun notifyListener() {
if (activeMode is SelectionMode) if (activeMode is SelectionMode) {
selectionMenu.get().onSelectionChange() selectionMenu.get().onSelectionChange()
else } else {
selectionMenu.get().onSelectionFinish() selectionMenu.get().onSelectionFinish()
} }
} }
} }
}

@ -57,13 +57,6 @@ class HabitCardViewFactory
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior) fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
} }
data class DelayedToggle(
var habit: Habit,
var timestamp: Timestamp,
var value: Int,
var notes: String
)
class HabitCardView( class HabitCardView(
@ActivityContext context: Context, @ActivityContext context: Context,
checkmarkPanelFactory: CheckmarkPanelViewFactory, checkmarkPanelFactory: CheckmarkPanelViewFactory,
@ -136,7 +129,6 @@ class HabitCardView(
private var scoreRing: RingView private var scoreRing: RingView
private var currentToggleTaskId = 0 private var currentToggleTaskId = 0
private var queuedToggles = mutableListOf<DelayedToggle>()
init { init {
scoreRing = RingView(context).apply { scoreRing = RingView(context).apply {
@ -160,12 +152,9 @@ class HabitCardView(
} }
checkmarkPanel = checkmarkPanelFactory.create().apply { checkmarkPanel = checkmarkPanelFactory.create().apply {
onToggle = { timestamp, value, notes, delay -> onToggle = { timestamp, value, notes ->
if (delay > 0) triggerRipple(timestamp) triggerRipple(timestamp)
habit?.let { habit?.let { behavior.onToggle(it, timestamp, value, notes) }
val taskId = queueToggle(it, timestamp, value, notes);
{ runPendingToggles(taskId) }.delay(delay)
}
} }
onEdit = { timestamp -> onEdit = { timestamp ->
triggerRipple(timestamp) triggerRipple(timestamp)
@ -205,25 +194,6 @@ class HabitCardView(
addView(innerFrame) addView(innerFrame)
} }
@Synchronized
private fun runPendingToggles(id: Int) {
if (currentToggleTaskId != id) return
for ((h, t, v, n) in queuedToggles) behavior.onToggle(h, t, v, n)
queuedToggles.clear()
}
@Synchronized
private fun queueToggle(
it: Habit,
timestamp: Timestamp,
value: Int,
notes: String,
): Int {
currentToggleTaskId += 1
queuedToggles.add(DelayedToggle(it, timestamp, value, notes))
return currentToggleTaskId
}
override fun onModelChange() { override fun onModelChange() {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
habit?.let { copyAttributesFrom(it) } habit?.let { copyAttributesFrom(it) }
@ -255,7 +225,6 @@ class HabitCardView(
} }
private fun copyAttributesFrom(h: Habit) { private fun copyAttributesFrom(h: Habit) {
fun getActiveColor(habit: Habit): Int { fun getActiveColor(habit: Habit): Int {
return when (habit.isArchived) { return when (habit.isArchived) {
true -> sres.getColor(R.attr.contrast60) true -> sres.getColor(R.attr.contrast60)
@ -301,7 +270,6 @@ class HabitCardView(
} }
private fun updateBackground(isSelected: Boolean) { private fun updateBackground(isSelected: Boolean) {
val background = when (isSelected) { val background = when (isSelected) {
true -> R.drawable.selected_box true -> R.drawable.selected_box
false -> R.drawable.ripple false -> R.drawable.ripple

@ -124,15 +124,20 @@ class HeaderView(
rect.set(0f, 0f, width, height) rect.set(0f, 0f, width, height)
rect.offset(canvas.width.toFloat() - dp(3.0f), 0f) rect.offset(canvas.width.toFloat() - dp(3.0f), 0f)
if (isReversed) rect.offset(-(index + 1) * width, 0f) if (isReversed) {
else rect.offset((index - buttonCount) * width, 0f) rect.offset(-(index + 1) * width, 0f)
} else {
rect.offset((index - buttonCount) * width, 0f)
}
if (isRTL()) rect.set( if (isRTL()) {
rect.set(
canvas.width - rect.right, canvas.width - rect.right,
rect.top, rect.top,
canvas.width - rect.left, canvas.width - rect.left,
rect.bottom rect.bottom
) )
}
val y1 = rect.centerY() - 0.25 * em val y1 = rect.centerY() - 0.25 * em
val y2 = rect.centerY() + 1.25 * em val y2 = rect.centerY() + 1.25 * em

@ -34,10 +34,10 @@ import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.HabitsDirFinder import org.isoron.uhabits.activities.HabitsDirFinder
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPopup import org.isoron.uhabits.activities.common.dialogs.NumberDialog
import org.isoron.uhabits.core.commands.Command import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
@ -51,6 +51,7 @@ import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import org.isoron.uhabits.intents.IntentFactory import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.utils.currentTheme import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dismissCurrentAndShow import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.dismissCurrentDialog
import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendFileScreen import org.isoron.uhabits.utils.showSendFileScreen
import org.isoron.uhabits.widgets.WidgetUpdater import org.isoron.uhabits.widgets.WidgetUpdater
@ -87,7 +88,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
habit = habit, habit = habit,
habitList = habitList, habitList = habitList,
preferences = preferences, preferences = preferences,
screen = screen, screen = screen
) )
view = ShowHabitView(this) view = ShowHabitView(this)
@ -98,13 +99,13 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
habitList = habitList, habitList = habitList,
screen = screen, screen = screen,
system = HabitsDirFinder(AndroidDirFinder(this)), system = HabitsDirFinder(AndroidDirFinder(this)),
taskRunner = appComponent.taskRunner, taskRunner = appComponent.taskRunner
) )
menu = ShowHabitMenu( menu = ShowHabitMenu(
activity = this, activity = this,
presenter = menuPresenter, presenter = menuPresenter,
preferences = preferences, preferences = preferences
) )
view.setListener(presenter) view.setListener(presenter)
@ -129,6 +130,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
} }
override fun onPause() { override fun onPause() {
dismissCurrentDialog()
commandRunner.removeListener(this) commandRunner.removeListener(this)
super.onPause() super.onPause()
} }
@ -148,7 +150,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
ShowHabitPresenter.buildState( ShowHabitPresenter.buildState(
habit = habit, habit = habit,
preferences = preferences, preferences = preferences,
theme = themeSwitcher.currentTheme, theme = themeSwitcher.currentTheme
) )
) )
} }
@ -170,41 +172,32 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
override fun showNumberPopup( override fun showNumberPopup(
value: Double, value: Double,
notes: String, notes: String,
preferences: Preferences,
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
) { ) {
val anchor = getPopupAnchor() ?: return val dialog = NumberDialog()
NumberPopup( dialog.arguments = Bundle().apply {
context = this@ShowHabitActivity, putDouble("value", value)
prefs = preferences, putString("notes", notes)
notes = notes,
anchor = anchor,
value = value,
).apply {
onToggle = { v, n -> callback.onNumberPicked(v, n) }
show()
} }
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog")
} }
override fun showCheckmarkPopup( override fun showCheckmarkPopup(
selectedValue: Int, selectedValue: Int,
notes: String, notes: String,
preferences: Preferences,
color: PaletteColor, color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback callback: ListHabitsBehavior.CheckMarkDialogCallback
) { ) {
val anchor = getPopupAnchor() ?: return val theme = view.currentTheme()
CheckmarkPopup( val dialog = CheckmarkDialog()
context = this@ShowHabitActivity, dialog.arguments = Bundle().apply {
prefs = preferences, putInt("color", theme.color(color).toInt())
notes = notes, putInt("value", selectedValue)
color = view.currentTheme().color(color).toInt(), putString("notes", notes)
anchor = anchor,
value = selectedValue,
).apply {
onToggle = { v, n -> callback.onNotesSaved(v, n) }
show()
} }
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog")
} }
private fun getPopupAnchor(): View? { private fun getPopupAnchor(): View? {

@ -28,7 +28,7 @@ import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
class ShowHabitMenu( class ShowHabitMenu(
val activity: ShowHabitActivity, val activity: ShowHabitActivity,
val presenter: ShowHabitMenuPresenter, val presenter: ShowHabitMenuPresenter,
val preferences: Preferences, val preferences: Preferences
) { ) {
fun onCreateOptionsMenu(menu: Menu): Boolean { fun onCreateOptionsMenu(menu: Menu): Boolean {
activity.menuInflater.inflate(R.menu.show_habit, menu) activity.menuInflater.inflate(R.menu.show_habit, menu)

@ -39,7 +39,7 @@ class ShowHabitView(context: Context) : FrameLayout(context) {
binding.toolbar, binding.toolbar,
title = data.title, title = data.title,
color = data.color, color = data.color,
theme = data.theme, theme = data.theme
) )
binding.subtitleCard.setState(data.subtitle) binding.subtitleCard.setState(data.subtitle)
binding.overviewCard.setState(data.overview) binding.overviewCard.setState(data.overview)

@ -63,7 +63,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
parent: AdapterView<*>?, parent: AdapterView<*>?,
view: View?, view: View?,
position: Int, position: Int,
id: Long, id: Long
) { ) {
presenter.onBoolSpinnerPosition(position) presenter.onBoolSpinnerPosition(position)
} }
@ -77,7 +77,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
parent: AdapterView<*>?, parent: AdapterView<*>?,
view: View?, view: View?,
position: Int, position: Int,
id: Long, id: Long
) { ) {
presenter.onNumericalSpinnerPosition(position) presenter.onNumericalSpinnerPosition(position)
} }

@ -45,7 +45,7 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
series = state.series, series = state.series,
defaultSquare = state.defaultSquare, defaultSquare = state.defaultSquare,
notesIndicators = state.notesIndicators, notesIndicators = state.notesIndicators,
firstWeekday = state.firstWeekday, firstWeekday = state.firstWeekday
) )
binding.chart.postInvalidate() binding.chart.postInvalidate()
} }

@ -52,7 +52,7 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
binding.frequencyLabel.text = formatFrequency( binding.frequencyLabel.text = formatFrequency(
state.frequency.numerator, state.frequency.numerator,
state.frequency.denominator, state.frequency.denominator,
resources, resources
) )
binding.questionLabel.setTextColor(color) binding.questionLabel.setTextColor(color)
binding.questionLabel.text = state.question binding.questionLabel.text = state.question

@ -40,7 +40,7 @@ class SettingsActivity : AppCompatActivity() {
toolbar = binding.toolbar, toolbar = binding.toolbar,
title = resources.getString(R.string.settings), title = resources.getString(R.string.settings),
color = PaletteColor(11), color = PaletteColor(11),
theme = themeSwitcher.currentTheme, theme = themeSwitcher.currentTheme
) )
setContentView(binding.root) setContentView(binding.root)
} }

@ -22,6 +22,7 @@ import android.app.backup.BackupManager
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
@ -43,6 +44,7 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames
import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel
import org.isoron.uhabits.notifications.RingtoneManager import org.isoron.uhabits.notifications.RingtoneManager
import org.isoron.uhabits.utils.StyledResources import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.startActivitySafely
import org.isoron.uhabits.widgets.WidgetUpdater import org.isoron.uhabits.widgets.WidgetUpdater
import java.util.Calendar import java.util.Calendar
@ -92,10 +94,12 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
override fun onPreferenceTreeClick(preference: Preference): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
val key = preference.key ?: return false val key = preference.key ?: return false
if (key == "reminderSound") { when (key) {
"reminderSound" -> {
showRingtonePicker() showRingtonePicker()
return true return true
} else if (key == "reminderCustomize") { }
"reminderCustomize" -> {
createAndroidNotificationChannel(requireContext()) createAndroidNotificationChannel(requireContext())
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
@ -103,6 +107,12 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
startActivity(intent) startActivity(intent)
return true return true
} }
"rateApp" -> {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.playStoreURL)))
activity?.startActivitySafely(intent)
return true
}
}
return super.onPreferenceTreeClick(preference) return super.onPreferenceTreeClick(preference)
} }

@ -32,7 +32,7 @@ class EditSettingActivity : AppCompatActivity() {
val habits = app.component.habitList.getFiltered( val habits = app.component.habitList.getFiltered(
HabitMatcher( HabitMatcher(
isArchivedAllowed = false, isArchivedAllowed = false,
isCompletedAllowed = true, isCompletedAllowed = true
) )
) )
AndroidThemeSwitcher(this, app.component.preferences).apply() AndroidThemeSwitcher(this, app.component.preferences).apply()
@ -43,7 +43,7 @@ class EditSettingActivity : AppCompatActivity() {
context = this, context = this,
habitList = app.component.habitList, habitList = app.component.habitList,
onSave = controller::onSave, onSave = controller::onSave,
args = args, args = args
) )
setContentView(view) setContentView(view)
} }

@ -54,7 +54,7 @@ class EditSettingRootView(
title = resources.getString(R.string.app_name), title = resources.getString(R.string.app_name),
color = PaletteColor(11), color = PaletteColor(11),
displayHomeAsUpEnabled = false, displayHomeAsUpEnabled = false,
theme = currentTheme(), theme = currentTheme()
) )
populateHabitSpinner() populateHabitSpinner()
binding.habitSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.habitSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
@ -69,7 +69,7 @@ class EditSettingRootView(
val habit = habitList.getByPosition(binding.habitSpinner.selectedItemPosition) val habit = habitList.getByPosition(binding.habitSpinner.selectedItemPosition)
val action = mapSpinnerPositionToAction( val action = mapSpinnerPositionToAction(
isNumerical = habit.isNumerical, isNumerical = habit.isNumerical,
itemPosition = binding.actionSpinner.selectedItemPosition, itemPosition = binding.actionSpinner.selectedItemPosition
) )
onSave(habit, action) onSave(habit, action)
} }

@ -27,22 +27,34 @@ class AndroidCursor(private val cursor: android.database.Cursor) : Cursor {
override fun moveToNext() = cursor.moveToNext() override fun moveToNext() = cursor.moveToNext()
override fun getInt(index: Int): Int? { override fun getInt(index: Int): Int? {
return if (cursor.isNull(index)) null return if (cursor.isNull(index)) {
else cursor.getInt(index) null
} else {
cursor.getInt(index)
}
} }
override fun getLong(index: Int): Long? { override fun getLong(index: Int): Long? {
return if (cursor.isNull(index)) null return if (cursor.isNull(index)) {
else cursor.getLong(index) null
} else {
cursor.getLong(index)
}
} }
override fun getDouble(index: Int): Double? { override fun getDouble(index: Int): Double? {
return if (cursor.isNull(index)) null return if (cursor.isNull(index)) {
else cursor.getDouble(index) null
} else {
cursor.getDouble(index)
}
} }
override fun getString(index: Int): String? { override fun getString(index: Int): String? {
return if (cursor.isNull(index)) null return if (cursor.isNull(index)) {
else cursor.getString(index) null
} else {
cursor.getString(index)
}
} }
} }

@ -26,7 +26,7 @@ import java.io.File
class AndroidDatabase( class AndroidDatabase(
private val db: SQLiteDatabase, private val db: SQLiteDatabase,
override val file: File?, override val file: File?
) : Database { ) : Database {
override fun beginTransaction() = db.beginTransaction() override fun beginTransaction() = db.beginTransaction()
@ -45,7 +45,7 @@ class AndroidDatabase(
tableName: String, tableName: String,
values: Map<String, Any?>, values: Map<String, Any?>,
where: String, where: String,
vararg params: String, vararg params: String
): Int { ): Int {
val contValues = mapToContentValues(values) val contValues = mapToContentValues(values)
return db.update(tableName, contValues, where, params) return db.update(tableName, contValues, where, params)
@ -59,7 +59,7 @@ class AndroidDatabase(
override fun delete( override fun delete(
tableName: String, tableName: String,
where: String, where: String,
vararg params: String, vararg params: String
) { ) {
db.delete(tableName, where, params) db.delete(tableName, where, params)
} }

@ -30,7 +30,7 @@ class AndroidDatabaseOpener @Inject constructor() : DatabaseOpener {
db = SQLiteDatabase.openDatabase( db = SQLiteDatabase.openDatabase(
file.absolutePath, file.absolutePath,
null, null,
SQLiteDatabase.OPEN_READWRITE, SQLiteDatabase.OPEN_READWRITE
), ),
file = file file = file
) )

@ -25,5 +25,6 @@ import dagger.Provides
@Module @Module
class ActivityContextModule( class ActivityContextModule(
@get:Provides @get:Provides
@get:ActivityContext val context: Context @get:ActivityContext
val context: Context
) )

@ -26,5 +26,6 @@ import dagger.Provides
class AppContextModule( class AppContextModule(
@get:Provides @get:Provides
@get:AppContext @get:AppContext
@param:AppContext val context: Context @param:AppContext
val context: Context
) )

@ -53,8 +53,9 @@ class IntentParser
var timestamp = intent.getLongExtra("timestamp", today) var timestamp = intent.getLongExtra("timestamp", today)
timestamp = DateUtils.getStartOfDay(timestamp) timestamp = DateUtils.getStartOfDay(timestamp)
if (timestamp < 0 || timestamp > today) if (timestamp < 0 || timestamp > today) {
throw IllegalArgumentException("timestamp is not valid") throw IllegalArgumentException("timestamp is not valid")
}
return Timestamp(timestamp) return Timestamp(timestamp)
} }

@ -96,7 +96,6 @@ class AndroidNotificationTray
timestamp: Timestamp, timestamp: Timestamp,
disableSound: Boolean = false disableSound: Boolean = false
): Notification { ): Notification {
val addRepetitionAction = Action( val addRepetitionAction = Action(
R.drawable.ic_action_check, R.drawable.ic_action_check,
context.getString(R.string.yes), context.getString(R.string.yes),
@ -146,8 +145,9 @@ class AndroidNotificationTray
.addAction(removeRepetitionAction) .addAction(removeRepetitionAction)
} }
if (!disableSound) if (!disableSound) {
builder.setSound(ringtoneManager.getURI()) builder.setSound(ringtoneManager.getURI())
}
if (SDK_INT < Build.VERSION_CODES.S) { if (SDK_INT < Build.VERSION_CODES.S) {
val snoozeAction = Action( val snoozeAction = Action(

@ -61,8 +61,9 @@ class RingtoneManager
"pref_ringtone_uri", "pref_ringtone_uri",
defaultRingtoneUri.toString() defaultRingtoneUri.toString()
)!! )!!
if (prefRingtoneUri.isNotEmpty()) if (prefRingtoneUri.isNotEmpty()) {
ringtoneUri = Uri.parse(prefRingtoneUri) ringtoneUri = Uri.parse(prefRingtoneUri)
}
return ringtoneUri return ringtoneUri
} }

@ -33,7 +33,8 @@ import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_LIGHT import org.isoron.uhabits.core.ui.views.DarkTheme
import org.isoron.uhabits.core.ui.views.LightTheme
import org.isoron.uhabits.receivers.ReminderController import org.isoron.uhabits.receivers.ReminderController
import org.isoron.uhabits.utils.SystemUtils import org.isoron.uhabits.utils.SystemUtils
import java.util.Calendar import java.util.Calendar
@ -51,11 +52,8 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
val app = applicationContext as HabitsApplication val app = applicationContext as HabitsApplication
val appComponent = app.component val appComponent = app.component
val themeSwitcher = AndroidThemeSwitcher(this, appComponent.preferences) val themeSwitcher = AndroidThemeSwitcher(this, appComponent.preferences)
if (themeSwitcher.getSystemTheme() == THEME_LIGHT) { themeSwitcher.setTheme()
setTheme(R.style.BaseDialog)
} else {
setTheme(R.style.BaseDialogDark)
}
val data = intent.data val data = intent.data
if (data == null) { if (data == null) {
finish() finish()
@ -75,6 +73,16 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
SystemUtils.unlockScreen(this) SystemUtils.unlockScreen(this)
} }
private fun AndroidThemeSwitcher.setTheme() {
if (this.isNightMode) {
setTheme(R.style.BaseDialogDark)
this.currentTheme = DarkTheme()
} else {
setTheme(R.style.BaseDialog)
this.currentTheme = LightTheme()
}
}
private fun showTimePicker() { private fun showTimePicker() {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
val dialog = TimePickerDialog.newInstance( val dialog = TimePickerDialog.newInstance(
@ -95,7 +103,9 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
if (snoozeValues[position] >= 0) { if (snoozeValues[position] >= 0) {
reminderController!!.onSnoozeDelayPicked(habit!!, snoozeValues[position]) reminderController!!.onSnoozeDelayPicked(habit!!, snoozeValues[position])
finish() finish()
} else showTimePicker() } else {
showTimePicker()
}
} }
override fun finish() { override fun finish() {

@ -27,7 +27,7 @@ import javax.inject.Inject
class ExportDBTaskFactory class ExportDBTaskFactory
@Inject constructor( @Inject constructor(
@AppContext private val context: Context, @AppContext private val context: Context,
private val system: AndroidDirFinder, private val system: AndroidDirFinder
) { ) {
fun create(listener: ExportDBTask.Listener) = ExportDBTask(context, system, listener) fun create(listener: ExportDBTask.Listener) = ExportDBTask(context, system, listener)
} }

@ -27,7 +27,7 @@ import javax.inject.Inject
class ImportDataTaskFactory class ImportDataTaskFactory
@Inject constructor( @Inject constructor(
private val importer: GenericImporter, private val importer: GenericImporter,
private val modelFactory: ModelFactory, private val modelFactory: ModelFactory
) { ) {
fun create(file: File, listener: ImportDataTask.Listener) = fun create(file: File, listener: ImportDataTask.Listener) =
ImportDataTask(importer, modelFactory, file, listener) ImportDataTask(importer, modelFactory, file, listener)

@ -24,6 +24,7 @@ import org.jetbrains.annotations.Contract
object AttributeSetUtils { object AttributeSetUtils {
const val ISORON_NAMESPACE = "http://isoron.org/android" const val ISORON_NAMESPACE = "http://isoron.org/android"
@JvmStatic @JvmStatic
fun getAttribute( fun getAttribute(
context: Context, context: Context,

@ -33,6 +33,7 @@ import java.text.SimpleDateFormat
object DatabaseUtils { object DatabaseUtils {
private var opener: HabitsDatabaseOpener? = null private var opener: HabitsDatabaseOpener? = null
@JvmStatic @JvmStatic
fun getDatabaseFile(context: Context): File { fun getDatabaseFile(context: Context): File {
val databaseFilename = databaseFilename val databaseFilename = databaseFilename

@ -6,16 +6,24 @@ import androidx.fragment.app.FragmentManager
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
var currentDialog: WeakReference<Dialog> = WeakReference(null) var currentDialog: WeakReference<Dialog> = WeakReference(null)
var currentDialogFragment: WeakReference<DialogFragment> = WeakReference(null)
fun Dialog.dismissCurrentAndShow() { fun dismissCurrentDialog() {
currentDialog.get()?.dismiss() currentDialog.get()?.dismiss()
currentDialog = WeakReference(null)
currentDialogFragment.get()?.dismiss()
currentDialogFragment = WeakReference(null)
}
fun Dialog.dismissCurrentAndShow() {
dismissCurrentDialog()
currentDialog = WeakReference(this) currentDialog = WeakReference(this)
show() show()
} }
fun DialogFragment.dismissCurrentAndShow(fragmentManager: FragmentManager, tag: String) { fun DialogFragment.dismissCurrentAndShow(fragmentManager: FragmentManager, tag: String) {
currentDialog.get()?.dismiss() dismissCurrentDialog()
currentDialogFragment = WeakReference(this)
show(fragmentManager, tag) show(fragmentManager, tag)
fragmentManager.executePendingTransactions() fragmentManager.executePendingTransactions()
currentDialog = WeakReference(this.dialog)
} }

@ -61,7 +61,6 @@ fun RelativeLayout.addBelow(
height: Int = WRAP_CONTENT, height: Int = WRAP_CONTENT,
applyCustomRules: (params: RelativeLayout.LayoutParams) -> Unit = {} applyCustomRules: (params: RelativeLayout.LayoutParams) -> Unit = {}
) { ) {
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply { view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
addRule(BELOW, subject.id) addRule(BELOW, subject.id)
applyCustomRules(this) applyCustomRules(this)
@ -75,7 +74,6 @@ fun RelativeLayout.addAtBottom(
width: Int = MATCH_PARENT, width: Int = MATCH_PARENT,
height: Int = WRAP_CONTENT height: Int = WRAP_CONTENT
) { ) {
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply { view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
addRule(ALIGN_PARENT_BOTTOM) addRule(ALIGN_PARENT_BOTTOM)
} }
@ -88,7 +86,6 @@ fun RelativeLayout.addAtTop(
width: Int = MATCH_PARENT, width: Int = MATCH_PARENT,
height: Int = WRAP_CONTENT height: Int = WRAP_CONTENT
) { ) {
view.layoutParams = RelativeLayout.LayoutParams(width, height).apply { view.layoutParams = RelativeLayout.LayoutParams(width, height).apply {
addRule(ALIGN_PARENT_TOP) addRule(ALIGN_PARENT_TOP)
} }
@ -159,7 +156,7 @@ fun Activity.restartWithFade(cls: Class<*>?) {
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
startActivity(Intent(this, cls)) startActivity(Intent(this, cls))
}, },
500, 500
) // HACK: Let the menu disappear first ) // HACK: Let the menu disappear first
} }
@ -168,7 +165,7 @@ fun View.setupToolbar(
title: String, title: String,
color: PaletteColor, color: PaletteColor,
theme: Theme, theme: Theme,
displayHomeAsUpEnabled: Boolean = true, displayHomeAsUpEnabled: Boolean = true
) { ) {
toolbar.elevation = InterfaceUtils.dpToPixels(context, 2f) toolbar.elevation = InterfaceUtils.dpToPixels(context, 2f)
val res = StyledResources(context) val res = StyledResources(context)

@ -147,11 +147,12 @@ abstract class BaseWidget(val context: Context, val id: Int, val stacked: Boolea
protected val preferedBackgroundAlpha: Int protected val preferedBackgroundAlpha: Int
get() { get() {
return if (stacked) return if (stacked) {
255 255
else } else {
prefs.widgetOpacity prefs.widgetOpacity
} }
}
init { init {
val app = context.applicationContext as HabitsApplication val app = context.applicationContext as HabitsApplication

@ -33,7 +33,7 @@ open class CheckmarkWidget(
context: Context, context: Context,
widgetId: Int, widgetId: Int,
protected val habit: Habit, protected val habit: Habit,
stacked: Boolean = false, stacked: Boolean = false
) : BaseWidget(context, widgetId, stacked) { ) : BaseWidget(context, widgetId, stacked) {
override val defaultHeight: Int = 125 override val defaultHeight: Int = 125
@ -41,7 +41,7 @@ open class CheckmarkWidget(
override fun getOnClickPendingIntent(context: Context): PendingIntent? { override fun getOnClickPendingIntent(context: Context): PendingIntent? {
return if (habit.isNumerical) { return if (habit.isNumerical) {
pendingIntentFactory.showNumberPicker(habit, DateUtils.getToday()) pendingIntentFactory.showNumberPicker(habit, DateUtils.getTodayWithOffset())
} else { } else {
pendingIntentFactory.toggleCheckmark(habit, null) pendingIntentFactory.toggleCheckmark(habit, null)
} }

@ -23,7 +23,10 @@ import android.content.Context
class CheckmarkWidgetProvider : BaseWidgetProvider() { class CheckmarkWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget { override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id) val habits = getHabitsFromWidgetId(id)
return if (habits.size == 1) CheckmarkWidget(context, id, habits[0]) return if (habits.size == 1) {
else StackWidget(context, id, StackWidgetType.CHECKMARK, habits) CheckmarkWidget(context, id, habits[0])
} else {
StackWidget(context, id, StackWidgetType.CHECKMARK, habits)
}
} }
} }

@ -27,7 +27,7 @@ import org.isoron.uhabits.widgets.views.EmptyWidgetView
class EmptyWidget( class EmptyWidget(
context: Context, context: Context,
widgetId: Int, widgetId: Int,
stacked: Boolean = false, stacked: Boolean = false
) : BaseWidget(context, widgetId, stacked) { ) : BaseWidget(context, widgetId, stacked) {
override val defaultHeight: Int = 200 override val defaultHeight: Int = 200
override val defaultWidth: Int = 200 override val defaultWidth: Int = 200

@ -33,7 +33,7 @@ class FrequencyWidget(
widgetId: Int, widgetId: Int,
private val habit: Habit, private val habit: Habit,
private val firstWeekday: Int, private val firstWeekday: Int,
stacked: Boolean = false, stacked: Boolean = false
) : BaseWidget(context, widgetId, stacked) { ) : BaseWidget(context, widgetId, stacked) {
override val defaultHeight: Int = 200 override val defaultHeight: Int = 200
override val defaultWidth: Int = 200 override val defaultWidth: Int = 200

@ -24,12 +24,15 @@ import android.content.Context
class FrequencyWidgetProvider : BaseWidgetProvider() { class FrequencyWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget { override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id) val habits = getHabitsFromWidgetId(id)
return if (habits.size == 1) FrequencyWidget( return if (habits.size == 1) {
FrequencyWidget(
context, context,
id, id,
habits[0], habits[0],
preferences.firstWeekdayInt preferences.firstWeekdayInt
) )
else StackWidget(context, id, StackWidgetType.FREQUENCY, habits) } else {
StackWidget(context, id, StackWidgetType.FREQUENCY, habits)
}
} }
} }

@ -36,7 +36,7 @@ class HistoryWidget(
context: Context, context: Context,
id: Int, id: Int,
private val habit: Habit, private val habit: Habit,
stacked: Boolean = false, stacked: Boolean = false
) : BaseWidget(context, id, stacked) { ) : BaseWidget(context, id, stacked) {
override val defaultHeight: Int = 250 override val defaultHeight: Int = 250
@ -53,7 +53,7 @@ class HistoryWidget(
val model = HistoryCardPresenter.buildState( val model = HistoryCardPresenter.buildState(
habit = habit, habit = habit,
firstWeekday = prefs.firstWeekday, firstWeekday = prefs.firstWeekday,
theme = WidgetTheme(), theme = WidgetTheme()
) )
(widgetView.dataView as AndroidDataView).apply { (widgetView.dataView as AndroidDataView).apply {
val historyChart = (this.view as HistoryChart) val historyChart = (this.view as HistoryChart)
@ -75,7 +75,7 @@ class HistoryWidget(
firstWeekday = prefs.firstWeekday, firstWeekday = prefs.firstWeekday,
series = listOf(), series = listOf(),
defaultSquare = HistoryChart.Square.OFF, defaultSquare = HistoryChart.Square.OFF,
notesIndicators = listOf(), notesIndicators = listOf()
) )
} }
).apply { ).apply {

@ -23,11 +23,14 @@ import android.content.Context
class HistoryWidgetProvider : BaseWidgetProvider() { class HistoryWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget { override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id) val habits = getHabitsFromWidgetId(id)
return if (habits.size == 1) HistoryWidget( return if (habits.size == 1) {
HistoryWidget(
context, context,
id, id,
habits[0] habits[0]
) )
else StackWidget(context, id, StackWidgetType.HISTORY, habits) } else {
StackWidget(context, id, StackWidgetType.HISTORY, habits)
}
} }
} }

@ -33,7 +33,7 @@ class ScoreWidget(
context: Context, context: Context,
id: Int, id: Int,
private val habit: Habit, private val habit: Habit,
stacked: Boolean = false, stacked: Boolean = false
) : BaseWidget(context, id, stacked) { ) : BaseWidget(context, id, stacked) {
override val defaultHeight: Int = 300 override val defaultHeight: Int = 300
override val defaultWidth: Int = 300 override val defaultWidth: Int = 300
@ -46,7 +46,7 @@ class ScoreWidget(
habit = habit, habit = habit,
firstWeekday = prefs.firstWeekdayInt, firstWeekday = prefs.firstWeekdayInt,
spinnerPosition = prefs.scoreCardSpinnerPosition, spinnerPosition = prefs.scoreCardSpinnerPosition,
theme = WidgetTheme(), theme = WidgetTheme()
) )
val widgetView = view as GraphWidgetView val widgetView = view as GraphWidgetView
widgetView.setBackgroundAlpha(preferedBackgroundAlpha) widgetView.setBackgroundAlpha(preferedBackgroundAlpha)

@ -23,7 +23,10 @@ import android.content.Context
class ScoreWidgetProvider : BaseWidgetProvider() { class ScoreWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget { override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id) val habits = getHabitsFromWidgetId(id)
return if (habits.size == 1) ScoreWidget(context, id, habits[0]) return if (habits.size == 1) {
else StackWidget(context, id, StackWidgetType.SCORE, habits) ScoreWidget(context, id, habits[0])
} else {
StackWidget(context, id, StackWidgetType.SCORE, habits)
}
} }
} }

@ -34,7 +34,7 @@ class StackWidget(
widgetId: Int, widgetId: Int,
private val widgetType: StackWidgetType, private val widgetType: StackWidgetType,
private val habits: List<Habit>, private val habits: List<Habit>,
stacked: Boolean = true, stacked: Boolean = true
) : BaseWidget(context, widgetId, stacked) { ) : BaseWidget(context, widgetId, stacked) {
override val defaultHeight: Int = 0 override val defaultHeight: Int = 0
override val defaultWidth: Int = 0 override val defaultWidth: Int = 0

@ -33,7 +33,7 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitNotFoundException import org.isoron.uhabits.core.models.HabitNotFoundException
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
import org.isoron.uhabits.intents.IntentFactory import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.intents.PendingIntentFactory import org.isoron.uhabits.intents.PendingIntentFactory
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
@ -101,7 +101,7 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
val landscapeViews = widget.landscapeRemoteViews val landscapeViews = widget.landscapeRemoteViews
val portraitViews = widget.portraitRemoteViews val portraitViews = widget.portraitRemoteViews
val factory = PendingIntentFactory(context, IntentFactory()) val factory = PendingIntentFactory(context, IntentFactory())
val intent = StackWidgetType.getIntentFillIn(factory, widgetType, h, habits, getToday()) val intent = StackWidgetType.getIntentFillIn(factory, widgetType, h, habits, getTodayWithOffset())
landscapeViews.setOnClickFillInIntent(R.id.button, intent) landscapeViews.setOnClickFillInIntent(R.id.button, intent)
portraitViews.setOnClickFillInIntent(R.id.button, intent) portraitViews.setOnClickFillInIntent(R.id.button, intent)
val remoteViews = RemoteViews(landscapeViews, portraitViews) val remoteViews = RemoteViews(landscapeViews, portraitViews)

@ -34,7 +34,7 @@ class StreakWidget(
context: Context, context: Context,
id: Int, id: Int,
private val habit: Habit, private val habit: Habit,
stacked: Boolean = false, stacked: Boolean = false
) : BaseWidget(context, id, stacked) { ) : BaseWidget(context, id, stacked) {
override val defaultHeight: Int = 200 override val defaultHeight: Int = 200
override val defaultWidth: Int = 200 override val defaultWidth: Int = 200

@ -23,7 +23,10 @@ import android.content.Context
class StreakWidgetProvider : BaseWidgetProvider() { class StreakWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget { override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id) val habits = getHabitsFromWidgetId(id)
return if (habits.size == 1) StreakWidget(context, id, habits[0]) return if (habits.size == 1) {
else StackWidget(context, id, StackWidgetType.STREAKS, habits) StreakWidget(context, id, habits[0])
} else {
StackWidget(context, id, StackWidgetType.STREAKS, habits)
}
} }
} }

@ -37,7 +37,7 @@ class TargetWidget(
context: Context, context: Context,
id: Int, id: Int,
private val habit: Habit, private val habit: Habit,
stacked: Boolean = false, stacked: Boolean = false
) : BaseWidget(context, id, stacked) { ) : BaseWidget(context, id, stacked) {
override val defaultHeight: Int = 200 override val defaultHeight: Int = 200
override val defaultWidth: Int = 200 override val defaultWidth: Int = 200
@ -53,7 +53,7 @@ class TargetWidget(
val data = TargetCardPresenter.buildState( val data = TargetCardPresenter.buildState(
habit = habit, habit = habit,
firstWeekday = prefs.firstWeekdayInt, firstWeekday = prefs.firstWeekdayInt,
theme = WidgetTheme(), theme = WidgetTheme()
) )
chart.setColor(WidgetTheme().color(habit.color).toInt()) chart.setColor(WidgetTheme().color(habit.color).toInt())
chart.setTargets(data.targets) chart.setTargets(data.targets)

@ -23,7 +23,10 @@ import android.content.Context
class TargetWidgetProvider : BaseWidgetProvider() { class TargetWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget { override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id) val habits = getHabitsFromWidgetId(id)
return if (habits.size == 1) TargetWidget(context, id, habits[0]) return if (habits.size == 1) {
else StackWidget(context, id, StackWidgetType.TARGET, habits) TargetWidget(context, id, habits[0])
} else {
StackWidget(context, id, StackWidgetType.TARGET, habits)
}
} }
} }

@ -24,7 +24,6 @@ import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.widget.AbsListView.CHOICE_MODE_MULTIPLE
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Button import android.widget.Button
import android.widget.ListView import android.widget.ListView
@ -34,7 +33,6 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.preferences.WidgetPreferences import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.widgets.WidgetUpdater import org.isoron.uhabits.widgets.WidgetUpdater
import java.util.ArrayList
class BooleanHabitPickerDialog : HabitPickerDialog() { class BooleanHabitPickerDialog : HabitPickerDialog() {
override fun shouldHideNumerical() = true override fun shouldHideNumerical() = true
@ -88,20 +86,12 @@ open class HabitPickerDialog : Activity() {
with(listView) { with(listView) {
adapter = ArrayAdapter( adapter = ArrayAdapter(
context, context,
android.R.layout.simple_list_item_multiple_choice, android.R.layout.simple_list_item_1,
habitNames habitNames
) )
choiceMode = CHOICE_MODE_MULTIPLE setOnItemClickListener { parent, view, position, id ->
itemsCanFocus = false confirm(mutableListOf(habitIds[position]))
} }
saveButton.setOnClickListener {
val selectedIds = mutableListOf<Long>()
for (i in 0..listView.count) {
if (listView.isItemChecked(i)) {
selectedIds.add(habitIds[i])
}
}
confirm(selectedIds)
} }
} }

@ -66,23 +66,21 @@ class CheckmarkWidgetView : HabitWidgetView {
val res = StyledResources(context) val res = StyledResources(context)
val bgColor: Int val bgColor: Int
val fgColor: Int val fgColor: Int
setShadowAlpha(0x4f)
when (entryState) { when (entryState) {
YES_MANUAL, SKIP -> { YES_MANUAL, SKIP -> {
bgColor = activeColor bgColor = activeColor
fgColor = res.getColor(R.attr.contrast0) fgColor = res.getColor(R.attr.contrast0)
setShadowAlpha(0x4f)
backgroundPaint!!.color = bgColor backgroundPaint!!.color = bgColor
frame!!.setBackgroundDrawable(background) frame!!.setBackgroundDrawable(background)
} }
YES_AUTO, NO, UNKNOWN -> { YES_AUTO, NO, UNKNOWN -> {
bgColor = res.getColor(R.attr.cardBgColor) bgColor = res.getColor(R.attr.cardBgColor)
fgColor = res.getColor(R.attr.contrast60) fgColor = res.getColor(R.attr.contrast60)
setShadowAlpha(0x00)
} }
else -> { else -> {
bgColor = res.getColor(R.attr.cardBgColor) bgColor = res.getColor(R.attr.cardBgColor)
fgColor = res.getColor(R.attr.contrast60) fgColor = res.getColor(R.attr.contrast60)
setShadowAlpha(0x00)
} }
} }
ring.setPercentage(percentage) ring.setPercentage(percentage)
@ -98,7 +96,8 @@ class CheckmarkWidgetView : HabitWidgetView {
private val text: String private val text: String
get() = if (isNumerical) { get() = if (isNumerical) {
(max(0, entryValue) / 1000.0).toShortString() (max(0, entryValue) / 1000.0).toShortString()
} else when (entryState) { } else {
when (entryState) {
YES_MANUAL, YES_AUTO -> resources.getString(R.string.fa_check) YES_MANUAL, YES_AUTO -> resources.getString(R.string.fa_check)
SKIP -> resources.getString(R.string.fa_skipped) SKIP -> resources.getString(R.string.fa_skipped)
UNKNOWN -> { UNKNOWN -> {
@ -114,6 +113,7 @@ class CheckmarkWidgetView : HabitWidgetView {
NO -> resources.getString(R.string.fa_times) NO -> resources.getString(R.string.fa_times)
else -> resources.getString(R.string.fa_times) else -> resources.getString(R.string.fa_times)
} }
}
override val innerLayoutId: Int override val innerLayoutId: Int
get() = R.layout.widget_checkmark get() = R.layout.widget_checkmark
@ -126,7 +126,7 @@ class CheckmarkWidgetView : HabitWidgetView {
} else { } else {
width = min(width, height) width = min(width, height)
} }
val textSize = min(0.2f * width, getDimension(context, R.dimen.smallerTextSize)) val textSize = min(0.175f * width, getDimension(context, R.dimen.smallTextSize))
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
if (isNumerical) { if (isNumerical) {
ring.setTextSize(textSize * 0.9f) ring.setTextSize(textSize * 0.9f)
@ -141,7 +141,8 @@ class CheckmarkWidgetView : HabitWidgetView {
} }
private fun init() { private fun init() {
val appComponent: HabitsApplicationComponent = (context.applicationContext as HabitsApplication).component val appComponent: HabitsApplicationComponent =
(context.applicationContext as HabitsApplication).component
preferences = appComponent.preferences preferences = appComponent.preferences
ring = findViewById<View>(R.id.scoreRing) as RingView ring = findViewById<View>(R.id.scoreRing) as RingView
label = findViewById<View>(R.id.label) as TextView label = findViewById<View>(R.id.label) as TextView

@ -69,7 +69,7 @@ abstract class HabitWidgetView : FrameLayout {
val shadowRadius = dpToPixels(context, 2f).toInt() val shadowRadius = dpToPixels(context, 2f).toInt()
val shadowOffset = dpToPixels(context, 1f).toInt() val shadowOffset = dpToPixels(context, 1f).toInt()
val shadowColor = Color.argb(shadowAlpha, 0, 0, 0) val shadowColor = Color.argb(shadowAlpha, 0, 0, 0)
val cornerRadius = dpToPixels(context, 5f) val cornerRadius = dpToPixels(context, 12f)
val radii = FloatArray(8) val radii = FloatArray(8)
Arrays.fill(radii, cornerRadius) Arrays.fill(radii, cornerRadius)
val shape = RoundRectShape(radii, null, null) val shape = RoundRectShape(radii, null, null)

@ -28,7 +28,7 @@
<item android:id="@android:id/mask"> <item android:id="@android:id/mask">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<corners android:radius="5dp"/> <corners android:radius="12dp"/>
<solid android:color="?android:colorPrimary"/> <solid android:color="?android:colorPrimary"/>
</shape> </shape>
<color android:color="@color/white"/> <color android:color="@color/white"/>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save