Compare commits

..

2 Commits

Author SHA1 Message Date
e8a4a9740e Merge branch 'dev' into feature/sync 2021-08-22 05:13:37 -05:00
7f1a1add8c Revert "Temporarily remove device sync"
This reverts commit da018fc64d.
2021-05-09 18:22:15 -05:00
314 changed files with 2518 additions and 3803 deletions

View File

@@ -13,10 +13,10 @@ jobs:
- name: Check out source code - name: Check out source code
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Install Java Development Kit 11 - name: Install Java Development Kit 1.8
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 1.8
- name: Build Project - name: Build Project
run: ./build.sh build run: ./build.sh build

View File

@@ -90,7 +90,7 @@ contribute, even if you are not a software developer.
is already completed, you are also very welcome to join and proofread it. is already completed, you are also very welcome to join and proofread it.
* **Write some code.** If you are an Android developer, you are very welcome to * **Write some code.** If you are an Android developer, you are very welcome to
contribute with code. Please see the [guidelines](https://github.com/iSoron/uhabits/blob/dev/docs/GUIDELINES.md). contribute with code. Please see `docs/GUIDELINES.md`.
## License ## License

View File

@@ -1,11 +1,11 @@
plugins { plugins {
val kotlinVersion = "1.6.10" val kotlinVersion = "1.5.0"
id("com.android.application") version ("7.0.3") apply (false) id("com.android.application") version ("4.2.0") 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 "10.3.0" id("org.jlleitschuh.gradle.ktlint") version "10.1.0"
} }
apply { apply {
@@ -15,9 +15,11 @@ apply {
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter()
mavenCentral() mavenCentral()
maven(url = "https://plugins.gradle.org/m2/") maven(url = "https://plugins.gradle.org/m2/")
maven(url = "https://oss.sonatype.org/content/repositories/snapshots/") maven(url = "https://oss.sonatype.org/content/repositories/snapshots/")
maven(url = "https://jitpack.io") maven(url = "https://kotlin.bintray.com/ktor")
maven(url = "https://kotlin.bintray.com/kotlin-js-wrappers")
} }
} }

View File

@@ -33,7 +33,7 @@ The repository will be downloaded to the directory `uhabits`.
2. When the IDE asks you for the project location, select `uhabits` and click "Ok". 2. When the IDE asks you for the project location, select `uhabits` and click "Ok".
3. Android Studio will spend some time indexing the project. When this is complete, click the toolbar icon "Sync Project with Gradle File", located near the right corner of the top toolbar. 3. Android Studio will spend some time indexing the project. When this is complete, click the toolbar icon "Sync Project with Gradle File", located near the right corner of the top toolbar.
4. The operation will likely fail several times due to missing Android SDK components. Each time it fails, click the link "Install missing platforms", "Install build tools", etc, and try again. 4. The operation will likely fail several times due to missing Android SDK components. Each time it fails, click the link "Install missing platforms", "Install build tools", etc, and try again.
5. To test the application, create a virtual Android device using the menu "Tools" and "AVD Manager". The default options should work fine, but feel free to customize the device. 5. To test the application, create a virtual Android device using the menu "Tools" and "AVD Manager". The default options should work fine, but free to customize the device.
6. Click the menu "Run" and "uhabits-android". The application should launch. 6. Click the menu "Run" and "uhabits-android". The application should launch.

View File

