Compare commits
2 Commits
2fc6c67432
...
feature/sy
| Author | SHA1 | Date | |
|---|---|---|---|
| e8a4a9740e | |||
| 7f1a1add8c |
4
.github/workflows/main.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
|
jcenter()
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
resolutionStrategy.eachPlugin {
|
resolutionStrategy.eachPlugin {
|
||||||
|
|||||||
@@ -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,10 +88,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
val daggerVersion = "2.42"
|
val daggerVersion = "2.38.1"
|
||||||
val kotlinVersion = "1.7.0"
|
val kotlinVersion = "1.5.21"
|
||||||
val kxCoroutinesVersion = "1.6.3"
|
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")
|
||||||
@@ -98,17 +100,17 @@ dependencies {
|
|||||||
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1")
|
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")
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 20 KiB |
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 -> }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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? {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 155 B |
|
After Width: | Height: | Size: 338 B |
BIN
uhabits-android/src/main/res/drawable-hdpi/ic_action_cancel.png
Normal file
|
After Width: | Height: | Size: 266 B |
BIN
uhabits-android/src/main/res/drawable-hdpi/ic_action_check.png
Normal file
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 492 B |
|
After Width: | Height: | Size: 563 B |
|
After Width: | Height: | Size: 249 B |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 354 B |
|
After Width: | Height: | Size: 111 B |
BIN
uhabits-android/src/main/res/drawable-hdpi/ic_action_snooze.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 337 B |
|
After Width: | Height: | Size: 240 B |
|
After Width: | Height: | Size: 237 B |
BIN
uhabits-android/src/main/res/drawable-hdpi/ic_arrow_up_dark.png
Normal file
|
After Width: | Height: | Size: 234 B |
BIN
uhabits-android/src/main/res/drawable-hdpi/ic_arrow_up_light.png
Normal file
|
After Width: | Height: | Size: 246 B |
|
After Width: | Height: | Size: 2.4 KiB |
BIN
uhabits-android/src/main/res/drawable-hdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 618 B |
|
After Width: | Height: | Size: 111 B |