@@ -1,5 +1,5 @@
org.gradle.parallel=false org.gradle.parallel=false
org.gradle.daemon=true org.gradle.daemon=true
org.gradle.jvmargs=-Xms2048m -Xmx2048m org.gradle.jvmargs=-Xms2048m -Xmx2048m -XX:MaxPermSize=2048m
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -1,6 +1,7 @@
pluginManagement { pluginManagement {
repositories { repositories {
gradlePluginPortal() gradlePluginPortal()
jcenter()
google() google()
} }
resolutionStrategy.eachPlugin { resolutionStrategy.eachPlugin {

View File

@@ -1,3 +1,5 @@
/* /*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org> * Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
* *
@@ -18,7 +20,7 @@
*/ */
plugins { plugins {
id("com.github.triplet.play") version "3.7.0" id("com.github.triplet.play") version "3.5.0"
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt") id("org.jetbrains.kotlin.kapt")
@@ -32,15 +34,15 @@ tasks.compileLint {
android { android {
compileSdk = 31 compileSdkVersion(30)
defaultConfig { defaultConfig {
versionCode = 20003 versionCode(20003)
versionName = "2.0.3" versionName("2.0.3")
minSdk = 23 minSdkVersion(23)
targetSdk = 31 targetSdkVersion(30)
applicationId = "org.isoron.uhabits" applicationId("org.isoron.uhabits")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner("androidx.test.runner.AndroidJUnitRunner")
} }
signingConfigs { signingConfigs {
@@ -56,7 +58,7 @@ android {
buildTypes { buildTypes {
getByName("release") { getByName("release") {
isMinifyEnabled = true minifyEnabled(true)
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt") proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt")
if (signingConfigs.findByName("release") != null) { if (signingConfigs.findByName("release") != null) {
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
@@ -68,7 +70,7 @@ android {
} }
} }
lint { lintOptions {
isCheckReleaseBuilds = false isCheckReleaseBuilds = false
isAbortOnError = false isAbortOnError = false
disable("GoogleAppIndexingWarning") disable("GoogleAppIndexingWarning")
@@ -86,29 +88,29 @@ android {
} }
dependencies { dependencies {
val daggerVersion = "2.43.2" val daggerVersion = "2.38.1"
val kotlinVersion = "1.7.10" val kotlinVersion = "1.5.21"
val kxCoroutinesVersion = "1.6.4" val kxCoroutinesVersion = "1.5.1"
val ktorVersion = "1.6.8" val ktorVersion = "1.6.2"
val espressoVersion = "3.4.0" val espressoVersion = "3.4.0"
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")
androidTestImplementation("com.google.dagger:dagger:$daggerVersion") androidTestImplementation("com.google.dagger:dagger:$daggerVersion")
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3") androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1")
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.2.0")
androidTestImplementation("androidx.test.ext:junit:1.1.3") androidTestImplementation("androidx.test.ext:junit:1.1.3")
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.4.0")
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
compileOnly("javax.annotation:jsr250-api:1.0") compileOnly("javax.annotation:jsr250-api:1.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
implementation("com.github.AppIntro:AppIntro:6.2.0") implementation("com.github.paolorotolo:appintro:4.1.0")
implementation("com.google.code.findbugs:jsr305:3.0.2") implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.google.dagger:dagger:$daggerVersion") implementation("com.google.dagger:dagger:$daggerVersion")
implementation("com.google.guava:guava:31.1-android") implementation("com.google.guava:guava:30.1.1-android")
implementation("io.ktor:ktor-client-android:$ktorVersion") implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-jackson:$ktorVersion") implementation("io.ktor:ktor-client-jackson:$ktorVersion")
@@ -116,11 +118,12 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
implementation("androidx.appcompat:appcompat:1.4.2") implementation("androidx.appcompat:appcompat:1.3.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.4.0")
implementation("com.opencsv:opencsv:5.6") implementation("com.google.zxing:core:3.4.1")
implementation("com.opencsv:opencsv:5.5.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")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -55,7 +55,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class HabitsTest : BaseUserInterfaceTest() { class HabitsTest : BaseUserInterfaceTest() {
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun shouldCreateHabit() { fun shouldCreateHabit() {
@@ -181,8 +180,6 @@ class HabitsTest : BaseUserInterfaceTest() {
longPressCheckmarks("Wake up early", count = 2) longPressCheckmarks("Wake up early", count = 2)
clickText("Wake up early") clickText("Wake up early")
verifyShowsScreen(SHOW_HABIT) verifyShowsScreen(SHOW_HABIT)
// TODO: find a better way than sleeping in tests
Thread.sleep(2001L)
verifyDisplaysText("10%") verifyDisplaysText("10%")
} }
@@ -197,8 +194,6 @@ class HabitsTest : BaseUserInterfaceTest() {
verifyDoesNotDisplayText("Track time") verifyDoesNotDisplayText("Track time")
verifyDisplaysText("Wake up early") verifyDisplaysText("Wake up early")
longPressCheckmarks("Wake up early", count = 1) longPressCheckmarks("Wake up early", count = 1)
// TODO: find a better way than sleeping in tests
Thread.sleep(2001L)
verifyDoesNotDisplayText("Wake up early") verifyDoesNotDisplayText("Wake up early")
clickMenu(TOGGLE_COMPLETED) clickMenu(TOGGLE_COMPLETED)
verifyDisplaysText("Track time") verifyDisplaysText("Track time")

View File

@@ -18,8 +18,7 @@
*/ */
package org.isoron.uhabits.acceptance.steps package org.isoron.uhabits.acceptance.steps
import android.os.Build import android.os.Build.VERSION
import android.os.Build.VERSION.SDK_INT
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
@@ -134,7 +133,7 @@ object CommonSteps : BaseUserInterfaceTest() {
@Throws(Exception::class) @Throws(Exception::class)
fun verifyOpensWebsite(url: String?) { fun verifyOpensWebsite(url: String?) {
var browserPkg = "org.chromium.webview_shell" var browserPkg = "org.chromium.webview_shell"
if (SDK_INT <= Build.VERSION_CODES.M) { if (VERSION.SDK_INT <= 23) {
browserPkg = "com.android.browser" browserPkg = "com.android.browser"
} }
assertTrue(device.wait(Until.hasObject(By.pkg(browserPkg)), 5000)) assertTrue(device.wait(Until.hasObject(By.pkg(browserPkg)), 5000))
@@ -179,22 +178,6 @@ object CommonSteps : BaseUserInterfaceTest() {
EditHabitSteps.clickSave() EditHabitSteps.clickSave()
} }
fun changeFrequencyToDaily(habitName: String) {
clickText(habitName)
Espresso.onView(ViewMatchers.withId(R.id.action_edit_habit)).perform(ViewActions.click())
EditHabitSteps.pickDailyFrequency()
EditHabitSteps.clickSave()
pressBack()
}
fun changeFrequencyToMonthly(habitName: String) {
clickText(habitName)
Espresso.onView(ViewMatchers.withId(R.id.action_edit_habit)).perform(ViewActions.click())
EditHabitSteps.pickMonthFrequency()
EditHabitSteps.clickSave()
pressBack()
}
enum class Screen { enum class Screen {
LIST_HABITS, SHOW_HABIT, EDIT_HABIT, SELECT_HABIT_TYPE LIST_HABITS, SHOW_HABIT, EDIT_HABIT, SELECT_HABIT_TYPE
} }

View File

@@ -36,24 +36,6 @@ object EditHabitSteps {
Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click())
} }
fun pickMonthFrequency() {
Espresso.onView(ViewMatchers.withId(R.id.boolean_frequency_picker))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.xTimesPerMonthRadioButton))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.xTimesPerMonthTextView))
.perform(ViewActions.replaceText("1"))
Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click())
}
fun pickDailyFrequency() {
Espresso.onView(ViewMatchers.withId(R.id.boolean_frequency_picker))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.everyDayRadioButton))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click())
}
fun pickColor(color: Int) { fun pickColor(color: Int) {
Espresso.onView(ViewMatchers.withId(R.id.colorButton)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.colorButton)).perform(ViewActions.click())
BaseUserInterfaceTest.device.findObject(By.descStartsWith(String.format("Color %d", color))) BaseUserInterfaceTest.device.findObject(By.descStartsWith(String.format("Color %d", color)))

View File

@@ -120,12 +120,6 @@ object ListHabitsSteps {
BaseUserInterfaceTest.device.waitForIdle() BaseUserInterfaceTest.device.waitForIdle()
} }
fun changeSort(sortText: String) {
clickViewWithId(R.id.action_filter)
Espresso.onView(ViewMatchers.withText("Sort")).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText(sortText)).perform(ViewActions.click())
}
enum class MenuItem { enum class MenuItem {
ABOUT, HELP, SETTINGS, EDIT, DELETE, ARCHIVE, TOGGLE_ARCHIVED, UNARCHIVE, TOGGLE_COMPLETED, ADD ABOUT, HELP, SETTINGS, EDIT, DELETE, ARCHIVE, TOGGLE_ARCHIVED, UNARCHIVE, TOGGLE_COMPLETED, ADD
} }

View File

@@ -18,7 +18,7 @@
*/ */
package org.isoron.uhabits.acceptance.steps package org.isoron.uhabits.acceptance.steps
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION
import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import junit.framework.Assert.assertFalse import junit.framework.Assert.assertFalse
@@ -50,21 +50,29 @@ object WidgetSteps {
private fun openWidgetScreen() { private fun openWidgetScreen() {
val h = BaseUserInterfaceTest.device.displayHeight val h = BaseUserInterfaceTest.device.displayHeight
val w = BaseUserInterfaceTest.device.displayWidth val w = BaseUserInterfaceTest.device.displayWidth
val listId = "com.android.launcher3:id/widgets_list_view" if (VERSION.SDK_INT <= 21) {
BaseUserInterfaceTest.device.pressHome() BaseUserInterfaceTest.device.pressHome()
BaseUserInterfaceTest.device.waitForIdle() BaseUserInterfaceTest.device.waitForIdle()
BaseUserInterfaceTest.device.drag(w / 2, h / 2, w / 2, h / 2, 8) BaseUserInterfaceTest.device.findObject(UiSelector().description("Apps")).click()
var button = BaseUserInterfaceTest.device.findObject(UiSelector().text("WIDGETS")) BaseUserInterfaceTest.device.findObject(UiSelector().description("Apps")).click()
if (!button.waitForExists(1000)) { BaseUserInterfaceTest.device.findObject(UiSelector().description("Widgets")).click()
button = BaseUserInterfaceTest.device.findObject(UiSelector().text("Widgets")) } else {
} val listId = "com.android.launcher3:id/widgets_list_view"
button.click() BaseUserInterfaceTest.device.pressHome()
if (SDK_INT >= 28) { BaseUserInterfaceTest.device.waitForIdle()
BaseUserInterfaceTest.device.drag(w / 2, h / 2, w / 2, h / 2, 8)
var button = BaseUserInterfaceTest.device.findObject(UiSelector().text("WIDGETS"))
if (!button.waitForExists(1000)) {
button = BaseUserInterfaceTest.device.findObject(UiSelector().text("Widgets"))
}
button.click()
if (VERSION.SDK_INT >= 28) {
UiScrollable(UiSelector().resourceId(listId))
.scrollForward()
}
UiScrollable(UiSelector().resourceId(listId)) UiScrollable(UiSelector().resourceId(listId))
.scrollForward() .scrollIntoView(UiSelector().text("Checkmark"))
} }
UiScrollable(UiSelector().resourceId(listId))
.scrollIntoView(UiSelector().text("Checkmark"))
} }
@Throws(Exception::class) @Throws(Exception::class)

View File

@@ -36,7 +36,6 @@ class EntryButtonViewTest : BaseViewTest() {
lateinit var view: CheckmarkButtonView lateinit var view: CheckmarkButtonView
var toggled = false var toggled = false
var edited = false
@Before @Before
override fun setUp() { override fun setUp() {
@@ -44,8 +43,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 }
} }
measureView(view, dpToPixels(48), dpToPixels(48)) measureView(view, dpToPixels(48), dpToPixels(48))
} }
@@ -72,28 +70,20 @@ class EntryButtonViewTest : BaseViewTest() {
fun testClick_withShortToggleDisabled() { fun testClick_withShortToggleDisabled() {
prefs.isShortToggleEnabled = false prefs.isShortToggleEnabled = false
view.performClick() view.performClick()
assertTrue(!toggled and edited) assertFalse(toggled)
} }
@Test @Test
fun testClick_withShortToggleEnabled() { fun testClick_withShortToggleEnabled() {
prefs.isShortToggleEnabled = true prefs.isShortToggleEnabled = true
view.performClick() view.performClick()
assertTrue(toggled and !edited) assertTrue(toggled)
} }
@Test @Test
fun testLongClick_withShortToggleDisabled() { fun testLongClick() {
prefs.isShortToggleEnabled = false
view.performLongClick() view.performLongClick()
assertTrue(toggled and !edited) assertTrue(toggled)
}
@Test
fun testLongClick_withShortToggleEnabled() {
prefs.isShortToggleEnabled = true
view.performLongClick()
assertTrue(!toggled and edited)
} }
private fun assertRendersCheckedExplicitly() { private fun assertRendersCheckedExplicitly() {

View File

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

View File

@@ -24,7 +24,6 @@ import androidx.test.filters.MediumTest
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
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.utils.PaletteUtils import org.isoron.uhabits.utils.PaletteUtils
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@@ -43,7 +42,6 @@ class NumberButtonViewTest : BaseViewTest() {
super.setUp() super.setUp()
view = component.getNumberButtonViewFactory().create().apply { view = component.getNumberButtonViewFactory().create().apply {
units = "steps" units = "steps"
targetType = NumericalHabitType.AT_LEAST
threshold = 100.0 threshold = 100.0
color = PaletteUtils.getAndroidTestColor(8) color = PaletteUtils.getAndroidTestColor(8)
onEdit = { edited = true } onEdit = { edited = true }
@@ -76,10 +74,10 @@ class NumberButtonViewTest : BaseViewTest() {
} }
@Test @Test
fun testRender_atMostAboveThreshold() { fun testRender_emptyUnits() {
view.value = 500.0 view.value = 500.0
view.targetType = NumericalHabitType.AT_MOST view.units = ""
assertRenders(view, "$PATH/render_at_most_above.png") assertRenders(view, "$PATH/render_unitless.png")
} }
@Test @Test
@@ -88,13 +86,6 @@ class NumberButtonViewTest : BaseViewTest() {
assertRenders(view, "$PATH/render_below.png") assertRenders(view, "$PATH/render_below.png")
} }
@Test
fun testRender_atMostBetweenThresholds() {
view.value = 110.0
view.targetType = NumericalHabitType.AT_MOST
assertRenders(view, "$PATH/render_at_most_between.png")
}
@Test @Test
fun testRender_zero() { fun testRender_zero() {
view.value = 0.0 view.value = 0.0
@@ -102,21 +93,15 @@ class NumberButtonViewTest : BaseViewTest() {
} }
@Test @Test
fun testRender_atMostBelowThreshold() { fun testClick_shortToggleDisabled() {
view.value = 0.0 prefs.isShortToggleEnabled = false
view.targetType = NumericalHabitType.AT_MOST view.performClick()
assertRenders(view, "$PATH/render_at_most_below.png") assertFalse(edited)
} }
@Test @Test
fun testRender_emptyUnits() { fun testClick_shortToggleEnabled() {
view.value = 500.0 prefs.isShortToggleEnabled = true
view.units = ""
assertRenders(view, "$PATH/render_unitless.png")
}
@Test
fun testClick() {
view.performClick() view.performClick()
assertTrue(edited) assertTrue(edited)
} }

View File

@@ -24,7 +24,6 @@ import androidx.test.filters.MediumTest
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
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.utils.PaletteUtils import org.isoron.uhabits.utils.PaletteUtils
import org.junit.After import org.junit.After
@@ -56,7 +55,6 @@ class NumberPanelViewTest : BaseViewTest() {
buttonCount = 4 buttonCount = 4
color = PaletteUtils.getAndroidTestColor(7) color = PaletteUtils.getAndroidTestColor(7)
units = "steps" units = "steps"
targetType = NumericalHabitType.AT_LEAST
threshold = 5000.0 threshold = 5000.0
} }
view.onAttachedToWindow() view.onAttachedToWindow()
@@ -76,7 +74,7 @@ class NumberPanelViewTest : BaseViewTest() {
@Test @Test
fun testEdit() { fun testEdit() {
val timestamps = mutableListOf<Timestamp>() val timestamps = mutableListOf<Timestamp>()
view.onEdit = { t -> timestamps.plusAssign(t) } view.onEdit = { timestamps.plusAssign(it) }
view.buttons[0].performLongClick() view.buttons[0].performLongClick()
view.buttons[2].performLongClick() view.buttons[2].performLongClick()
view.buttons[3].performLongClick() view.buttons[3].performLongClick()
@@ -87,7 +85,7 @@ class NumberPanelViewTest : BaseViewTest() {
fun testEdit_withOffset() { fun testEdit_withOffset() {
val timestamps = mutableListOf<Timestamp>() val timestamps = mutableListOf<Timestamp>()
view.dataOffset = 3 view.dataOffset = 3
view.onEdit = { t -> timestamps += t } view.onEdit = { timestamps += it }
view.buttons[0].performLongClick() view.buttons[0].performLongClick()
view.buttons[2].performLongClick() view.buttons[2].performLongClick()
view.buttons[3].performLongClick() view.buttons[3].performLongClick()

View File

@@ -53,6 +53,8 @@ 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),
unit = "",
targetValue = 0.0,
theme = LightTheme(), theme = LightTheme(),
) )
) )

View File

@@ -61,7 +61,7 @@ class PerformanceTest : BaseAndroidTest() {
val habit = fixtures.createEmptyHabit() val habit = fixtures.createEmptyHabit()
for (i in 0..4999) { for (i in 0..4999) {
val timestamp: Timestamp = Timestamp(i * DAY_LENGTH) val timestamp: Timestamp = Timestamp(i * DAY_LENGTH)
CreateRepetitionCommand(habitList, habit, timestamp, 1, "").run() CreateRepetitionCommand(habitList, habit, timestamp, 1).run()
} }
db.setTransactionSuccessful() db.setTransactionSuccessful()
db.endTransaction() db.endTransaction()

View File

@@ -21,12 +21,9 @@ package org.isoron.uhabits.regression
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import org.isoron.uhabits.BaseUserInterfaceTest import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.acceptance.steps.CommonSteps
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.EDIT_HABIT import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.EDIT_HABIT
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.LIST_HABITS import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.LIST_HABITS
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.SELECT_HABIT_TYPE import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.SELECT_HABIT_TYPE
import org.isoron.uhabits.acceptance.steps.CommonSteps.changeFrequencyToDaily
import org.isoron.uhabits.acceptance.steps.CommonSteps.changeFrequencyToMonthly
import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.createHabit import org.isoron.uhabits.acceptance.steps.CommonSteps.createHabit
import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
@@ -40,12 +37,9 @@ import org.isoron.uhabits.acceptance.steps.EditHabitSteps.clickSave
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.typeName import org.isoron.uhabits.acceptance.steps.EditHabitSteps.typeName
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.ADD import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.ADD
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.DELETE import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.DELETE
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.changeSort
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.longPressCheckmarks import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.longPressCheckmarks
import org.isoron.uhabits.core.models.Entry.Companion.NO
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.junit.Test import org.junit.Test
@@ -89,37 +83,4 @@ class ListHabitsRegressionTest : BaseUserInterfaceTest() {
offsetHeaders() offsetHeaders()
verifyDisplaysCheckmarks("Wake up early", listOf(UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN)) verifyDisplaysCheckmarks("Wake up early", listOf(UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN))
} }
/**
* https://github.com/iSoron/uhabits/issues/1131
*/
@Test
@Throws(Exception::class)
fun should_refresh_sort_after_habit_edit() {
launchApp()
verifyShowsScreen(LIST_HABITS)
changeSort("By score")
changeSort("By status")
longPressCheckmarks("Meditate", count = 1)
changeFrequencyToMonthly("Read books")
longPressCheckmarks("Read books", count = 2)
longPressCheckmarks("Read books", count = 1)
verifyDisplaysCheckmarks("Meditate", listOf(YES_AUTO, YES_MANUAL, YES_AUTO, YES_MANUAL))
CommonSteps.verifyDisplaysTextInSequence(
"Wake up early",
"Read books",
"Meditate",
"Track time"
)
changeFrequencyToDaily("Meditate")
verifyDisplaysCheckmarks("Meditate", listOf(NO, YES_MANUAL, UNKNOWN, YES_MANUAL))
CommonSteps.verifyDisplaysTextInSequence(
"Wake up early",
"Meditate",
"Read books",
"Track time",
)
}
} }

View File

@@ -0,0 +1,154 @@
/*
* 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.sync
import androidx.test.filters.MediumTest
import com.fasterxml.jackson.databind.ObjectMapper
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.MockRequestHandleScope
import io.ktor.client.engine.mock.respond
import io.ktor.client.engine.mock.respondError
import io.ktor.client.engine.mock.respondOk
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.request.HttpRequestData
import io.ktor.client.request.HttpResponseData
import io.ktor.http.HttpStatusCode
import io.ktor.http.fullPath
import io.ktor.http.headersOf
import kotlinx.coroutines.runBlocking
import org.isoron.uhabits.BaseAndroidTest
import org.isoron.uhabits.core.sync.AbstractSyncServer
import org.isoron.uhabits.core.sync.GetDataVersionResponse
import org.isoron.uhabits.core.sync.KeyNotFoundException
import org.isoron.uhabits.core.sync.RegisterReponse
import org.isoron.uhabits.core.sync.ServiceUnavailable
import org.isoron.uhabits.core.sync.SyncData
import org.junit.Test
@MediumTest
class RemoteSyncServerTest : BaseAndroidTest() {
private val mapper = ObjectMapper()
val data = SyncData(1, "Hello world")
@Test
fun when_register_succeeds_should_return_key() = runBlocking {
val server = server("/register") {
respondWithJson(RegisterReponse("ABCDEF"))
}
assertEquals("ABCDEF", server.register())
}
@Test(expected = ServiceUnavailable::class)
fun when_register_fails_should_raise_correct_exception() = runBlocking {
val server = server("/register") {
respondError(HttpStatusCode.ServiceUnavailable)
}
server.register()
return@runBlocking
}
@Test
fun when_get_data_version_succeeds_should_return_version() = runBlocking {
server("/db/ABC/version") {
respondWithJson(GetDataVersionResponse(5))
}.apply {
assertEquals(5, getDataVersion("ABC"))
}
return@runBlocking
}
@Test(expected = ServiceUnavailable::class)
fun when_get_data_version_with_server_error_should_raise_exception() = runBlocking {
server("/db/ABC/version") {
respondError(HttpStatusCode.InternalServerError)
}.apply {
getDataVersion("ABC")
}
return@runBlocking
}
@Test(expected = KeyNotFoundException::class)
fun when_get_data_version_with_invalid_key_should_raise_exception() = runBlocking {
server("/db/ABC/version") {
respondError(HttpStatusCode.NotFound)
}.apply {
getDataVersion("ABC")
}
return@runBlocking
}
@Test
fun when_get_data_succeeds_should_return_data() = runBlocking {
server("/db/ABC") {
respondWithJson(data)
}.apply {
assertEquals(data, getData("ABC"))
}
return@runBlocking
}
@Test(expected = KeyNotFoundException::class)
fun when_get_data_with_invalid_key_should_raise_exception() = runBlocking {
server("/db/ABC") {
respondError(HttpStatusCode.NotFound)
}.apply {
getData("ABC")
}
return@runBlocking
}
@Test
fun when_put_succeeds_should_not_raise_exceptions() = runBlocking {
server("/db/ABC") {
respondOk()
}.apply {
put("ABC", data)
}
return@runBlocking
}
private fun server(
expectedPath: String,
action: MockRequestHandleScope.(HttpRequestData) -> HttpResponseData
): AbstractSyncServer {
return RemoteSyncServer(
httpClient = HttpClient(MockEngine) {
install(JsonFeature)
engine {
addHandler { request ->
when (request.url.fullPath) {
expectedPath -> action(request)
else -> error("unexpected call: ${request.url.fullPath}")
}
}
}
},
preferences = prefs
)
}
private fun MockRequestHandleScope.respondWithJson(content: Any) =
respond(
mapper.writeValueAsBytes(content),
headers = headersOf("Content-Type" to listOf("application/json"))
)
}

View File

@@ -24,8 +24,8 @@ import android.widget.FrameLayout
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 org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.`is` import org.hamcrest.CoreMatchers.`is`
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
import org.isoron.uhabits.R import org.isoron.uhabits.R

View File

@@ -17,12 +17,13 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>. ~ with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
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.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" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".HabitsApplication" android:name=".HabitsApplication"
@@ -41,6 +42,14 @@
android:value=".activities.habits.list.ListHabitsActivity" /> android:value=".activities.habits.list.ListHabitsActivity" />
</activity> </activity>
<activity
android:name=".activities.sync.SyncActivity"
android:exported="true">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.settings.SettingsActivity" />
</activity>
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw" /> android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw" />
@@ -49,11 +58,21 @@
android:name=".activities.habits.list.ListHabitsActivity" android:name=".activities.habits.list.ListHabitsActivity"
android:exported="true" android:exported="true"
android:label="@string/main_activity_title" android:label="@string/main_activity_title"
android:launchMode="singleTop" /> android:launchMode="singleTop">
<tools:validation testUrl="https://loophabits.org/sync/123" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="loophabits.org"
android:pathPrefix="/sync" />
</intent-filter>
</activity>
<activity-alias <activity-alias
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"
android:label="@string/main_activity_title" android:label="@string/main_activity_title"
android:launchMode="singleTop" android:launchMode="singleTop"
android:targetActivity=".activities.habits.list.ListHabitsActivity"> android:targetActivity=".activities.habits.list.ListHabitsActivity">
@@ -86,7 +105,6 @@
<activity <activity
android:name=".widgets.activities.HabitPickerDialog" android:name=".widgets.activities.HabitPickerDialog"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.Dialog"> android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
@@ -95,7 +113,6 @@
<activity <activity
android:name=".widgets.activities.BooleanHabitPickerDialog" android:name=".widgets.activities.BooleanHabitPickerDialog"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.Dialog"> android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
@@ -104,7 +121,6 @@
<activity <activity
android:name=".widgets.activities.NumericalHabitPickerDialog" android:name=".widgets.activities.NumericalHabitPickerDialog"
android:exported="true"
android:theme="@style/Theme.AppCompat.Light.Dialog"> android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
@@ -121,11 +137,10 @@
<activity <activity
android:name=".widgets.activities.NumericalCheckmarkWidgetActivity" android:name=".widgets.activities.NumericalCheckmarkWidgetActivity"
android:excludeFromRecents="true"
android:exported="true"
android:label="NumericalCheckmarkWidget" android:label="NumericalCheckmarkWidget"
android:noHistory="true" android:noHistory="true"
android:theme="@style/Theme.Transparent"> android:excludeFromRecents="true"
android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter> <intent-filter>
<action android:name="org.isoron.uhabits.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY" /> <action android:name="org.isoron.uhabits.ACTION_SHOW_NUMERICAL_VALUE_ACTIVITY" />
</intent-filter> </intent-filter>
@@ -133,14 +148,13 @@
<activity <activity
android:name=".notifications.SnoozeDelayPickerActivity" android:name=".notifications.SnoozeDelayPickerActivity"
android:taskAffinity=""
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:taskAffinity=""
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<receiver <receiver
android:name=".widgets.CheckmarkWidgetProvider" android:name=".widgets.CheckmarkWidgetProvider"
android:exported="true"
android:label="@string/checkmark"> android:label="@string/checkmark">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -158,7 +172,6 @@
<receiver <receiver
android:name=".widgets.HistoryWidgetProvider" android:name=".widgets.HistoryWidgetProvider"
android:exported="true"
android:label="@string/history"> android:label="@string/history">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -171,7 +184,6 @@
<receiver <receiver
android:name=".widgets.ScoreWidgetProvider" android:name=".widgets.ScoreWidgetProvider"
android:exported="true"
android:label="@string/score"> android:label="@string/score">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -184,7 +196,6 @@
<receiver <receiver
android:name=".widgets.StreakWidgetProvider" android:name=".widgets.StreakWidgetProvider"
android:exported="true"
android:label="@string/streaks"> android:label="@string/streaks">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -197,7 +208,6 @@
<receiver <receiver
android:name=".widgets.FrequencyWidgetProvider" android:name=".widgets.FrequencyWidgetProvider"
android:exported="true"
android:label="@string/frequency"> android:label="@string/frequency">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -210,7 +220,6 @@
<receiver <receiver
android:name=".widgets.TargetWidgetProvider" android:name=".widgets.TargetWidgetProvider"
android:exported="true"
android:label="@string/target"> android:label="@string/target">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -221,17 +230,13 @@
android:resource="@xml/widget_target_info" /> android:resource="@xml/widget_target_info" />
</receiver> </receiver>
<receiver <receiver android:name=".receivers.ReminderReceiver">
android:name=".receivers.ReminderReceiver"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".receivers.WidgetReceiver" <receiver android:name=".receivers.WidgetReceiver">
android:exported="true"
android:permission="false">
<intent-filter> <intent-filter>
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<action android:name="org.isoron.uhabits.ACTION_SET_NUMERICAL_VALUE" /> <action android:name="org.isoron.uhabits.ACTION_SET_NUMERICAL_VALUE" />
@@ -282,7 +287,7 @@
<!-- Locale/Tasker --> <!-- Locale/Tasker -->
<receiver <receiver
android:name=".automation.FireSettingReceiver" android:name=".automation.FireSettingReceiver"
android:exported="false"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" /> <action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
</intent-filter> </intent-filter>

View File

@@ -49,12 +49,23 @@ class AndroidDataView(
override fun onShowPress(e: MotionEvent?) = Unit override fun onShowPress(e: MotionEvent?) = Unit
override fun onSingleTapUp(e: MotionEvent?): Boolean { override fun onSingleTapUp(e: MotionEvent?): Boolean {
return handleClick(e, true) val x: Float
val y: Float
try {
val pointerId = e!!.getPointerId(0)
x = e.getX(pointerId)
y = e.getY(pointerId)
} catch (ex: RuntimeException) {
// Android often throws IllegalArgumentException here. Apparently,
// the pointer id may become invalid shortly after calling
// e.getPointerId.
return false
}
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
return true
} }
override fun onLongPress(e: MotionEvent?) { override fun onLongPress(e: MotionEvent?) = Unit
handleClick(e)
}
override fun onScroll( override fun onScroll(
e1: MotionEvent?, e1: MotionEvent?,
@@ -126,22 +137,4 @@ class AndroidDataView(
} }
} }
} }
private fun handleClick(e: MotionEvent?, isSingleTap: Boolean = false): Boolean {
val x: Float
val y: Float
try {
val pointerId = e!!.getPointerId(0)
x = e.getX(pointerId)
y = e.getY(pointerId)
} catch (ex: RuntimeException) {
// Android often throws IllegalArgumentException here. Apparently,
// the pointer id may become invalid shortly after calling
// e.getPointerId.
return false
}
if (isSingleTap) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
else view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
return true
}
} }

View File

@@ -1,127 +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.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import org.isoron.uhabits.R
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.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.preferences.Preferences
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.utils.dimBehind
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.sres
const val POPUP_WIDTH = 4 * 48f + 16f
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 = { _, _ -> }
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() {
arrayOf(view.yesBtn, view.skipBtn).forEach {
it.setTextColor(color)
}
arrayOf(view.noBtn, view.unknownBtn).forEach {
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
}
}
private fun initTypefaces() {
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
it.typeface = getFontAwesome(context)
}
}
private fun hideDisabledButtons() {
if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE
if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE
}
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.window?.apply {
setLayout(
view.root.dp(POPUP_WIDTH).toInt(),
view.root.dp(POPUP_HEIGHT).toInt()
)
setBackgroundDrawableResource(android.R.color.transparent)
}
fun onClick(v: Int) {
this.value = v
save()
}
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
view.noBtn.setOnClickListener { onClick(NO) }
view.skipBtn.setOnClickListener { onClick(SKIP) }
view.unknownBtn.setOnClickListener { onClick(UNKNOWN) }
dialog.setCanceledOnTouchOutside(true)
dialog.dimBehind()
dialog.show()
}
fun save() {
onToggle(value, view.notes.text.toString().trim())
dialog.dismiss()
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.content.Context
import android.content.DialogInterface
import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.inject.ActivityContext
class ConfirmSyncKeyDialog(
@ActivityContext context: Context,
callback: OnConfirmedCallback
) : AlertDialog(context) {
init {
setTitle(R.string.device_sync)
val res = context.resources
setMessage(res.getString(R.string.sync_confirm))
setButton(
BUTTON_POSITIVE,
res.getString(R.string.yes)
) { dialog: DialogInterface?, which: Int -> callback.onConfirmed() }
setButton(
BUTTON_NEGATIVE,
res.getString(R.string.no)
) { dialog: DialogInterface?, which: Int -> }
}
}

View File

@@ -21,6 +21,7 @@ package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
@@ -43,7 +44,7 @@ class FrequencyPickerDialog(
constructor() : this(1, 1) constructor() : this(1, 1)
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val inflater = LayoutInflater.from(requireActivity()) val inflater = LayoutInflater.from(activity!!)
contentView = inflater.inflate(R.layout.frequency_picker_dialog, null) contentView = inflater.inflate(R.layout.frequency_picker_dialog, null)
addBeforeAfterText( addBeforeAfterText(
@@ -61,19 +62,15 @@ class FrequencyPickerDialog(
contentView.xTimesPerMonthContainer, contentView.xTimesPerMonthContainer,
) )
addBeforeAfterText(
this.getString(R.string.x_times_per_y_days),
contentView.xTimesPerYDaysContainer,
)
contentView.everyDayRadioButton.setOnClickListener { contentView.everyDayRadioButton.setOnClickListener {
check(contentView.everyDayRadioButton) check(contentView.everyDayRadioButton)
unfocusAll()
} }
contentView.everyXDaysRadioButton.setOnClickListener { contentView.everyXDaysRadioButton.setOnClickListener {
check(contentView.everyXDaysRadioButton) check(contentView.everyXDaysRadioButton)
val everyXDaysTextView = contentView.everyXDaysTextView val everyXDaysTextView = contentView.everyXDaysTextView
selectInputField(everyXDaysTextView) focus(everyXDaysTextView)
} }
contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus -> contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
@@ -82,7 +79,7 @@ class FrequencyPickerDialog(
contentView.xTimesPerWeekRadioButton.setOnClickListener { contentView.xTimesPerWeekRadioButton.setOnClickListener {
check(contentView.xTimesPerWeekRadioButton) check(contentView.xTimesPerWeekRadioButton)
selectInputField(contentView.xTimesPerWeekTextView) focus(contentView.xTimesPerWeekTextView)
} }
contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus -> contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
@@ -91,27 +88,14 @@ class FrequencyPickerDialog(
contentView.xTimesPerMonthRadioButton.setOnClickListener { contentView.xTimesPerMonthRadioButton.setOnClickListener {
check(contentView.xTimesPerMonthRadioButton) check(contentView.xTimesPerMonthRadioButton)
selectInputField(contentView.xTimesPerMonthTextView) focus(contentView.xTimesPerMonthTextView)
} }
contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus -> contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) check(contentView.xTimesPerMonthRadioButton) if (hasFocus) check(contentView.xTimesPerMonthRadioButton)
} }
contentView.xTimesPerYDaysRadioButton.setOnClickListener { return AlertDialog.Builder(activity!!)
check(contentView.xTimesPerYDaysRadioButton)
selectInputField(contentView.xTimesPerYDaysXTextView)
}
contentView.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
}
contentView.xTimesPerYDaysYTextView.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
}
return AlertDialog.Builder(requireActivity())
.setView(contentView) .setView(contentView)
.setPositiveButton(R.string.save) { _, _ -> onSaveClicked() } .setPositiveButton(R.string.save) { _, _ -> onSaveClicked() }
.create() .create()
@@ -122,11 +106,12 @@ class FrequencyPickerDialog(
container: LinearLayout container: LinearLayout
) { ) {
val parts = str.split("%d") val parts = str.split("%d")
for (i in parts.indices) { container.addView(
container.addView( TextView(activity).apply { text = parts[0].trim() }, 1,
TextView(activity).apply { text = parts[i].trim() }, 2 * i + 1, )
) container.addView(
} TextView(activity).apply { text = parts[1].trim() }, 3,
)
} }
private fun onSaveClicked() { private fun onSaveClicked() {
@@ -147,12 +132,6 @@ class FrequencyPickerDialog(
denominator = 7 denominator = 7
} }
} }
contentView.xTimesPerYDaysRadioButton.isChecked -> {
if (contentView.xTimesPerYDaysXTextView.text.isNotEmpty() && contentView.xTimesPerYDaysYTextView.text.isNotEmpty()) {
numerator = Integer.parseInt(contentView.xTimesPerYDaysXTextView.text.toString())
denominator = Integer.parseInt(contentView.xTimesPerYDaysYTextView.text.toString())
}
}
else -> { else -> {
if (contentView.xTimesPerMonthTextView.text.isNotEmpty()) { if (contentView.xTimesPerMonthTextView.text.isNotEmpty()) {
numerator = Integer.parseInt(contentView.xTimesPerMonthTextView.text.toString()) numerator = Integer.parseInt(contentView.xTimesPerMonthTextView.text.toString())
@@ -168,10 +147,10 @@ class FrequencyPickerDialog(
dismiss() dismiss()
} }
private fun check(view: RadioButton) { private fun check(view: RadioButton?) {
uncheckAll() uncheckAll()
view.isChecked = true view?.isChecked = true
view.requestFocus() view?.requestFocus()
} }
override fun onResume() { override fun onResume() {
@@ -184,7 +163,7 @@ class FrequencyPickerDialog(
if (freqDenominator == 30 || freqDenominator == 31) { if (freqDenominator == 30 || freqDenominator == 31) {
contentView.xTimesPerMonthRadioButton.isChecked = true contentView.xTimesPerMonthRadioButton.isChecked = true
contentView.xTimesPerMonthTextView.setText(freqNumerator.toString()) contentView.xTimesPerMonthTextView.setText(freqNumerator.toString())
selectInputField(contentView.xTimesPerMonthTextView) focus(contentView.xTimesPerMonthTextView)
} else { } else {
if (freqNumerator == 1) { if (freqNumerator == 1) {
if (freqDenominator == 1) { if (freqDenominator == 1) {
@@ -192,23 +171,23 @@ class FrequencyPickerDialog(
} else { } else {
contentView.everyXDaysRadioButton.isChecked = true contentView.everyXDaysRadioButton.isChecked = true
contentView.everyXDaysTextView.setText(freqDenominator.toString()) contentView.everyXDaysTextView.setText(freqDenominator.toString())
selectInputField(contentView.everyXDaysTextView) focus(contentView.everyXDaysTextView)
} }
} else { } else {
if (freqDenominator == 7) { if (freqDenominator == 7) {
contentView.xTimesPerWeekRadioButton.isChecked = true contentView.xTimesPerWeekRadioButton.isChecked = true
contentView.xTimesPerWeekTextView.setText(freqNumerator.toString()) contentView.xTimesPerWeekTextView.setText(freqNumerator.toString())
selectInputField(contentView.xTimesPerWeekTextView) focus(contentView.xTimesPerWeekTextView)
} else { } else {
contentView.xTimesPerYDaysRadioButton.isChecked = true Log.w("FrequencyPickerDialog", "Unknown frequency: $freqNumerator/$freqDenominator")
contentView.xTimesPerYDaysXTextView.setText(freqNumerator.toString()) contentView.everyDayRadioButton.isChecked = true
contentView.xTimesPerYDaysYTextView.setText(freqDenominator.toString())
} }
} }
} }
} }
private fun selectInputField(view: EditText) { private fun focus(view: EditText) {
view.requestFocus()
view.setSelection(view.text.length) view.setSelection(view.text.length)
} }
@@ -217,6 +196,11 @@ class FrequencyPickerDialog(
contentView.everyXDaysRadioButton.isChecked = false contentView.everyXDaysRadioButton.isChecked = false
contentView.xTimesPerWeekRadioButton.isChecked = false contentView.xTimesPerWeekRadioButton.isChecked = false
contentView.xTimesPerMonthRadioButton.isChecked = false contentView.xTimesPerMonthRadioButton.isChecked = false
contentView.xTimesPerYDaysRadioButton.isChecked = false }
private fun unfocusAll() {
contentView.everyXDaysTextView.clearFocus()
contentView.xTimesPerWeekTextView.clearFocus()
contentView.xTimesPerMonthTextView.clearFocus()
} }
} }

View File

@@ -43,7 +43,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
private lateinit var commandRunner: CommandRunner private lateinit var commandRunner: CommandRunner
private lateinit var habit: Habit private lateinit var habit: Habit
private lateinit var preferences: Preferences private lateinit var preferences: Preferences
lateinit var dataView: AndroidDataView private lateinit var dataView: AndroidDataView
private var chart: HistoryChart? = null private var chart: HistoryChart? = null
private var onDateClickedListener: OnDateClickedListener? = null private var onDateClickedListener: OnDateClickedListener? = null
@@ -62,11 +62,9 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
firstWeekday = preferences.firstWeekday, firstWeekday = preferences.firstWeekday,
paletteColor = habit.color, paletteColor = habit.color,
series = emptyList(), series = emptyList(),
defaultSquare = HistoryChart.Square.OFF,
notesIndicators = emptyList(),
theme = themeSwitcher.currentTheme, theme = themeSwitcher.currentTheme,
today = DateUtils.getTodayWithOffset().toLocalDate(), today = DateUtils.getTodayWithOffset().toLocalDate(),
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {}, onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
padding = 10.0, padding = 10.0,
) )
dataView = AndroidDataView(context!!, null) dataView = AndroidDataView(context!!, null)
@@ -103,8 +101,6 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
theme = LightTheme() theme = LightTheme()
) )
chart?.series = model.series chart?.series = model.series
chart?.defaultSquare = model.defaultSquare
chart?.notesIndicators = model.notesIndicators
dataView.postInvalidate() dataView.postInvalidate()
} }

View File

@@ -0,0 +1,108 @@
/*
* 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.content.Context
import android.content.DialogInterface
import android.text.InputFilter
import android.view.LayoutInflater
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.NumberPicker
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils
import javax.inject.Inject
import kotlin.math.roundToLong
class NumberPickerFactory
@Inject constructor(
@ActivityContext private val context: Context
) {
fun create(
value: Double,
unit: String,
callback: ListHabitsBehavior.NumberPickerCallback
): AlertDialog {
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.number_picker_dialog, null)
val picker = view.findViewById<NumberPicker>(R.id.picker)
val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
val tvUnit = view.findViewById<TextView>(R.id.tvUnit)
val intValue = (value * 100).roundToLong().toInt()
picker.minValue = 0
picker.maxValue = Integer.MAX_VALUE / 100
picker.value = intValue / 100
picker.wrapSelectorWheel = false
picker2.minValue = 0
picker2.maxValue = 19
picker2.setFormatter { v -> String.format("%02d", 5 * v) }
picker2.value = intValue % 100 / 5
refreshInitialValue(picker2)
tvUnit.text = unit
val dialog = AlertDialog.Builder(context)
.setView(view)
.setTitle(R.string.change_value)
.setPositiveButton(android.R.string.ok) { _, _ ->
picker.clearFocus()
val v = picker.value + 0.05 * picker2.value
callback.onNumberPicked(v)
}
.setOnDismissListener {
callback.onNumberPickerDismissed()
}
.create()
dialog.setOnShowListener {
picker.getChildAt(0)?.requestFocus()
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
InterfaceUtils.setupEditorAction(
picker
) { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE)
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
false
}
return dialog
}
private fun refreshInitialValue(picker: NumberPicker) {
// Workaround for Android bug:
// https://code.google.com/p/android/issues/detail?id=35482
val f = NumberPicker::class.java.getDeclaredField("mInputText")
f.isAccessible = true
val inputText = f.get(picker) as EditText
inputText.filters = arrayOfNulls<InputFilter>(0)
}
}

View File

@@ -1,111 +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.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 = { _, _ -> }
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)
}
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.show()
}
fun save() {
val value = view.value.text.toString().toDoubleOrNull() ?: originalValue
val notes = view.notes.text.toString()
onToggle(value, notes)
dialog.dismiss()
}
}

View File

@@ -29,7 +29,6 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getShortWeekdayNames
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset
import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaySequence import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaySequence
import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaysInMonth
import org.isoron.uhabits.utils.ColorUtils.mixColors import org.isoron.uhabits.utils.ColorUtils.mixColors
import org.isoron.uhabits.utils.StyledResources import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.toSimpleDataFormat import org.isoron.uhabits.utils.toSimpleDataFormat
@@ -63,6 +62,7 @@ class FrequencyChart : ScrollableChart {
private var primaryColor = 0 private var primaryColor = 0
private var isBackgroundTransparent = false private var isBackgroundTransparent = false
private lateinit var frequency: HashMap<Timestamp, Array<Int>> private lateinit var frequency: HashMap<Timestamp, Array<Int>>
private var maxFreq = 0
private var firstWeekday = Calendar.SUNDAY private var firstWeekday = Calendar.SUNDAY
constructor(context: Context?) : super(context) { constructor(context: Context?) : super(context) {
@@ -82,6 +82,7 @@ class FrequencyChart : ScrollableChart {
fun setFrequency(frequency: java.util.HashMap<Timestamp, Array<Int>>) { fun setFrequency(frequency: java.util.HashMap<Timestamp, Array<Int>>) {
this.frequency = frequency this.frequency = frequency
maxFreq = getMaxFreq(frequency)
postInvalidate() postInvalidate()
} }
@@ -90,6 +91,15 @@ class FrequencyChart : ScrollableChart {
postInvalidate() postInvalidate()
} }
private fun getMaxFreq(frequency: HashMap<Timestamp, Array<Int>>): Int {
var maxValue = 1
for (values in frequency.values) for (value in values) maxValue = max(
value,
maxValue
)
return maxValue
}
fun setIsBackgroundTransparent(isBackgroundTransparent: Boolean) { fun setIsBackgroundTransparent(isBackgroundTransparent: Boolean) {
this.isBackgroundTransparent = isBackgroundTransparent this.isBackgroundTransparent = isBackgroundTransparent
initColors() initColors()
@@ -156,7 +166,6 @@ class FrequencyChart : ScrollableChart {
private fun drawColumn(canvas: Canvas, rect: RectF?, date: GregorianCalendar) { private fun drawColumn(canvas: Canvas, rect: RectF?, date: GregorianCalendar) {
val values = frequency[Timestamp(date)] val values = frequency[Timestamp(date)]
val weekDaysInMonth = getWeekdaysInMonth(Timestamp(date))
val rowHeight = rect!!.height() / 8.0f val rowHeight = rect!!.height() / 8.0f
prevRect!!.set(rect) prevRect!!.set(rect)
val localeWeekdayList: Array<Int> = getWeekdaySequence(firstWeekday) val localeWeekdayList: Array<Int> = getWeekdaySequence(firstWeekday)
@@ -164,8 +173,7 @@ 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])
drawMarker(canvas, rect, values[i], weekDaysInMonth[i])
rect.offset(0f, rowHeight) rect.offset(0f, rowHeight)
} }
drawFooter(canvas, rect, date) drawFooter(canvas, rect, date)
@@ -213,16 +221,12 @@ class FrequencyChart : ScrollableChart {
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!) canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!)
} }
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?, frequency: Int) { private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) {
// value can be negative when the entry is skipped
val valueCopy = value?.let { max(0, it) }
val padding = rect!!.height() * 0.2f val padding = rect!!.height() * 0.2f
// maximal allowed mark radius // maximal allowed mark radius
val maxRadius = (rect.height() - 2 * padding) / 2.0f val maxRadius = (rect.height() - 2 * padding) / 2.0f
// the real mark radius is scaled down by a factor depending on the maximal frequency // the real mark radius is scaled down by a factor depending on the maximal frequency
val scale = 1.0f / maxFreq * value!!
val scale = 1.0f / frequency * valueCopy!!
val radius = maxRadius * scale val radius = maxRadius * scale
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt()) val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
pGraph!!.color = colors[colorIndex] pGraph!!.color = colors[colorIndex]
@@ -285,5 +289,6 @@ class FrequencyChart : ScrollableChart {
frequency[Timestamp(date)] = values frequency[Timestamp(date)] = values
date.add(Calendar.MONTH, -1) date.add(Calendar.MONTH, -1)
} }
maxFreq = getMaxFreq(frequency)
} }
} }

View File

@@ -69,7 +69,7 @@ fun formatFrequency(freqNum: Int, freqDen: Int, resources: Resources) = when {
freqNum == 1 && freqDen == 7 -> resources.getString(R.string.every_week) freqNum == 1 && freqDen == 7 -> resources.getString(R.string.every_week)
freqNum == 1 && freqDen > 1 -> resources.getString(R.string.every_x_days, freqDen) freqNum == 1 && freqDen > 1 -> resources.getString(R.string.every_x_days, freqDen)
freqDen == 7 -> resources.getString(R.string.x_times_per_week, freqNum) freqDen == 7 -> resources.getString(R.string.x_times_per_week, freqNum)
else -> resources.getString(R.string.x_times_per_y_days, freqNum, freqDen) else -> "$freqNum/$freqDen"
} }
class EditHabitActivity : AppCompatActivity() { class EditHabitActivity : AppCompatActivity() {
@@ -88,7 +88,6 @@ class EditHabitActivity : AppCompatActivity() {
var reminderHour = -1 var reminderHour = -1
var reminderMin = -1 var reminderMin = -1
var reminderDays: WeekdayList = WeekdayList.EVERY_DAY var reminderDays: WeekdayList = WeekdayList.EVERY_DAY
var targetType = NumericalHabitType.AT_LEAST
override fun onCreate(state: Bundle?) { override fun onCreate(state: Bundle?) {
super.onCreate(state) super.onCreate(state)
@@ -108,7 +107,6 @@ class EditHabitActivity : AppCompatActivity() {
color = habit.color color = habit.color
freqNum = habit.frequency.numerator freqNum = habit.frequency.numerator
freqDen = habit.frequency.denominator freqDen = habit.frequency.denominator
targetType = habit.targetType
habit.reminder?.let { habit.reminder?.let {
reminderHour = it.hour reminderHour = it.hour
reminderMin = it.minute reminderMin = it.minute
@@ -140,7 +138,6 @@ class EditHabitActivity : AppCompatActivity() {
HabitType.YES_NO -> { HabitType.YES_NO -> {
binding.unitOuterBox.visibility = View.GONE binding.unitOuterBox.visibility = View.GONE
binding.targetOuterBox.visibility = View.GONE binding.targetOuterBox.visibility = View.GONE
binding.targetTypeOuterBox.visibility = View.GONE
} }
HabitType.NUMERICAL -> { HabitType.NUMERICAL -> {
binding.nameInput.hint = getString(R.string.measurable_short_example) binding.nameInput.hint = getString(R.string.measurable_short_example)
@@ -175,23 +172,6 @@ class EditHabitActivity : AppCompatActivity() {
dialog.show(supportFragmentManager, "frequencyPicker") dialog.show(supportFragmentManager, "frequencyPicker")
} }
populateTargetType()
binding.targetTypePicker.setOnClickListener {
val builder = AlertDialog.Builder(this)
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
arrayAdapter.add(getString(R.string.target_type_at_least))
arrayAdapter.add(getString(R.string.target_type_at_most))
builder.setAdapter(arrayAdapter) { dialog, which ->
targetType = when (which) {
0 -> NumericalHabitType.AT_LEAST
else -> NumericalHabitType.AT_MOST
}
populateTargetType()
dialog.dismiss()
}
builder.show()
}
binding.numericalFrequencyPicker.setOnClickListener { binding.numericalFrequencyPicker.setOnClickListener {
val builder = AlertDialog.Builder(this) val builder = AlertDialog.Builder(this)
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item) val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
@@ -282,7 +262,7 @@ class EditHabitActivity : AppCompatActivity() {
habit.frequency = Frequency(freqNum, freqDen) habit.frequency = Frequency(freqNum, freqDen)
if (habitType == HabitType.NUMERICAL) { if (habitType == HabitType.NUMERICAL) {
habit.targetValue = targetInput.text.toString().toDouble() habit.targetValue = targetInput.text.toString().toDouble()
habit.targetType = targetType habit.targetType = NumericalHabitType.AT_LEAST
habit.unit = unitInput.text.trim().toString() habit.unit = unitInput.text.trim().toString()
} }
habit.type = habitType habit.type = habitType
@@ -344,13 +324,6 @@ class EditHabitActivity : AppCompatActivity() {
} }
} }
private fun populateTargetType() {
binding.targetTypePicker.text = when (targetType) {
NumericalHabitType.AT_MOST -> getString(R.string.target_type_at_most)
else -> getString(R.string.target_type_at_least)
}
}
private fun updateColors() { private fun updateColors() {
androidColor = themeSwitcher.currentTheme.color(color).toInt() androidColor = themeSwitcher.currentTheme.color(color).toInt()
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor) binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)

View File

@@ -26,10 +26,12 @@ import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.isoron.uhabits.BaseExceptionHandler import org.isoron.uhabits.BaseExceptionHandler
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.sync.SyncManager
import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_DARK import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_DARK
import org.isoron.uhabits.core.utils.MidnightTimer import org.isoron.uhabits.core.utils.MidnightTimer
@@ -47,13 +49,13 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
lateinit var screen: ListHabitsScreen lateinit var screen: ListHabitsScreen
lateinit var prefs: Preferences lateinit var prefs: Preferences
lateinit var midnightTimer: MidnightTimer lateinit var midnightTimer: MidnightTimer
lateinit var syncManager: SyncManager
private val scope = CoroutineScope(Dispatchers.Main) private val scope = CoroutineScope(Dispatchers.Main)
private lateinit var menu: ListHabitsMenu private lateinit var menu: ListHabitsMenu
override fun onQuestionMarksChanged() { override fun onQuestionMarksChanged() {
invalidateOptionsMenu() invalidateOptionsMenu()
menu.behavior.onPreferencesChanged()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -69,6 +71,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
prefs = appComponent.preferences prefs = appComponent.preferences
prefs.addListener(this) prefs.addListener(this)
syncManager = appComponent.syncManager
pureBlack = prefs.isPureBlackEnabled pureBlack = prefs.isPureBlackEnabled
midnightTimer = appComponent.midnightTimer midnightTimer = appComponent.midnightTimer
rootView = component.listHabitsRootView rootView = component.listHabitsRootView
@@ -83,8 +86,11 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
override fun onPause() { override fun onPause() {
midnightTimer.onPause() midnightTimer.onPause()
screen.onDetached() screen.onDettached()
adapter.cancelRefresh() adapter.cancelRefresh()
scope.launch {
syncManager.onPause()
}
super.onPause() super.onPause()
} }
@@ -93,6 +99,9 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
screen.onAttached() screen.onAttached()
rootView.postInvalidate() rootView.postInvalidate()
midnightTimer.onResume() midnightTimer.onResume()
scope.launch {
syncManager.onResume()
}
taskRunner.run { taskRunner.run {
AutoBackup(this@ListHabitsActivity).run() AutoBackup(this@ListHabitsActivity).run()
} }

View File

@@ -39,7 +39,7 @@ class ListHabitsMenu @Inject constructor(
@ActivityContext context: Context, @ActivityContext context: Context,
private val preferences: Preferences, private val preferences: Preferences,
private val themeSwitcher: ThemeSwitcher, private val themeSwitcher: ThemeSwitcher,
val behavior: ListHabitsMenuBehavior private val behavior: ListHabitsMenuBehavior
) { ) {
val activity = (context as AppCompatActivity) val activity = (context as AppCompatActivity)

View File

@@ -22,14 +22,14 @@ 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.util.Log
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.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
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.ConfirmSyncKeyDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
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
@@ -42,7 +42,6 @@ import org.isoron.uhabits.core.commands.EditHabitCommand
import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.ThemeSwitcher import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
@@ -54,6 +53,8 @@ import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.FILE_NOT_RECOGNIZED import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.FILE_NOT_RECOGNIZED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_FAILED import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_FAILED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_SUCCESSFUL import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_SUCCESSFUL
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.SYNC_ENABLED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.SYNC_KEY_ALREADY_INSTALLED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
@@ -63,7 +64,6 @@ import org.isoron.uhabits.tasks.ExportDBTaskFactory
import org.isoron.uhabits.tasks.ImportDataTask import org.isoron.uhabits.tasks.ImportDataTask
import org.isoron.uhabits.tasks.ImportDataTaskFactory import org.isoron.uhabits.tasks.ImportDataTaskFactory
import org.isoron.uhabits.utils.copyTo import org.isoron.uhabits.utils.copyTo
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.restartWithFade import org.isoron.uhabits.utils.restartWithFade
import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendEmailScreen import org.isoron.uhabits.utils.showSendEmailScreen
@@ -92,9 +92,8 @@ class ListHabitsScreen
private val exportDBFactory: ExportDBTaskFactory, private val exportDBFactory: ExportDBTaskFactory,
private val importTaskFactory: ImportDataTaskFactory, private val importTaskFactory: ImportDataTaskFactory,
private val colorPickerFactory: ColorPickerDialogFactory, private val colorPickerFactory: ColorPickerDialogFactory,
private val behavior: Lazy<ListHabitsBehavior>, private val numberPickerFactory: NumberPickerFactory,
private val preferences: Preferences, private val behavior: Lazy<ListHabitsBehavior>
private val rootView: Lazy<ListHabitsRootView>,
) : CommandRunner.Listener, ) : CommandRunner.Listener,
ListHabitsBehavior.Screen, ListHabitsBehavior.Screen,
ListHabitsMenuBehavior.Screen, ListHabitsMenuBehavior.Screen,
@@ -104,9 +103,17 @@ class ListHabitsScreen
fun onAttached() { fun onAttached() {
commandRunner.addListener(this) commandRunner.addListener(this)
if (activity.intent.action == "android.intent.action.VIEW") {
val uri = activity.intent.data!!.toString()
val parts = uri.replace(Regex("^.*sync/"), "").split("#")
val syncKey = parts[0]
val encKey = parts[1]
Log.i("ListHabitsScreen", "sync: $syncKey enc: $encKey")
behavior.get().onSyncKeyOffer(syncKey, encKey)
}
} }
fun onDetached() { fun onDettached() {
commandRunner.removeListener(this) commandRunner.removeListener(this)
} }
@@ -201,6 +208,8 @@ class ListHabitsScreen
DATABASE_REPAIRED -> R.string.database_repaired DATABASE_REPAIRED -> R.string.database_repaired
COULD_NOT_GENERATE_BUG_REPORT -> R.string.bug_report_failed COULD_NOT_GENERATE_BUG_REPORT -> R.string.bug_report_failed
FILE_NOT_RECOGNIZED -> R.string.file_not_recognized FILE_NOT_RECOGNIZED -> R.string.file_not_recognized
SYNC_ENABLED -> R.string.sync_enabled
SYNC_KEY_ALREADY_INSTALLED -> R.string.sync_key_already_installed
} }
) )
) )
@@ -227,42 +236,16 @@ class ListHabitsScreen
picker.show(activity.supportFragmentManager, "picker") picker.show(activity.supportFragmentManager, "picker")
} }
override fun showNumberPopup( override fun showNumberPicker(
value: Double, value: Double,
notes: String, unit: String,
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
) { ) {
val view = rootView.get() numberPickerFactory.create(value, unit, callback).show()
NumberPopup(
context = context,
prefs = preferences,
anchor = view,
notes = notes,
value = value,
).apply {
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
show()
}
} }
override fun showCheckmarkPopup( override fun showConfirmInstallSyncKey(callback: OnConfirmedCallback) {
selectedValue: Int, ConfirmSyncKeyDialog(activity, callback).show()
notes: String,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
val view = rootView.get()
CheckmarkPopup(
context = context,
prefs = preferences,
anchor = view,
color = view.currentTheme().color(color).toInt(),
notes = notes,
value = selectedValue,
).apply {
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
show()
}
} }
private fun getExecuteString(command: Command): String? { private fun getExecuteString(command: Command): String? {

View File

@@ -37,15 +37,13 @@ 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.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.drawNotesIndicator import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.sp import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sres 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,
@@ -73,42 +71,33 @@ class CheckmarkButtonView(
invalidate() invalidate()
} }
var notes = "" var onToggle: (Int) -> Unit = {}
set(value) {
field = value
invalidate()
}
var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> }
var onEdit: () -> Unit = { }
private var drawer = Drawer() private var drawer = Drawer()
init { init {
isFocusable = false
setOnClickListener(this) setOnClickListener(this)
setOnLongClickListener(this) setOnLongClickListener(this)
} }
fun performToggle(delay: Long) { fun performToggle() {
value = Entry.nextToggleValue( value = if (preferences.isSkipEnabled) {
value = value, Entry.nextToggleValueWithSkip(value)
isSkipEnabled = preferences.isSkipEnabled, } else {
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled Entry.nextToggleValueWithoutSkip(value)
) }
onToggle(value, notes, delay) onToggle(value)
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) performToggle()
else onEdit() else showMessage(resources.getString(R.string.long_press_to_toggle))
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
if (preferences.isShortToggleEnabled) onEdit() performToggle()
else performToggle(TOGGLE_DELAY_MILLIS)
return true return true
} }
@@ -156,11 +145,6 @@ class CheckmarkButtonView(
} }
else -> R.string.fa_check else -> R.string.fa_check
} }
paint.textSize = when {
id == R.string.fa_question -> sp(12.0f)
value == YES_AUTO -> sp(13.0f)
else -> sp(14.0f)
}
if (value == YES_AUTO) { if (value == YES_AUTO) {
paint.strokeWidth = 5f paint.strokeWidth = 5f
paint.style = Paint.Style.STROKE paint.style = Paint.Style.STROKE
@@ -169,6 +153,11 @@ class CheckmarkButtonView(
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
} }
paint.textSize = when (id) {
UNKNOWN -> dim(R.dimen.smallerTextSize)
else -> dim(R.dimen.smallTextSize)
}
val label = resources.getString(id) val label = resources.getString(id)
val em = paint.measureText("m") val em = paint.measureText("m")
@@ -181,8 +170,6 @@ class CheckmarkButtonView(
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
canvas.drawText(label, rect.centerX(), rect.centerY(), paint) canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
} }
drawNotesIndicator(canvas, color, em, notes)
} }
} }
} }

View File

@@ -54,19 +54,7 @@ class CheckmarkPanelView(
setupButtons() setupButtons()
} }
var notes = arrayOf<String>() var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
set(values) {
field = values
setupButtons()
}
var onToggle: (Timestamp, Int, String, Long) -> Unit = { _, _, _, _ -> }
set(value) {
field = value
setupButtons()
}
var onEdit: (Timestamp) -> Unit = { _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
@@ -84,13 +72,8 @@ class CheckmarkPanelView(
index + dataOffset < values.size -> values[index + dataOffset] index + dataOffset < values.size -> values[index + dataOffset]
else -> UNKNOWN else -> UNKNOWN
} }
button.notes = when {
index + dataOffset < notes.size -> notes[index + dataOffset]
else -> ""
}
button.color = color button.color = color
button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) } button.onToggle = { value -> onToggle(timestamp, value) }
button.onEdit = { onEdit(timestamp) }
} }
} }
} }

View File

@@ -124,9 +124,8 @@ class HabitCardListAdapter @Inject constructor(
val habit = cache.getHabitByPosition(position) val habit = cache.getHabitByPosition(position)
val score = cache.getScore(habit!!.id!!) val score = cache.getScore(habit!!.id!!)
val checkmarks = cache.getCheckmarks(habit.id!!) val checkmarks = cache.getCheckmarks(habit.id!!)
val notes = cache.getNotes(habit.id!!)
val selected = selected.contains(habit) val selected = selected.contains(habit)
listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected) listView!!.bindCardView(holder, habit, score, checkmarks, selected)
} }
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) { override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {

View File

@@ -87,7 +87,6 @@ class HabitCardListView(
habit: Habit, habit: Habit,
score: Double, score: Double,
checkmarks: IntArray, checkmarks: IntArray,
notes: Array<String>,
selected: Boolean selected: Boolean
): View { ): View {
val cardView = holder.itemView as HabitCardView val cardView = holder.itemView as HabitCardView
@@ -99,7 +98,6 @@ class HabitCardListView(
cardView.score = score cardView.score = score
cardView.unit = habit.unit cardView.unit = habit.unit
cardView.threshold = habit.targetValue / habit.frequency.denominator cardView.threshold = habit.targetValue / habit.frequency.denominator
cardView.notes = notes
val detector = GestureDetector(context, CardViewGestureDetector(holder)) val detector = GestureDetector(context, CardViewGestureDetector(holder))
cardView.setOnTouchListener { _, ev -> cardView.setOnTouchListener { _, ev ->

View File

@@ -21,8 +21,8 @@ package org.isoron.uhabits.activities.habits.list.views
import android.content.Context import android.content.Context
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.M
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.TextUtils import android.text.TextUtils
@@ -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,
@@ -122,22 +115,12 @@ class HabitCardView(
numberPanel.threshold = value numberPanel.threshold = value
} }
var notes
get() = checkmarkPanel.notes
set(values) {
checkmarkPanel.notes = values
numberPanel.notes = values
}
var checkmarkPanel: CheckmarkPanelView var checkmarkPanel: CheckmarkPanelView
private var numberPanel: NumberPanelView private var numberPanel: NumberPanelView
private var innerFrame: LinearLayout private var innerFrame: LinearLayout
private var label: TextView private var label: TextView
private var scoreRing: RingView private var scoreRing: RingView
private var currentToggleTaskId = 0
private var queuedToggles = mutableListOf<DelayedToggle>()
init { init {
scoreRing = RingView(context).apply { scoreRing = RingView(context).apply {
val thickness = dp(3f) val thickness = dp(3f)
@@ -154,22 +137,13 @@ class HabitCardView(
maxLines = 2 maxLines = 2
ellipsize = TextUtils.TruncateAt.END ellipsize = TextUtils.TruncateAt.END
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f) layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
if (SDK_INT >= Build.VERSION_CODES.Q) { if (SDK_INT >= M) breakStrategy = BREAK_STRATEGY_BALANCED
breakStrategy = BREAK_STRATEGY_BALANCED
}
} }
checkmarkPanel = checkmarkPanelFactory.create().apply { checkmarkPanel = checkmarkPanelFactory.create().apply {
onToggle = { timestamp, value, notes, delay -> onToggle = { timestamp, value ->
if (delay > 0) triggerRipple(timestamp)
habit?.let {
val taskId = queueToggle(it, timestamp, value, notes);
{ runPendingToggles(taskId) }.delay(delay)
}
}
onEdit = { timestamp ->
triggerRipple(timestamp) triggerRipple(timestamp)
habit?.let { behavior.onEdit(it, timestamp) } habit?.let { behavior.onToggle(it, timestamp, value) }
} }
} }
@@ -205,25 +179,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) }
@@ -281,7 +236,6 @@ class HabitCardView(
numberPanel.apply { numberPanel.apply {
color = c color = c
units = h.unit units = h.unit
targetType = h.targetType
threshold = h.targetValue threshold = h.targetValue
visibility = when (h.isNumerical) { visibility = when (h.isNumerical) {
true -> View.VISIBLE true -> View.VISIBLE
@@ -308,10 +262,4 @@ class HabitCardView(
} }
innerFrame.setBackgroundResource(background) innerFrame.setBackgroundResource(background)
} }
companion object {
fun (() -> Unit).delay(delayInMillis: Long) {
Handler(Looper.getMainLooper()).postDelayed(this, delayInMillis)
}
}
} }

View File

@@ -29,16 +29,13 @@ import android.view.View
import android.view.View.OnClickListener import android.view.View.OnClickListener
import android.view.View.OnLongClickListener import android.view.View.OnLongClickListener
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils.getDimension import org.isoron.uhabits.utils.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.dim import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.drawNotesIndicator
import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.showMessage
import java.text.DecimalFormat import java.text.DecimalFormat
import javax.inject.Inject import javax.inject.Inject
@@ -91,25 +88,13 @@ class NumberButtonView(
invalidate() invalidate()
} }
var targetType = AT_LEAST
set(value) {
field = value
invalidate()
}
var units = "" var units = ""
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
} }
var notes = ""
set(value) {
field = value
invalidate()
}
var onEdit: () -> Unit = { }
var onEdit: () -> Unit = {}
private var drawer: Drawer = Drawer(context) private var drawer: Drawer = Drawer(context)
init { init {
@@ -118,7 +103,8 @@ class NumberButtonView(
} }
override fun onClick(v: View) { override fun onClick(v: View) {
onEdit() if (preferences.isShortToggleEnabled) onEdit()
else showMessage(resources.getString(R.string.long_press_to_edit))
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
@@ -141,16 +127,11 @@ class NumberButtonView(
private val em: Float private val em: Float
private val rect: RectF = RectF() private val rect: RectF = RectF()
private val sr = StyledResources(context)
private val lowContrast: Int private val lowContrast: Int
private val mediumContrast: Int private val mediumContrast: Int
private val paint = TextPaint().apply {
typeface = getFontAwesome()
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private val pUnit: TextPaint = TextPaint().apply { private val pUnit: TextPaint = TextPaint().apply {
textSize = getDimension(context, R.dimen.smallerTextSize) textSize = getDimension(context, R.dimen.smallerTextSize)
typeface = NORMAL_TYPEFACE typeface = NORMAL_TYPEFACE
@@ -167,16 +148,15 @@ class NumberButtonView(
init { init {
em = pNumber.measureText("m") em = pNumber.measureText("m")
lowContrast = sres.getColor(R.attr.contrast40) lowContrast = sr.getColor(R.attr.contrast40)
mediumContrast = sres.getColor(R.attr.contrast60) mediumContrast = sr.getColor(R.attr.contrast60)
} }
fun draw(canvas: Canvas) { fun draw(canvas: Canvas) {
val activeColor = when { val activeColor = when {
value < 0.0 -> lowContrast value <= 0.0 -> lowContrast
(targetType == AT_LEAST) && (value >= threshold) -> color value < threshold -> mediumContrast
(targetType == AT_MOST) && (value <= threshold) -> color else -> color
else -> mediumContrast
} }
val label: String val label: String
@@ -184,11 +164,6 @@ class NumberButtonView(
val textSize: Float val textSize: Float
when { when {
value == Entry.SKIP.toDouble() / 1000 -> {
label = resources.getString(R.string.fa_skipped)
textSize = dim(R.dimen.smallTextSize)
typeface = getFontAwesome()
}
value >= 0 -> { value >= 0 -> {
label = value.toShortString() label = value.toShortString()
typeface = BOLD_TYPEFACE typeface = BOLD_TYPEFACE
@@ -221,8 +196,6 @@ class NumberButtonView(
rect.offset(0f, 1.3f * em) rect.offset(0f, 1.3f * em)
canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit) canvas.drawText(units, rect.centerX(), rect.centerY(), pUnit)
} }
drawNotesIndicator(canvas, color, em, notes)
} }
} }
} }

View File

@@ -20,7 +20,6 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.Context import android.content.Context
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
@@ -48,12 +47,6 @@ class NumberPanelView(
setupButtons() setupButtons()
} }
var targetType = NumericalHabitType.AT_LEAST
set(value) {
field = value
setupButtons()
}
var threshold = 0.0 var threshold = 0.0
set(value) { set(value) {
field = value field = value
@@ -72,13 +65,7 @@ class NumberPanelView(
setupButtons() setupButtons()
} }
var notes = arrayOf<String>() var onEdit: (Timestamp) -> Unit = {}
set(values) {
field = values
setupButtons()
}
var onEdit: (Timestamp) -> Unit = { _ -> }
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
@@ -96,12 +83,7 @@ class NumberPanelView(
index + dataOffset < values.size -> values[index + dataOffset] index + dataOffset < values.size -> values[index + dataOffset]
else -> 0.0 else -> 0.0
} }
button.notes = when {
index + dataOffset < notes.size -> notes[index + dataOffset]
else -> ""
}
button.color = color button.color = color
button.targetType = targetType
button.threshold = threshold button.threshold = threshold
button.units = units button.units = units
button.onEdit = { onEdit(timestamp) } button.onEdit = { onEdit(timestamp) }

View File

@@ -23,25 +23,21 @@ import android.os.Bundle
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.AndroidDirFinder import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.HabitsApplication 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.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.NumberPickerFactory
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
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
@@ -49,7 +45,6 @@ import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
import org.isoron.uhabits.core.ui.views.OnDateClickedListener 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.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
@@ -166,49 +161,12 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) window.decorView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
} }
override fun showNumberPopup( override fun showNumberPicker(
value: Double, value: Double,
notes: String, unit: String,
preferences: Preferences, callback: ListHabitsBehavior.NumberPickerCallback,
callback: ListHabitsBehavior.NumberPickerCallback
) { ) {
val anchor = getPopupAnchor() ?: return NumberPickerFactory(this@ShowHabitActivity).create(value, unit, callback).show()
NumberPopup(
context = this@ShowHabitActivity,
prefs = preferences,
notes = notes,
anchor = anchor,
value = value,
).apply {
onToggle = { v, n -> callback.onNumberPicked(v, n) }
show()
}
}
override fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
preferences: Preferences,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
val anchor = getPopupAnchor() ?: return
CheckmarkPopup(
context = this@ShowHabitActivity,
prefs = preferences,
notes = notes,
color = view.currentTheme().color(color).toInt(),
anchor = anchor,
value = selectedValue,
).apply {
onToggle = { v, n -> callback.onNotesSaved(v, n) }
show()
}
}
private fun getPopupAnchor(): View? {
val dialog = supportFragmentManager.findFragmentByTag("historyEditor") as HistoryEditorDialog?
return dialog?.dataView
} }
override fun showEditHabitScreen(habit: Habit) { override fun showEditHabitScreen(habit: Habit) {

View File

@@ -43,8 +43,6 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
theme = state.theme, theme = state.theme,
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()), dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
series = state.series, series = state.series,
defaultSquare = state.defaultSquare,
notesIndicators = state.notesIndicators,
firstWeekday = state.firstWeekday, firstWeekday = state.firstWeekday,
) )
binding.chart.postInvalidate() binding.chart.postInvalidate()

View File

@@ -28,7 +28,6 @@ import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.edit.formatFrequency import org.isoron.uhabits.activities.habits.edit.formatFrequency
import org.isoron.uhabits.activities.habits.list.views.toShortString import org.isoron.uhabits.activities.habits.list.views.toShortString
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
import org.isoron.uhabits.utils.InterfaceUtils import org.isoron.uhabits.utils.InterfaceUtils
@@ -66,12 +65,7 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
binding.questionLabel.visibility = View.VISIBLE binding.questionLabel.visibility = View.VISIBLE
binding.targetIcon.visibility = View.VISIBLE binding.targetIcon.visibility = View.VISIBLE
binding.targetText.visibility = View.VISIBLE binding.targetText.visibility = View.VISIBLE
if (state.isNumerical) { if (!state.isNumerical) {
binding.targetIcon.text = when (state.targetType) {
NumericalHabitType.AT_LEAST -> resources.getString(R.string.fa_arrow_circle_up)
else -> resources.getString(R.string.fa_arrow_circle_down)
}
} else {
binding.targetIcon.visibility = View.GONE binding.targetIcon.visibility = View.GONE
binding.targetText.visibility = View.GONE binding.targetText.visibility = View.GONE
} }

View File

@@ -21,9 +21,8 @@ package org.isoron.uhabits.activities.intro
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import com.github.paolorotolo.appintro.AppIntro2
import com.github.appintro.AppIntro2 import com.github.paolorotolo.appintro.AppIntroFragment
import com.github.appintro.AppIntroFragment
import org.isoron.uhabits.R import org.isoron.uhabits.R
/** /**
@@ -31,9 +30,7 @@ import org.isoron.uhabits.R
* launched for the first time. * launched for the first time.
*/ */
class IntroActivity : AppIntro2() { class IntroActivity : AppIntro2() {
override fun init(savedInstanceState: Bundle?) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
showStatusBar(false) showStatusBar(false)
addSlide( addSlide(
@@ -64,13 +61,11 @@ class IntroActivity : AppIntro2() {
) )
} }
override fun onDonePressed(currentFragment: Fragment?) { override fun onNextPressed() {}
super.onDonePressed(currentFragment)
override fun onDonePressed() {
finish() finish()
} }
override fun onSkipPressed(currentFragment: Fragment?) { override fun onSlideChanged() {}
super.onSkipPressed(currentFragment)
finish()
}
} }

View File

@@ -19,14 +19,16 @@
package org.isoron.uhabits.activities.settings package org.isoron.uhabits.activities.settings
import android.app.backup.BackupManager import android.app.backup.BackupManager
import android.content.Context
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.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
@@ -41,6 +43,7 @@ import org.isoron.uhabits.activities.habits.list.RESULT_REPAIR_DB
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames
import org.isoron.uhabits.intents.IntentFactory
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.widgets.WidgetUpdater import org.isoron.uhabits.widgets.WidgetUpdater
@@ -90,13 +93,20 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
showRingtonePicker() showRingtonePicker()
return true return true
} else if (key == "reminderCustomize") { } else if (key == "reminderCustomize") {
if (SDK_INT < Build.VERSION_CODES.O) return true if (VERSION.SDK_INT < Build.VERSION_CODES.O) return true
createAndroidNotificationChannel(context!!) createAndroidNotificationChannel(context!!)
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context!!.packageName) intent.putExtra(Settings.EXTRA_APP_PACKAGE, context!!.packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID) intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID)
startActivity(intent) startActivity(intent)
return true return true
} else if (key == "pref_sync_enabled_dummy") {
if (prefs.isSyncEnabled) {
prefs.disableSync()
} else {
val context: Context? = activity
context!!.startActivity(IntentFactory().startSyncActivity(context))
}
} }
return super.onPreferenceTreeClick(preference) return super.onPreferenceTreeClick(preference)
} }
@@ -111,14 +121,21 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
devCategory.isVisible = false devCategory.isVisible = false
} }
updateWeekdayPreference() updateWeekdayPreference()
updateSyncPreferences()
if (SDK_INT < Build.VERSION_CODES.O) if (VERSION.SDK_INT < Build.VERSION_CODES.O)
findPreference("reminderCustomize").isVisible = false findPreference("reminderCustomize").isVisible = false
else { else {
findPreference("reminderSound").isVisible = false findPreference("reminderSound").isVisible = false
} }
} }
private fun updateSyncPreferences() {
findPreference("pref_sync_display").isVisible = prefs.isSyncEnabled
(findPreference("pref_sync_enabled_dummy") as CheckBoxPreference).isChecked =
prefs.isSyncEnabled
}
private fun updateWeekdayPreference() { private fun updateWeekdayPreference() {
val weekdayPref = findPreference("pref_first_weekday") as ListPreference val weekdayPref = findPreference("pref_first_weekday") as ListPreference
val currentFirstWeekday = prefs.firstWeekday.daysSinceSunday + 1 val currentFirstWeekday = prefs.firstWeekday.daysSinceSunday + 1
@@ -140,6 +157,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
} }
BackupManager.dataChanged("org.isoron.uhabits") BackupManager.dataChanged("org.isoron.uhabits")
updateWeekdayPreference() updateWeekdayPreference()
updateSyncPreferences()
} }
private fun setResultOnPreferenceClick(key: String, result: Int) { private fun setResultOnPreferenceClick(key: String, result: Int) {

View File

@@ -0,0 +1,132 @@
/*
* 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.sync
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.text.Html
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.ui.screens.sync.SyncBehavior
import org.isoron.uhabits.databinding.ActivitySyncBinding
import org.isoron.uhabits.sync.RemoteSyncServer
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.utils.setupToolbar
import org.isoron.uhabits.utils.showMessage
class SyncActivity : AppCompatActivity(), SyncBehavior.Screen {
private lateinit var binding: ActivitySyncBinding
private lateinit var behavior: SyncBehavior
private val scope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstance: Bundle?) {
super.onCreate(savedInstance)
val component = (application as HabitsApplication).component
val preferences = component.preferences
val server = RemoteSyncServer(preferences = preferences)
val themeSwitcher = AndroidThemeSwitcher(this, component.preferences)
themeSwitcher.apply()
behavior = SyncBehavior(this, preferences, server, component.logging)
binding = ActivitySyncBinding.inflate(layoutInflater)
binding.errorIcon.typeface = getFontAwesome(this)
binding.root.setupToolbar(
toolbar = binding.toolbar,
color = PaletteColor(11),
title = resources.getString(R.string.device_sync),
theme = themeSwitcher.currentTheme,
)
binding.syncLink.setOnClickListener { copyToClipboard() }
binding.instructions.text = Html.fromHtml(resources.getString(R.string.sync_instructions))
setContentView(binding.root)
}
override fun onResume() {
super.onResume()
scope.launch {
behavior.onResume()
}
}
private fun copyToClipboard() {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("Loop Sync Link", binding.syncLink.text))
showMessage(resources.getString(R.string.copied_to_the_clipboard))
}
suspend fun generateQR(msg: String): Bitmap = Dispatchers.IO {
val writer = QRCodeWriter()
val matrix = writer.encode(msg, BarcodeFormat.QR_CODE, 1024, 1024)
val height = matrix.height
val width = matrix.width
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
val bgColor = Color.WHITE
val fgColor = Color.BLACK
for (x in 0 until width) {
for (y in 0 until height) {
val color = if (matrix.get(x, y)) fgColor else bgColor
bitmap.setPixel(x, y, color)
}
}
return@IO bitmap
}
suspend fun showQR(msg: String) {
binding.progress.visibility = View.GONE
binding.qrCode.visibility = View.VISIBLE
binding.qrCode.setImageBitmap(generateQR(msg))
}
override suspend fun showLoadingScreen() {
binding.qrCode.visibility = View.GONE
binding.progress.visibility = View.VISIBLE
binding.errorPanel.visibility = View.GONE
}
override suspend fun showErrorScreen() {
binding.qrCode.visibility = View.GONE
binding.progress.visibility = View.GONE
binding.errorPanel.visibility = View.VISIBLE
}
override suspend fun showLink(link: String) {
binding.qrCode.visibility = View.GONE
binding.progress.visibility = View.VISIBLE
binding.errorPanel.visibility = View.GONE
binding.syncLink.text = link
showQR(link)
}
}

View File

@@ -23,17 +23,17 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.HabitMatcher import org.isoron.uhabits.core.models.HabitMatcherBuilder
class EditSettingActivity : AppCompatActivity() { class EditSettingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val app = applicationContext as HabitsApplication val app = applicationContext as HabitsApplication
val habits = app.component.habitList.getFiltered( val habits = app.component.habitList.getFiltered(
HabitMatcher( HabitMatcherBuilder()
isArchivedAllowed = false, .setArchivedAllowed(false)
isCompletedAllowed = true, .setCompletedAllowed(true)
) .build()
) )
AndroidThemeSwitcher(this, app.component.preferences).apply() AndroidThemeSwitcher(this, app.component.preferences).apply()

View File

@@ -29,6 +29,7 @@ import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.preferences.WidgetPreferences import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.core.reminders.ReminderScheduler import org.isoron.uhabits.core.reminders.ReminderScheduler
import org.isoron.uhabits.core.sync.SyncManager
import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.ui.screens.habits.list.HabitCardListCache import org.isoron.uhabits.core.ui.screens.habits.list.HabitCardListCache
@@ -63,4 +64,5 @@ interface HabitsApplicationComponent {
val taskRunner: TaskRunner val taskRunner: TaskRunner
val widgetPreferences: WidgetPreferences val widgetPreferences: WidgetPreferences
val widgetUpdater: WidgetUpdater val widgetUpdater: WidgetUpdater
val syncManager: SyncManager
} }

View File

@@ -19,6 +19,7 @@
package org.isoron.uhabits.inject package org.isoron.uhabits.inject
import android.content.Context
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.AppScope
@@ -33,6 +34,8 @@ import org.isoron.uhabits.core.models.sqlite.SQLiteHabitList
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.preferences.WidgetPreferences import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.core.reminders.ReminderScheduler import org.isoron.uhabits.core.reminders.ReminderScheduler
import org.isoron.uhabits.core.sync.AbstractSyncServer
import org.isoron.uhabits.core.sync.NetworkManager
import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.database.AndroidDatabase import org.isoron.uhabits.database.AndroidDatabase
@@ -41,6 +44,8 @@ import org.isoron.uhabits.intents.IntentScheduler
import org.isoron.uhabits.io.AndroidLogging import org.isoron.uhabits.io.AndroidLogging
import org.isoron.uhabits.notifications.AndroidNotificationTray import org.isoron.uhabits.notifications.AndroidNotificationTray
import org.isoron.uhabits.preferences.SharedPreferencesStorage import org.isoron.uhabits.preferences.SharedPreferencesStorage
import org.isoron.uhabits.sync.AndroidNetworkManager
import org.isoron.uhabits.sync.RemoteSyncServer
import org.isoron.uhabits.utils.DatabaseUtils import org.isoron.uhabits.utils.DatabaseUtils
import java.io.File import java.io.File
@@ -109,6 +114,18 @@ class HabitsModule(dbFile: File) {
return AndroidLogging() return AndroidLogging()
} }
@Provides
@AppScope
fun getNetworkManager(@AppContext context: Context): NetworkManager {
return AndroidNetworkManager(context)
}
@Provides
@AppScope
fun getSyncServer(preferences: Preferences): AbstractSyncServer {
return RemoteSyncServer(preferences)
}
@Provides @Provides
@AppScope @AppScope
fun getDatabase(): Database { fun getDatabase(): Database {

View File

@@ -28,6 +28,7 @@ import org.isoron.uhabits.activities.habits.edit.EditHabitActivity
import org.isoron.uhabits.activities.habits.show.ShowHabitActivity import org.isoron.uhabits.activities.habits.show.ShowHabitActivity
import org.isoron.uhabits.activities.intro.IntroActivity import org.isoron.uhabits.activities.intro.IntroActivity
import org.isoron.uhabits.activities.settings.SettingsActivity import org.isoron.uhabits.activities.settings.SettingsActivity
import org.isoron.uhabits.activities.sync.SyncActivity
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import javax.inject.Inject import javax.inject.Inject
@@ -100,4 +101,8 @@ class IntentFactory
intent.putExtra("habitType", habitType) intent.putExtra("habitType", habitType)
return intent return intent
} }
fun startSyncActivity(context: Context): Intent {
return Intent(context, SyncActivity::class.java)
}
} }

View File

@@ -25,6 +25,8 @@ import android.app.AlarmManager.RTC_WAKEUP
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Context.ALARM_SERVICE import android.content.Context.ALARM_SERVICE
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.M
import android.util.Log import android.util.Log
import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
@@ -56,7 +58,10 @@ class IntentScheduler
) )
return SchedulerResult.IGNORED return SchedulerResult.IGNORED
} }
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent) if (SDK_INT >= M)
manager.setExactAndAllowWhileIdle(alarmType, timestamp, intent)
else
manager.setExact(alarmType, timestamp, intent)
return SchedulerResult.OK return SchedulerResult.OK
} }

View File

@@ -20,7 +20,6 @@
package org.isoron.uhabits.intents package org.isoron.uhabits.intents
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getBroadcast import android.app.PendingIntent.getBroadcast
import android.content.Context import android.content.Context
@@ -50,7 +49,7 @@ class PendingIntentFactory
action = WidgetReceiver.ACTION_ADD_REPETITION action = WidgetReceiver.ACTION_ADD_REPETITION
if (timestamp != null) putExtra("timestamp", timestamp.unixTime) if (timestamp != null) putExtra("timestamp", timestamp.unixTime)
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun dismissNotification(habit: Habit): PendingIntent = fun dismissNotification(habit: Habit): PendingIntent =
@@ -61,7 +60,7 @@ class PendingIntentFactory
action = WidgetReceiver.ACTION_DISMISS_REMINDER action = WidgetReceiver.ACTION_DISMISS_REMINDER
data = Uri.parse(habit.uriString) data = Uri.parse(habit.uriString)
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun removeRepetition(habit: Habit, timestamp: Timestamp?): PendingIntent = fun removeRepetition(habit: Habit, timestamp: Timestamp?): PendingIntent =
@@ -73,7 +72,7 @@ class PendingIntentFactory
data = Uri.parse(habit.uriString) data = Uri.parse(habit.uriString)
if (timestamp != null) putExtra("timestamp", timestamp.unixTime) if (timestamp != null) putExtra("timestamp", timestamp.unixTime)
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun showHabit(habit: Habit): PendingIntent = fun showHabit(habit: Habit): PendingIntent =
@@ -85,7 +84,7 @@ class PendingIntentFactory
habit habit
) )
) )
.getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)!! .getPendingIntent(0, FLAG_UPDATE_CURRENT)!!
fun showReminder( fun showReminder(
habit: Habit, habit: Habit,
@@ -101,7 +100,7 @@ class PendingIntentFactory
putExtra("timestamp", timestamp) putExtra("timestamp", timestamp)
putExtra("reminderTime", reminderTime) putExtra("reminderTime", reminderTime)
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun snoozeNotification(habit: Habit): PendingIntent = fun snoozeNotification(habit: Habit): PendingIntent =
@@ -112,7 +111,7 @@ class PendingIntentFactory
data = Uri.parse(habit.uriString) data = Uri.parse(habit.uriString)
action = ReminderReceiver.ACTION_SNOOZE_REMINDER action = ReminderReceiver.ACTION_SNOOZE_REMINDER
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent = fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent =
@@ -124,7 +123,7 @@ class PendingIntentFactory
action = WidgetReceiver.ACTION_TOGGLE_REPETITION action = WidgetReceiver.ACTION_TOGGLE_REPETITION
if (timestamp != null) putExtra("timestamp", timestamp) if (timestamp != null) putExtra("timestamp", timestamp)
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun setNumericalValue( fun setNumericalValue(
@@ -143,7 +142,7 @@ class PendingIntentFactory
putExtra("numericalValue", numericalValue) putExtra("numericalValue", numericalValue)
if (timestamp != null) putExtra("timestamp", timestamp) if (timestamp != null) putExtra("timestamp", timestamp)
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
fun updateWidgets(): PendingIntent = fun updateWidgets(): PendingIntent =
@@ -153,6 +152,6 @@ class PendingIntentFactory
Intent(context, WidgetReceiver::class.java).apply { Intent(context, WidgetReceiver::class.java).apply {
action = WidgetReceiver.ACTION_UPDATE_WIDGETS_VALUE action = WidgetReceiver.ACTION_UPDATE_WIDGETS_VALUE
}, },
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT FLAG_UPDATE_CURRENT
) )
} }

View File

@@ -25,6 +25,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory.decodeResource import android.graphics.BitmapFactory.decodeResource
import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.util.Log import android.util.Log
@@ -149,15 +150,16 @@ class AndroidNotificationTray
if (!disableSound) if (!disableSound)
builder.setSound(ringtoneManager.getURI()) builder.setSound(ringtoneManager.getURI())
if (SDK_INT < Build.VERSION_CODES.S) { if (preferences.shouldMakeNotificationsLed())
val snoozeAction = Action( builder.setLights(Color.RED, 1000, 1000)
R.drawable.ic_action_snooze,
context.getString(R.string.snooze), val snoozeAction = Action(
pendingIntents.snoozeNotification(habit) R.drawable.ic_action_snooze,
) context.getString(R.string.snooze),
wearableExtender.addAction(snoozeAction) pendingIntents.snoozeNotification(habit)
builder.addAction(snoozeAction) )
} wearableExtender.addAction(snoozeAction)
builder.addAction(snoozeAction)
builder.extend(wearableExtender) builder.extend(wearableExtender)
return builder.build() return builder.build()

View File

@@ -90,6 +90,8 @@ class SharedPreferencesStorage
preferences.isMidnightDelayEnabled = getBoolean(key, false) preferences.isMidnightDelayEnabled = getBoolean(key, false)
"pref_sticky_notifications" -> "pref_sticky_notifications" ->
preferences.setNotificationsSticky(getBoolean(key, false)) preferences.setNotificationsSticky(getBoolean(key, false))
"pref_led_notifications" ->
preferences.setNotificationsLed(getBoolean(key, false))
"pref_unknown_enabled" -> { "pref_unknown_enabled" -> {
preferences.areQuestionMarksEnabled = getBoolean(key, false) preferences.areQuestionMarksEnabled = getBoolean(key, false)
} }

View File

@@ -22,8 +22,6 @@ import android.content.BroadcastReceiver
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.util.Log import android.util.Log
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
@@ -78,21 +76,8 @@ class ReminderReceiver : BroadcastReceiver() {
} }
ACTION_SNOOZE_REMINDER -> { ACTION_SNOOZE_REMINDER -> {
if (habit == null) return if (habit == null) return
if (SDK_INT < Build.VERSION_CODES.S) { Log.d("ReminderReceiver", String.format("onSnoozePressed habit=%d", habit.id))
Log.d( reminderController.onSnoozePressed(habit, context)
"ReminderReceiver",
String.format("onSnoozePressed habit=%d", habit.id)
)
reminderController.onSnoozePressed(habit, context)
} else {
Log.w(
"ReminderReceiver",
String.format(
"onSnoozePressed habit=%d, should be deactivated in recent versions.",
habit.id
)
)
}
} }
Intent.ACTION_BOOT_COMPLETED -> { Intent.ACTION_BOOT_COMPLETED -> {
Log.d("ReminderReceiver", "onBootCompleted") Log.d("ReminderReceiver", "onBootCompleted")

View File

@@ -0,0 +1,59 @@
/*
* 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.sync
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
import org.isoron.uhabits.core.sync.NetworkManager
class AndroidNetworkManager(
val context: Context,
) : NetworkManager, ConnectivityManager.NetworkCallback() {
val listeners = mutableListOf<NetworkManager.Listener>()
var connected = false
init {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
cm.registerNetworkCallback(NetworkRequest.Builder().build(), this)
}
override fun addListener(listener: NetworkManager.Listener) {
if (connected) listener.onNetworkAvailable()
else listener.onNetworkLost()
listeners.add(listener)
}
override fun remoteListener(listener: NetworkManager.Listener) {
listeners.remove(listener)
}
override fun onAvailable(network: Network) {
connected = true
for (l in listeners) l.onNetworkAvailable()
}
override fun onLost(network: Network) {
connected = false
for (l in listeners) l.onNetworkLost()
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.sync
import android.util.Log
import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
import io.ktor.client.features.ClientRequestException
import io.ktor.client.features.ServerResponseException
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.put
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.sync.AbstractSyncServer
import org.isoron.uhabits.core.sync.EditConflictException
import org.isoron.uhabits.core.sync.GetDataVersionResponse
import org.isoron.uhabits.core.sync.KeyNotFoundException
import org.isoron.uhabits.core.sync.RegisterReponse
import org.isoron.uhabits.core.sync.ServiceUnavailable
import org.isoron.uhabits.core.sync.SyncData
class RemoteSyncServer(
private val preferences: Preferences,
private val httpClient: HttpClient = HttpClient(Android) {
install(JsonFeature)
}
) : AbstractSyncServer {
override suspend fun register(): String = Dispatchers.IO {
try {
val url = "${preferences.syncBaseURL}/register"
Log.i("RemoteSyncServer", "POST $url")
val response: RegisterReponse = httpClient.post(url)
return@IO response.key
} catch (e: ServerResponseException) {
throw ServiceUnavailable()
}
}
override suspend fun put(key: String, newData: SyncData) = Dispatchers.IO {
try {
val url = "${preferences.syncBaseURL}/db/$key"
Log.i("RemoteSyncServer", "PUT $url")
val response: String = httpClient.put(url) {
header("Content-Type", "application/json")
body = newData
}
} catch (e: ServerResponseException) {
throw ServiceUnavailable()
} catch (e: ClientRequestException) {
Log.w("RemoteSyncServer", "ClientRequestException", e)
if (e.message!!.contains("409")) throw EditConflictException()
if (e.message!!.contains("404")) throw KeyNotFoundException()
throw e
}
}
override suspend fun getData(key: String): SyncData = Dispatchers.IO {
try {
val url = "${preferences.syncBaseURL}/db/$key"
Log.i("RemoteSyncServer", "GET $url")
return@IO httpClient.get<SyncData>(url)
} catch (e: ServerResponseException) {
throw ServiceUnavailable()
} catch (e: ClientRequestException) {
Log.w("RemoteSyncServer", "ClientRequestException", e)
throw KeyNotFoundException()
}
}
override suspend fun getDataVersion(key: String): Long = Dispatchers.IO {
try {
val url = "${preferences.syncBaseURL}/db/$key/version"
Log.i("RemoteSyncServer", "GET $url")
val response: GetDataVersionResponse = httpClient.get(url)
return@IO response.version
} catch (e: ServerResponseException) {
throw ServiceUnavailable()
} catch (e: ClientRequestException) {
Log.w("RemoteSyncServer", "ClientRequestException", e)
throw KeyNotFoundException()
}
}
}

View File

@@ -22,13 +22,14 @@ import android.app.Activity
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.view.WindowManager import android.view.WindowManager
object SystemUtils { object SystemUtils {
val isAndroidOOrLater: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
fun unlockScreen(activity: Activity) { fun unlockScreen(activity: Activity) {
if (SDK_INT >= Build.VERSION_CODES.O) { if (isAndroidOOrLater) {
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
km.requestDismissKeyguard(activity, null) km.requestDismissKeyguard(activity, null)
} else { } else {

View File

@@ -20,22 +20,16 @@
package org.isoron.uhabits.utils package org.isoron.uhabits.utils
import android.app.Activity import android.app.Activity
import android.app.Dialog
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Handler import android.os.Handler
import android.os.SystemClock
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.WindowManager
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM import android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM
import android.widget.RelativeLayout.ALIGN_PARENT_TOP import android.widget.RelativeLayout.ALIGN_PARENT_TOP
@@ -205,33 +199,5 @@ fun View.dim(id: Int) = InterfaceUtils.getDimension(context, id)
fun View.sp(value: Float) = InterfaceUtils.spToPixels(context, value) fun View.sp(value: Float) = InterfaceUtils.spToPixels(context, value)
fun View.dp(value: Float) = InterfaceUtils.dpToPixels(context, value) fun View.dp(value: Float) = InterfaceUtils.dpToPixels(context, value)
fun View.str(id: Int) = resources.getString(id) fun View.str(id: Int) = resources.getString(id)
fun View.drawNotesIndicator(canvas: Canvas, color: Int, size: Float, notes: String) {
val pNotesIndicator = Paint()
pNotesIndicator.color = color
if (notes.isNotBlank()) {
val cy = 0.8f * size
canvas.drawCircle(width.toFloat() - cy, cy, 8f, pNotesIndicator)
}
}
val View.sres: StyledResources val View.sres: StyledResources
get() = StyledResources(context) get() = StyledResources(context)
fun Dialog.dimBehind() {
window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
window?.setDimAmount(0.5f)
}
fun View.requestFocusWithKeyboard() {
// For some reason, Android does not open the soft keyboard by default when view.requestFocus
// is called. Several online solutions suggest using InputMethodManager, but these solutions
// are not reliable; sometimes the keyboard does not show, and sometimes it does not go away
// after focus is lost. Here, we simulate a click on the view, which triggers the keyboard.
// Based on: https://stackoverflow.com/a/7699556
postDelayed({
val time = SystemClock.uptimeMillis()
dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0f, 0f, 0))
dispatchTouchEvent(MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, 0f, 0f, 0))
}, 250)
}

View File

@@ -56,10 +56,7 @@ class HistoryWidget(
theme = WidgetTheme(), theme = WidgetTheme(),
) )
(widgetView.dataView as AndroidDataView).apply { (widgetView.dataView as AndroidDataView).apply {
val historyChart = (this.view as HistoryChart) (this.view as HistoryChart).series = model.series
historyChart.series = model.series
historyChart.defaultSquare = model.defaultSquare
historyChart.notesIndicators = model.notesIndicators
} }
} }
@@ -74,8 +71,6 @@ class HistoryWidget(
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()), dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
firstWeekday = prefs.firstWeekday, firstWeekday = prefs.firstWeekday,
series = listOf(), series = listOf(),
defaultSquare = HistoryChart.Square.OFF,
notesIndicators = listOf(),
) )
} }
).apply { ).apply {

View File

@@ -26,8 +26,8 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
import org.isoron.platform.utils.StringUtils
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.utils.StringUtils
class StackWidget( class StackWidget(
context: Context, context: Context,

View File

@@ -27,11 +27,11 @@ import android.util.Log
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.RemoteViewsService import android.widget.RemoteViewsService
import android.widget.RemoteViewsService.RemoteViewsFactory import android.widget.RemoteViewsService.RemoteViewsFactory
import org.isoron.platform.utils.StringUtils.Companion.splitLongs
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
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.StringUtils.Companion.splitLongs
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
import java.util.ArrayList import java.util.ArrayList

View File

@@ -22,12 +22,11 @@ package org.isoron.uhabits.widgets.activities
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.Window
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout import android.widget.FrameLayout
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.common.dialogs.NumberPopup import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.widgets.WidgetBehavior import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
@@ -40,13 +39,11 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
private lateinit var behavior: WidgetBehavior private lateinit var behavior: WidgetBehavior
private lateinit var data: IntentParser.CheckmarkIntentData private lateinit var data: IntentParser.CheckmarkIntentData
private lateinit var widgetUpdater: WidgetUpdater private lateinit var widgetUpdater: WidgetUpdater
private lateinit var rootView: View
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
rootView = FrameLayout(this) requestWindowFeature(Window.FEATURE_NO_TITLE)
rootView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) setContentView(FrameLayout(this))
setContentView(rootView)
val app = this.applicationContext as HabitsApplication val app = this.applicationContext as HabitsApplication
val component = app.component val component = app.component
val parser = app.component.intentParser val parser = app.component.intentParser
@@ -58,14 +55,13 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
component.preferences component.preferences
) )
widgetUpdater = component.widgetUpdater widgetUpdater = component.widgetUpdater
rootView.post { showNumberSelector(this)
showNumberSelector(this)
}
SystemUtils.unlockScreen(this) SystemUtils.unlockScreen(this)
} }
override fun onNumberPicked(newValue: Double, notes: String) { override fun onNumberPicked(newValue: Double) {
behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt(), notes) behavior.setValue(data.habit, data.timestamp, (newValue * 1000).toInt())
widgetUpdater.updateWidgets() widgetUpdater.updateWidgets()
finish() finish()
} }
@@ -77,22 +73,14 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
private fun showNumberSelector(context: Context) { private fun showNumberSelector(context: Context) {
val app = this.applicationContext as HabitsApplication val app = this.applicationContext as HabitsApplication
AndroidThemeSwitcher(this, app.component.preferences).apply() AndroidThemeSwitcher(this, app.component.preferences).apply()
val numberPickerFactory = NumberPickerFactory(context)
val today = DateUtils.getTodayWithOffset() val today = DateUtils.getTodayWithOffset()
val entry = data.habit.computedEntries.get(today) val entry = data.habit.computedEntries.get(today)
NumberPopup( numberPickerFactory.create(
context = context, entry.value / 1000.0,
prefs = app.component.preferences, data.habit.unit,
anchor = rootView, this
notes = entry.notes, ).show()
value = entry.value / 1000.0,
).apply {
onToggle = { value, notes ->
onNumberPicked(value, notes)
finish()
overridePendingTransition(0, 0)
}
show()
}
} }
companion object { companion object {

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF"
android:alpha="0.8">
<path
android:fillColor="@android:color/white"
android:pathData="M20,12l-1.41,-1.41L13,16.17V4h-2v12.17l-5.58,-5.59L4,12l8,8 8,-8z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
<path
android:fillColor="@android:color/white"
android:pathData="M20,12l-1.41,-1.41L13,16.17V4h-2v12.17l-5.58,-5.59L4,12l8,8 8,-8z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF"
android:alpha="0.8">
<path
android:fillColor="@android:color/white"
android:pathData="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
<path
android:fillColor="@android:color/white"
android:pathData="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

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