Compare commits

..

1 Commits

Author SHA1 Message Date
Jakub Kalinowski
25a3509988 Reimplementing the multiple popups handling in the new popup solution. 2022-08-13 14:39:01 +02:00
91 changed files with 641 additions and 401 deletions

View File

@@ -9,7 +9,6 @@ on:
jobs:
Test:
runs-on: self-hosted
timeout-minutes: 30
steps:
- name: Check out source code
uses: actions/checkout@v1
@@ -18,7 +17,7 @@ jobs:
run: ./build.sh build
- name: Run Android tests
run: ./build.sh android-tests-parallel 28 29 30 31 32 33
run: ./build.sh android-tests-parallel 23 24 25 26 27 28 30 31
- name: Upload artifacts
if: always()

5
.gitignore vendored
View File

@@ -12,8 +12,13 @@
.idea
.secret
build
build/
captures
local.properties
node_modules
*xcuserdata*
*.sketch
/design
/releases
/screenshots
crowdin.yml

16
.secret/decrypt.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
cd "$(dirname "$0")"
if [ -z "$GPG_PASSWORD" ]; then
echo Env variable GPG_PASSWORD must be defined
exit 1
fi
gpg \
--quiet \
--batch \
--yes \
--decrypt \
--passphrase="$GPG_PASSWORD" \
--output secret.tar.gz \
secret
tar -xzf secret.tar.gz
rm secret.tar.gz

BIN
.secret/secret Normal file

Binary file not shown.

View File

@@ -1,10 +1,6 @@
# Changelog
## [2.1.1] -- 2022-09-24
### Fixed
- Fix Tasker plugin (@iSoron, #1503)
## [2.1.0] -- 2022-09-10
## [2.1.0] -- Unreleased
### Added
- Allow user to add notes to specific dates (@vbh, #1103)
- Allow user to track "at most" numerical habits (@KristianTashkov, #1101)
@@ -13,7 +9,6 @@
- Improve number picker (@hiqua, @iSoron, #1082, #1370)
- Add new checkmark and number picker (@iSoron, #1370)
- Allow user to import numerical habits from HabitBull (@hiqua, #1278)
- Add support for Android 13 themed icons (@cheeeeer, #1497)
### Removed
- Hide snooze button Android 12 notifications (@hiqua, #1226)
@@ -33,7 +28,6 @@
- Fix small issues in calendar chart (@kalina559, #1314)
- Resort habit list after edit (@hiqua, #1350)
- Fix marker scaling in frequency display (@eduebernal, #1425)
- Fix widgets not working correctly on API 33 (@iSoron, #1488)
### Refactoring & Testing
- Replace raster icons by vector assets (@kalina559)

View File

@@ -1,11 +1,11 @@
plugins {
val kotlinVersion = "1.7.21"
id("com.android.application") version ("7.3.1") apply (false)
val kotlinVersion = "1.6.10"
id("com.android.application") version ("7.0.3") apply (false)
id("org.jetbrains.kotlin.android") 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.multiplatform") version kotlinVersion apply (false)
id("org.jlleitschuh.gradle.ktlint") version "11.0.0"
id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
}
apply {

View File

@@ -85,10 +85,10 @@ android_setup() {
$AVDMANAGER delete avd --name $AVDNAME
log_info "Creating new Android virtual device (API $API)..."
(echo "y" | $SDKMANAGER --install "system-images;android-$API;google_apis;x86_64") || return 1
(echo "y" | $SDKMANAGER --install "system-images;android-$API;default;x86_64") || return 1
$AVDMANAGER create avd \
--name $AVDNAME \
--package "system-images;android-$API;google_apis;x86_64" \
--package "system-images;android-$API;default;x86_64" \
--device "Nexus 4" || return 1
flock -u 10

View File

@@ -33,7 +33,7 @@ The repository will be downloaded to the directory `uhabits`.
2. When the IDE asks you for the project location, select `uhabits` and click "Ok".
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.
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.

View File

@@ -2,25 +2,51 @@
Loop Habit Tracker has a fairly large number of automated tests to reduce the chance of bugs being silently introduced in our code base. The tests are divided into three categories:
- **Unit tests:** These tests run very quickly on the developer's computer, inside a JVM, and do not need an Android emulator or device. They typically test the correctness of core functions of the application, such as the computation of scores and streaks.
- **Instrumented tests:** These tests require an Android emulator or device. _Medium_ instrumented tests are still quite fast to run, since only individual classes are tested. The app itself does not need to be launched. Examples include _view tests_, which render our custom views on the device and compare them against prerendered images. _Large_ instrumented tests launch the application on an Android emulator and interact with it by touching the screen, much like a regular user.
* **Small tests:** These tests run very quickly on the developer's computer, inside a JVM, and do not need an Android emulator or device. They typically test the correctness of core functions of the application, such as the computation of scores and streaks.
* **Medium tests:** These tests require an Android emulator or device, but they are still quite fast to run, since only individual classes are tested. The app itself does not need to be launched. Examples include *view tests*, which render our custom views on the device and compare them against prerendered images.
* **Large tests:** These are end-to-end tests, which launch the application on an Android emulator and interact with it by touching the screen, much like a regular user.
## Running unit tests
## Running small tests
Unit tests can be launched by running `./gradlew test` or by right-clicking a particular class/method in Android Studio and selecting "Run testMethod()" or "Run ClassTest". An alternative way is to use `build.sh`, the script used by our continuous integration server. By running `./build.sh build`, the script will automatically build and run all small tests.
Small tests can be launched by running `./gradlew test` or by right-clicking a particular class/method in Android Studio and selecting "Run testMethod()" or "Run ClassTest". An alternative way is to use `build.sh`, the script used by our continuous integration server. By running `./build.sh build`, the script will automatically build and run all small tests.
## Running instrumented tests
## Running medium tests
To run medium tests, it is recommended to use the `build.sh` script.
To run medium tests, it is recommended to use the `build.sh` script:
1. Run `./build.sh android-setup API` to create the emulator, where `API` is the desired API level.
2. Run `./build.sh android-tests API` to run the tests on a single API.
3. Run `./build.sh android-tests-parallel API API...` to run the tests on multiple APIs in parallel.
./build.sh build
./build.sh medium-tests
Note that instrumented tests are designed to run on a clean install, inside an emulator. They will not work on actual devices. All tests are also designed for a particular screen size, namely the Nexus 4 configuration (4.7" 768x1280 xhdpi), and a particular locale, namely English (US). Furthermore:
- No additional apps should be installed on the device;
- The homescreen must look exactly like it was when the emulator was originally created, with no additional icons or widgets;
- All animations must be manually disabled.
For this script to succeed, make sure that an emulator is currently running, or that a device (with developer mode activated) is connected via USB.
If there are failing view tests (that is, if some custom views do not render exactly like the prerendered images we have), then both the actual and expected images will be automatically downloaded from the device to the folder `uhabits-android/build/outputs`. After verifying the differences, if you feel that the actual images are actually fine and should replace the prerendered ones, then run `./build.sh android-accept-images`.
**WARNING!** This script will uninstall the app prior to testing it, and therefore delete all user data!
If there are failing view tests (that is, if some custom views do not render exactly like the prerendered images we have), then the script `./build.sh fetch-images` can be used to download both the actual and the expected images from the device. The images will be downloaded from the device into the folder `tmp/`. After verifying the differences, if you feel that the actual images are actually fine and should replace the prerendered ones, then run `./build.sh accept-images`.
## Running large tests
Large tests are significantly more complicated to run. In particular, they require:
* An Android emulator; they will **not** work on actual devices;
* A vanilla x86 AOSP image; they will **not** work with Google API images;
* A particular screen size, namely the Nexus 4 configuration on Android Studio (4.7 768x1280 xhdpi);
* A particular locale, namely English (US).
Furthermore:
* No additional apps should be installed on the device;
* The homescreen must look exactly like it was when the emulator was originally created, with no additional icons or widgets;
* Developer mode must be activated, and all animations must be manually disabled.
Only the following Android versions are supported by our test suite:
* Android 7.0 (API 24)
* Android 7.1.1 (API 25)
* Android 8.0 (API 26)
* Android 8.1 (API 27)
* Android 9.0 (API 28)
* Android 10.0 (API 29)
After creating an emulator and configuring it exactly as described above, launch it, wait for it to finish booting up, then run `./build.sh large-tests`. As mentioned before, this script will uninstall the app before testing it, and therefore will delete all the user data.

View File

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

2
landing/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
out/
.sass-cache

27
landing/Makefile Normal file
View File

@@ -0,0 +1,27 @@
haml := src/*.haml
sass := src/*.sass
html := $(patsubst src/%, out/%, $(patsubst %.haml,%.html,$(wildcard $(haml))))
css := $(patsubst src/%, out/%, $(patsubst %.sass,%.css,$(wildcard $(sass))))
src := $(wildcard src/**)
compile: $(html) $(css)
@rsync -rupE assets/ out/
out/%.css: src/%.sass $(src)
@echo ' sass $<'
@mkdir -p `dirname $@`
@sass $< $@
out/%.html: src/%.haml $(src)
@echo ' haml $<'
@mkdir -p `dirname $@`
@haml -E UTF-8 $< $@
push:
rsync -avP out/ axavier.org:/www/loophabits.org/
clean:
@rm -rfv out
@rm -rfv tmp

27
landing/README.md Normal file
View File

@@ -0,0 +1,27 @@
Loop Habit Tracker Landing Page
===============================
This folder contains the source code that generates the project landing page, currently hosted at https://loophabits.org/
Pull requests with ideas for improving it are very welcome.
Build instructions
------------------
1. Install `haml`:
```bash
sudo apt install ruby-haml
```
2. Install `pandoc-ruby`:
```bash
gem install pandoc-ruby
```
3. Run `Makefile`
```bash
make
```
4. View the results (using, for example, [npm serve](https://www.npmjs.com/package/serve))
```bash
npm serve out/
```

1
landing/assets/faq.html Normal file
View File

@@ -0,0 +1 @@
<meta http-equiv="Refresh" content="0; url='https://github.com/iSoron/uhabits/discussions/689'" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
landing/assets/lib/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

106
landing/src/index.haml Normal file
View File

@@ -0,0 +1,106 @@
!!! 5
%html
%head
%meta(charset="UTF-8")
%link(href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css")
%meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no")
%title Loop Habit Tracker
%link(rel="stylesheet" type="text/css" href="lib/css/bootstrap.min.css")
%link(rel="stylesheet" type="text/css" href="index.css")
%body
.navbar.navbar-expand-md.navbar-light.bg-light
%a.navbar-brand(href="/")
%b Loop
Habit Tracker
%button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation")
%span.navbar-toggler-icon
#navbar.collapse.navbar-collapse
%ul.navbar-nav.mr-auto.mt-2.mt-lg-0
%li.nav-item
%a.nav-link(href="faq.html") FAQ
%li.nav-item
%a.nav-link(href="privacy.html") Privacy
%li.nav-item
%a.nav-link(href="https://source.loophabits.org") Source Code
%li.nav-item
%a.nav-link(href="https://translate.loophabits.org") Translate
.jumbotron.jumbotron-fluid
.site-wrapper
.container
.row.vertical-align
.col-md
%h1.display-4
Get your life on track
%p.lead
With daily reminders, beautiful charts and insightful statistics,
Loop Habit Tracker&trade; helps you create and maintain great habits. Completely free and open-source.
.store-badges
%a(href="https://play.google.com/store/apps/details?id=org.isoron.uhabits")
%img(src="images/google-play.png")
%a(href="https://f-droid.org/en/packages/org.isoron.uhabits/")
%img(src="images/f-droid.png")
.col-md
.s2
%img.screenshot(src="screenshots/uhabits1.png")
.s1
%img.screenshot(src="screenshots/uhabits4.png")
.section.screenshots
%span
%a(href="screenshots/uhabits1.png")
%img(src="screenshots/uhabits1_th.png")
%a(href="screenshots/uhabits2.png")
%img(src="screenshots/uhabits2_th.png")
%a(href="screenshots/uhabits3.png")
%img(src="screenshots/uhabits3_th.png")
%span
%a(href="screenshots/uhabits4.png")
%img(src="screenshots/uhabits4_th.png")
%a(href="screenshots/uhabits5.png")
%img(src="screenshots/uhabits5_th.png")
.section
.feature-header
%h1
Features
.container
.row
.col-md
%ul
%li
%h3 Habit score
Loop has an advanced formula for calculating the strength of your habits. Every repetition makes your habit stronger and every missed day makes it weaker. A few missed days after a long streak, however, will not completely destroy your progress, unlike many other don't-break-the-chain apps.
%li
%h3 Flexible schedules
In addition to daily habits, Loop supports habits with more complex schedules, such as 3 times per week or every other day.
%li
%h3 Reminders
Schedule notifications to remind you of your habits. Each habit can have its own reminder, at a chosen time of the day. Easily check or dismiss your habit directly from the notification.
%li
%h3 Widgets
Be reminded of your habits whenever you unlock your phone. Colorful widgets allow you to track your habits directly from your home screen, without even opening the app.
.col-md
%ul
%li
%h3 Take control of your data
If you want to further analyze your data, or move it to another service, Loop allows you to export it to spreadsheets (CSV) or to a database file (SQLite). For power users, check marks can be added through task automation apps such as Tasker.
%li
%h3 No limitations
Track as many habits as you wish. Loop imposes no artificial limits on how many habits you can have. All features are available to all users, and there are no in-app purchases.
%li
%h3 Completely ad-free and open source
There are no advertisements, annoying notifications or intrusive permissions in this app, and there will never be. The app is completely open-source (GPLv3).
%li
%h3 Works offline and respects your privacy
Loop doesn't require an Internet connection or online account registration. Your confidential data is never sent to anyone. Neither the developers nor any third-parties have access to it.
.section.footer
Copyright © 2016&ndash;2020, Alinson Santos Xavier. All Rights Reserved.
%script(type="text/javascript" src="lib/js/jquery.min.js")
%script(type="text/javascript" src="lib/js/bootstrap.bundle.min.js")

104
landing/src/index.sass Normal file
View File

@@ -0,0 +1,104 @@
html, body
max-width: 100%
overflow-x: hidden
body
font-family: 'Open Sans', sans-serif
padding-bottom: 0
a, a:hover
text-decoration: none
.navbar
box-shadow: rgba(0,0,0,0.4) 0px 0px 20px
background-color: white !important
.nav-link
margin: 0px 18px
.section
background-color: transparent
padding: 18px 0px
.container
ul
list-style-type: none
h3
font-size: 16px
font-weight: bold
margin: 18px 0px 0px 0px
.screenshots
text-align: center
background-color: #222
img
margin: 0.5%
border-radius: 10px
border: 3px solid #fff2
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.5)
max-width: 17%
.footer
color: #888
background-color: #222
text-align: center
font-size: 12px
.jumbotron
background: linear-gradient(rgba(0,30,200,0.8),rgba(90,30,150,0.5)), url("images/hero-background-filter.jpg")
box-shadow: rgba(0,0,0,0.5) 0px 0px 20px
margin: 0
h1
max-width: 25rem
font-weight: bold
color: white
p
max-width: 40rem
color: white
.screenshot
box-shadow: rgba(0, 0, 0, 0.5) 5px 5px 20px
padding: 0px 0px 0px 0px
border-radius: 10px
border: 2px solid rgba(255, 255, 255, 0.2)
background-color: transparent
max-width: 300px
.store-badges
margin: 2rem 1rem
img
opacity: 0.8
height: 75px
img:hover
opacity: 1.0
.s1
padding-bottom: 50px
padding-left: 50px
.s2
position: absolute
top: 50px
left: 175px
.feature-header
text-align: center
font-weight: bold
padding: 18px
.align-right
text-align: right
.vertical-align
display: flex
align-items: center
.content
max-width: 800px
margin: 18px auto
padding: 0px 18px
//padding-left: 120px
h2, h3, h4
margin: 27px 0px 9px 0px
h2, h3
//margin-left: -120px
h4
//margin-left: -60px
font-size: 16px
font-weight: bold

32
landing/src/privacy.haml Normal file
View File

@@ -0,0 +1,32 @@
!!! 5
%html
%head
%meta(charset="UTF-8")
%link(href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css")
%meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no")
%title Privacy | Loop Habit Tracker
%link(rel="stylesheet" type="text/css" href="lib/css/bootstrap.min.css")
%link(rel="stylesheet" type="text/css" href="index.css")
%body
.navbar.navbar-expand-md.navbar-light.bg-light
%a.navbar-brand(href="/")
%b Loop
Habit Tracker
%button.navbar-toggler(type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation")
%span.navbar-toggler-icon
#navbar.collapse.navbar-collapse
%ul.navbar-nav.mr-auto.mt-2.mt-lg-0
%li.nav-item
%a.nav-link(href="faq.html") FAQ
%li.nav-item
%a.nav-link(href="privacy.html") Privacy
%li.nav-item
%a.nav-link(href="https://source.loophabits.org") Source Code
%li.nav-item
%a.nav-link(href="https://translate.loophabits.org") Translate
%body
.content
:markdown
#{File.open("src/privacy.md").read}

14
landing/src/privacy.md Normal file
View File

@@ -0,0 +1,14 @@
## Privacy Policy
- All data provided to Loop Habit Tracker is only stored locally in your
device. Loop Habit Tracker does not upload your data anywhere. The
developers of Loop Habit Tracker do not have access to your data.
- Your data is not shared with any 3rd parties. Loop Habit Tracker does not
include any advertisement libraries or any 3rd party tracking (analytics)
code, such as Google Analytics or Facebook SDK.
- If you have activated "backup & reset" in your phone settings (Settings /
Backup & Reset / Back up my data), you should be aware that Android itself
will periodically save a copy of your phone's data in Google's servers. The
developers of Loop Habit Tracker do not have access to this data.

View File

@@ -32,12 +32,12 @@ tasks.compileLint {
android {
compileSdk = 32
compileSdk = 31
defaultConfig {
versionCode = 20101
versionName = "2.1.1"
minSdk = 28
versionCode = 20100
versionName = "2.1.0"
minSdk = 23
targetSdk = 31
applicationId = "org.isoron.uhabits"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -68,6 +68,12 @@ android {
}
}
lint {
isCheckReleaseBuilds = false
isAbortOnError = false
disable("GoogleAppIndexingWarning")
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
targetCompatibility(JavaVersion.VERSION_1_8)
@@ -80,25 +86,25 @@ android {
}
dependencies {
val daggerVersion = "2.44.2"
val kotlinVersion = "1.7.21"
val kxCoroutinesVersion = "1.6.4"
val daggerVersion = "2.41"
val kotlinVersion = "1.6.21"
val kxCoroutinesVersion = "1.6.1"
val ktorVersion = "1.6.8"
val espressoVersion = "3.5.0"
val espressoVersion = "3.4.0"
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
androidTestImplementation("com.google.dagger:dagger:$daggerVersion")
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1")
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
androidTestImplementation("androidx.annotation:annotation:1.5.0")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.annotation:annotation:1.3.0")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
androidTestImplementation("androidx.test:rules:1.5.0")
androidTestImplementation("androidx.test:rules:1.4.0")
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
compileOnly("javax.annotation:jsr250-api:1.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
implementation("com.github.AppIntro:AppIntro:6.2.0")
implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.google.dagger:dagger:$daggerVersion")
@@ -110,11 +116,11 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
implementation("androidx.appcompat:appcompat:1.5.1")
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("androidx.legacy:legacy-preference-v14:1.0.0")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("com.google.android.material:material:1.8.0")
implementation("com.opencsv:opencsv:5.7.1")
implementation("com.google.android.material:material:1.5.0")
implementation("com.opencsv:opencsv:5.6")
implementation(project(":uhabits-core"))
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")

View File

@@ -20,7 +20,6 @@ package org.isoron.uhabits.acceptance.steps
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
@@ -74,7 +73,7 @@ object CommonSteps : BaseUserInterfaceTest() {
}
fun offsetHeaders() {
device.swipe(500, 160, 350, 160, 20)
device.swipe(750, 160, 600, 160, 20)
}
fun scrollToText(text: String?) {

View File

@@ -53,7 +53,6 @@ object ListHabitsSteps {
clickViewWithId(R.id.action_filter)
CommonSteps.clickText(R.string.hide_completed)
}
else -> throw RuntimeException()
}
device.waitForIdle()
}

View File

@@ -270,7 +270,7 @@
<!-- Locale/Tasker -->
<receiver
android:name=".automation.FireSettingReceiver"
android:exported="true">
android:exported="false">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
</intent-filter>

View File

@@ -22,6 +22,7 @@ import android.animation.Keyframe;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.SuppressLint;
import android.os.Build;
import android.text.format.Time;
import android.view.View;
@@ -42,13 +43,17 @@ public class Utils {
static final String SHARED_PREFS_NAME = "com.android.calendar_preferences";
public static boolean isJellybeanOrLater() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
}
/**
* Try to speak the specified text, for accessibility. Only available on JB or later.
* @param text Text to announce.
*/
@SuppressLint("NewApi")
public static void tryAccessibilityAnnounce(View view, CharSequence text) {
if (view != null && text != null) {
if (isJellybeanOrLater() && view != null && text != null) {
view.announceForAccessibility(text);
}
}

View File

@@ -383,6 +383,10 @@ public abstract class DayPickerView extends ListView implements OnScrollListener
if (child instanceof MonthView) {
final CalendarDay focus = ((MonthView) child).getAccessibilityFocus();
if (focus != null) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
// Clear focus to avoid ListView bug in Jelly Bean MR1.
((MonthView) child).clearAccessibilityFocus();
}
return focus;
}
}

View File

@@ -35,7 +35,6 @@ 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.dismissCurrentAndShow
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.sres
@@ -118,7 +117,7 @@ class CheckmarkPopup(
view.unknownBtn.setOnClickListener { onClick(UNKNOWN) }
dialog.setCanceledOnTouchOutside(true)
dialog.dimBehind()
dialog.dismissCurrentAndShow()
dialog.show()
}
fun save() {

View File

@@ -28,7 +28,7 @@ import org.isoron.uhabits.utils.toPaletteColor
class ColorPickerDialog : ColorPickerDialog() {
fun setListener(callback: OnColorPickedCallback) {
super.setOnColorSelectedListener { c: Int ->
val pc = c.toPaletteColor(requireContext())
val pc = c.toPaletteColor(context!!)
callback.onColorPicked(pc)
}
}

View File

@@ -149,10 +149,8 @@ class FrequencyPickerDialog(
}
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())
numerator = Integer.parseInt(contentView.xTimesPerYDaysXTextView.text.toString())
denominator = Integer.parseInt(contentView.xTimesPerYDaysYTextView.text.toString())
}
}
else -> {

View File

@@ -19,7 +19,6 @@
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.platform.gui.AndroidDataView
@@ -50,13 +49,12 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
private var onDateClickedListener: OnDateClickedListener? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
clearCurrentDialog()
val component = (requireActivity().application as HabitsApplication).component
val component = (activity!!.application as HabitsApplication).component
commandRunner = component.commandRunner
habit = component.habitList.getById(requireArguments().getLong("habit"))!!
habit = component.habitList.getById(arguments!!.getLong("habit"))!!
preferences = component.preferences
val themeSwitcher = AndroidThemeSwitcher(requireActivity(), preferences)
val themeSwitcher = AndroidThemeSwitcher(activity!!, preferences)
themeSwitcher.apply()
chart = HistoryChart(
@@ -71,23 +69,15 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
padding = 10.0,
)
dataView = AndroidDataView(requireContext(), null)
dataView = AndroidDataView(context!!, null)
dataView.view = chart!!
val dialog = Dialog(requireContext()).apply {
return Dialog(context!!).apply {
val metrics = resources.displayMetrics
val maxHeight = resources.getDimensionPixelSize(R.dimen.history_editor_max_height)
setContentView(dataView)
window!!.setLayout(metrics.widthPixels, min(metrics.heightPixels, maxHeight))
}
currentDialog = dialog
return dialog
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
currentDialog = null
}
override fun onResume() {
@@ -121,14 +111,4 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
override fun onCommandFinished(command: Command) {
refreshData()
}
companion object {
// HistoryEditorDialog handles multiple dialogs on its own,
// because sometimes we want it to be shown under another dialog (e.g. NumberPopup)
var currentDialog: Dialog? = null
fun clearCurrentDialog() {
currentDialog?.dismiss()
currentDialog = null
}
}
}

View File

@@ -31,7 +31,6 @@ import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.dimBehind
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.requestFocusWithKeyboard
import java.text.DecimalFormat
@@ -74,6 +73,7 @@ class NumberPopup(
}
fun show() {
clearCurrentDialog()
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
dialog.setContentView(view.root)
dialog.window?.apply {
@@ -85,6 +85,7 @@ class NumberPopup(
}
dialog.setOnDismissListener {
onDismiss()
currentDialog = null
}
view.value.setOnKeyListener { _, keyCode, event ->
@@ -104,7 +105,8 @@ class NumberPopup(
view.value.requestFocusWithKeyboard()
dialog.setCanceledOnTouchOutside(true)
dialog.dimBehind()
dialog.dismissCurrentAndShow()
currentDialog = dialog
dialog.show()
}
fun save() {
@@ -113,4 +115,14 @@ class NumberPopup(
onToggle(value, notes)
dialog.dismiss()
}
companion object {
// Used to make sure that 2 popups aren't shown on top of each other
// If dialog that's already shown is detected, it's dismissed before the next one is opened.
private var currentDialog: Dialog? = null
fun clearCurrentDialog() {
currentDialog?.dismiss()
currentDialog = null
}
}
}

View File

@@ -60,7 +60,7 @@ class WeekdayPickerDialog :
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(
requireActivity()
activity!!
)
builder
.setTitle(R.string.select_weekdays)
@@ -73,7 +73,6 @@ class WeekdayPickerDialog :
.setNegativeButton(
android.R.string.cancel
) { _: DialogInterface?, _: Int -> dismiss() }
return builder.create()
}

View File

@@ -63,9 +63,7 @@ class FrequencyChart : ScrollableChart {
private var primaryColor = 0
private var isBackgroundTransparent = false
private lateinit var frequency: HashMap<Timestamp, Array<Int>>
private var maxFreq = 0
private var firstWeekday = Calendar.SUNDAY
private var isNumerical: Boolean = false
constructor(context: Context?) : super(context) {
init()
@@ -82,14 +80,8 @@ class FrequencyChart : ScrollableChart {
postInvalidate()
}
fun setIsNumerical(type: Boolean) {
isNumerical = type
postInvalidate()
}
fun setFrequency(frequency: java.util.HashMap<Timestamp, Array<Int>>) {
this.frequency = frequency
maxFreq = getMaxFreq(frequency)
postInvalidate()
}
@@ -98,15 +90,6 @@ class FrequencyChart : ScrollableChart {
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) {
this.isBackgroundTransparent = isBackgroundTransparent
initColors()
@@ -230,7 +213,7 @@ class FrequencyChart : ScrollableChart {
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!)
}
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?, weekdayFrequency: Int) {
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?, frequency: Int) {
// value can be negative when the entry is skipped
val valueCopy = value?.let { max(0, it) }
@@ -238,8 +221,8 @@ class FrequencyChart : ScrollableChart {
// maximal allowed mark radius
val maxRadius = (rect.height() - 2 * padding) / 2.0f
// the real mark radius is scaled down by a factor depending on the maximal frequency
val scalingFactor = if (isNumerical) maxFreq else weekdayFrequency
val scale = 1.0f / scalingFactor * valueCopy!!
val scale = 1.0f / frequency * valueCopy!!
val radius = maxRadius * scale
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
pGraph!!.color = colors[colorIndex]
@@ -302,6 +285,5 @@ class FrequencyChart : ScrollableChart {
frequency[Timestamp(date)] = values
date.add(Calendar.MONTH, -1)
}
maxFreq = getMaxFreq(frequency)
}
}

View File

@@ -59,7 +59,6 @@ import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.uhabits.databinding.ActivityEditHabitBinding
import org.isoron.uhabits.utils.ColorUtils
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.formatTime
import org.isoron.uhabits.utils.toFormattedString
@@ -157,23 +156,23 @@ class EditHabitActivity : AppCompatActivity() {
val colorPickerDialogFactory = ColorPickerDialogFactory(this)
binding.colorButton.setOnClickListener {
val picker = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
picker.setListener { paletteColor ->
val dialog = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
dialog.setListener { paletteColor ->
this.color = paletteColor
updateColors()
}
picker.dismissCurrentAndShow(supportFragmentManager, "colorPicker")
dialog.show(supportFragmentManager, "colorPicker")
}
populateFrequency()
binding.booleanFrequencyPicker.setOnClickListener {
val picker = FrequencyPickerDialog(freqNum, freqDen)
picker.onFrequencyPicked = { num, den ->
val dialog = FrequencyPickerDialog(freqNum, freqDen)
dialog.onFrequencyPicked = { num, den ->
freqNum = num
freqDen = den
populateFrequency()
}
picker.dismissCurrentAndShow(supportFragmentManager, "frequencyPicker")
dialog.show(supportFragmentManager, "frequencyPicker")
}
populateTargetType()
@@ -190,8 +189,7 @@ class EditHabitActivity : AppCompatActivity() {
populateTargetType()
dialog.dismiss()
}
val dialog = builder.create()
dialog.dismissCurrentAndShow()
builder.show()
}
binding.numericalFrequencyPicker.setOnClickListener {
@@ -237,7 +235,7 @@ class EditHabitActivity : AppCompatActivity() {
is24HourMode,
androidColor
)
dialog.dismissCurrentAndShow(supportFragmentManager, "timePicker")
dialog.show(supportFragmentManager, "timePicker")
}
binding.reminderDatePicker.setOnClickListener {
@@ -249,7 +247,7 @@ class EditHabitActivity : AppCompatActivity() {
populateReminder()
}
dialog.setSelectedDays(reminderDays)
dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker")
dialog.show(supportFragmentManager, "dayPicker")
}
binding.buttonSave.setOnClickListener {

View File

@@ -40,13 +40,13 @@ class HabitTypeDialog : AppCompatDialogFragment() {
val binding = SelectHabitTypeBinding.inflate(inflater, container, false)
binding.buttonYesNo.setOnClickListener {
val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.YES_NO.value)
val intent = IntentFactory().startEditActivity(activity!!, HabitType.YES_NO.value)
startActivity(intent)
dismiss()
}
binding.buttonMeasurable.setOnClickListener {
val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.NUMERICAL.value)
val intent = IntentFactory().startEditActivity(activity!!, HabitType.NUMERICAL.value)
startActivity(intent)
dismiss()
}

View File

@@ -21,7 +21,6 @@ package org.isoron.uhabits.activities.habits.list
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
@@ -85,6 +84,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this))
component.listHabitsBehavior.onStartup()
setContentView(rootView)
parseIntents()
}
override fun onPause() {
@@ -100,17 +100,11 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
rootView.postInvalidate()
midnightTimer.onResume()
taskRunner.run {
try {
AutoBackup(this@ListHabitsActivity).run()
appComponent.widgetUpdater.updateWidgets()
} catch (e: Exception) {
Log.e("ListHabitActivity", "TaskRunner failed", e)
}
AutoBackup(this@ListHabitsActivity).run()
}
if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) {
restartWithFade(ListHabitsActivity::class.java)
}
parseIntents()
super.onResume()
}
@@ -130,7 +124,6 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
}
private fun parseIntents() {
if (intent == null) return
if (intent.action == ACTION_EDIT) {
val habitId = intent.extras?.getLong("habit")
val timestamp = intent.extras?.getLong("timestamp")
@@ -139,12 +132,6 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
component.listHabitsBehavior.onEdit(habit, Timestamp(timestamp))
}
}
intent = null
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
}
companion object {

View File

@@ -64,7 +64,6 @@ import org.isoron.uhabits.tasks.ImportDataTask
import org.isoron.uhabits.tasks.ImportDataTaskFactory
import org.isoron.uhabits.utils.copyTo
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.restartWithFade
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendEmailScreen
@@ -164,7 +163,7 @@ class ListHabitsScreen
}
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) {
ConfirmDeleteDialog(activity, callback, quantity).dismissCurrentAndShow()
ConfirmDeleteDialog(activity, callback, quantity).show()
}
override fun showEditHabitsScreen(selected: List<Habit>) {
@@ -225,7 +224,7 @@ class ListHabitsScreen
override fun showColorPicker(defaultColor: PaletteColor, callback: OnColorPickedCallback) {
val picker = colorPickerFactory.create(defaultColor, themeSwitcher.currentTheme!!)
picker.setListener(callback)
picker.dismissCurrentAndShow(activity.supportFragmentManager, "picker")
picker.show(activity.supportFragmentManager, "picker")
}
override fun showNumberPopup(

View File

@@ -50,7 +50,6 @@ import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendFileScreen
import org.isoron.uhabits.widgets.WidgetUpdater
@@ -221,7 +220,6 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
ShowHabitMenuPresenter.Message.COULD_NOT_EXPORT -> {
showMessage(resources.getString(R.string.could_not_export))
}
else -> {}
}
}
@@ -230,7 +228,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
}
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) {
ConfirmDeleteDialog(this@ShowHabitActivity, callback, 1).dismissCurrentAndShow()
ConfirmDeleteDialog(this@ShowHabitActivity, callback, 1).show()
}
override fun close() {

View File

@@ -33,7 +33,6 @@ class FrequencyCardView(context: Context, attrs: AttributeSet) : LinearLayout(co
fun setState(state: FrequencyCardState) {
val androidColor = state.theme.color(state.color).toInt()
binding.frequencyChart.setFrequency(state.frequency)
binding.frequencyChart.setIsNumerical(state.isNumerical)
binding.frequencyChart.setFirstWeekday(state.firstWeekday)
binding.title.setTextColor(androidColor)
binding.frequencyChart.setColor(androidColor)

View File

@@ -22,7 +22,8 @@ import android.app.backup.BackupManager
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.provider.Settings
import android.util.Log
@@ -44,7 +45,6 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames
import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel
import org.isoron.uhabits.notifications.RingtoneManager
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.startActivitySafely
import org.isoron.uhabits.widgets.WidgetUpdater
import java.util.Calendar
@@ -65,7 +65,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.preferences)
val appContext = requireContext().applicationContext
val appContext = context!!.applicationContext
if (appContext is HabitsApplication) {
prefs = appContext.component.preferences
widgetUpdater = appContext.component.widgetUpdater
@@ -94,31 +94,24 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
override fun onPreferenceTreeClick(preference: Preference): Boolean {
val key = preference.key ?: return false
when (key) {
"reminderSound" -> {
showRingtonePicker()
return true
}
"reminderCustomize" -> {
createAndroidNotificationChannel(requireContext())
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID)
startActivity(intent)
return true
}
"rateApp" -> {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.playStoreURL)))
activity?.startActivitySafely(intent)
return true
}
if (key == "reminderSound") {
showRingtonePicker()
return true
} else if (key == "reminderCustomize") {
if (SDK_INT < Build.VERSION_CODES.O) return true
createAndroidNotificationChannel(context!!)
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context!!.packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID)
startActivity(intent)
return true
}
return super.onPreferenceTreeClick(preference)
}
override fun onResume() {
super.onResume()
ringtoneManager = RingtoneManager(requireActivity())
ringtoneManager = RingtoneManager(activity!!)
sharedPrefs = preferenceManager.sharedPreferences
sharedPrefs!!.registerOnSharedPreferenceChangeListener(this)
if (!prefs.isDeveloper) {
@@ -127,7 +120,11 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
}
updateWeekdayPreference()
findPreference("reminderSound").isVisible = false
if (SDK_INT < Build.VERSION_CODES.O)
findPreference("reminderCustomize").isVisible = false
else {
findPreference("reminderSound").isVisible = false
}
}
private fun updateWeekdayPreference() {
@@ -157,8 +154,8 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
val pref = findPreference(key)
pref.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
requireActivity().setResult(result)
requireActivity().finish()
activity!!.setResult(result)
activity!!.finish()
true
}
}

View File

@@ -21,16 +21,13 @@ package org.isoron.uhabits.intents
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_MUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getActivity
import android.app.PendingIntent.getBroadcast
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
import org.isoron.uhabits.activities.habits.show.ShowHabitActivity
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.Timestamp
@@ -92,20 +89,6 @@ class PendingIntentFactory
)
.getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)!!
fun showHabitTemplate(): PendingIntent {
return getActivity(
context,
0,
Intent(context, ShowHabitActivity::class.java),
getIntentTemplateFlags()
)
}
fun showHabitFillIn(habit: Habit) =
Intent().apply {
data = Uri.parse(habit.uriString)
}
fun showReminder(
habit: Habit,
reminderTime: Long?,
@@ -159,7 +142,7 @@ class PendingIntentFactory
fun showNumberPicker(habit: Habit, timestamp: Timestamp): PendingIntent? {
return getActivity(
context,
(habit.id!! % Integer.MAX_VALUE).toInt() + 1,
0,
Intent(context, ListHabitsActivity::class.java).apply {
action = ListHabitsActivity.ACTION_EDIT
putExtra("habit", habit.id)
@@ -168,43 +151,4 @@ class PendingIntentFactory
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
}
fun showNumberPickerTemplate(): PendingIntent {
return getActivity(
context,
1,
Intent(context, ListHabitsActivity::class.java).apply {
action = ListHabitsActivity.ACTION_EDIT
},
getIntentTemplateFlags()
)
}
fun showNumberPickerFillIn(habit: Habit, timestamp: Timestamp) = Intent().apply {
putExtra("habit", habit.id)
putExtra("timestamp", timestamp.unixTime)
}
private fun getIntentTemplateFlags(): Int {
var flags = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
flags = flags or FLAG_MUTABLE
}
return flags
}
fun toggleCheckmarkTemplate(): PendingIntent =
getBroadcast(
context,
2,
Intent(context, WidgetReceiver::class.java).apply {
action = WidgetReceiver.ACTION_TOGGLE_REPETITION
},
getIntentTemplateFlags()
)
fun toggleCheckmarkFillIn(habit: Habit, timestamp: Timestamp) = Intent().apply {
data = Uri.parse(habit.uriString)
putExtra("timestamp", timestamp.unixTime)
}
}

View File

@@ -168,12 +168,14 @@ class AndroidNotificationTray
fun createAndroidNotificationChannel(context: Context) {
val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE)
as NotificationManager
val channel = NotificationChannel(
REMINDERS_CHANNEL_ID,
context.resources.getString(R.string.reminder),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(channel)
if (SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
REMINDERS_CHANNEL_ID,
context.resources.getString(R.string.reminder),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(channel)
}
}
}
}

View File

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

View File

@@ -1,21 +0,0 @@
package org.isoron.uhabits.utils
import android.app.Dialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import java.lang.ref.WeakReference
var currentDialog: WeakReference<Dialog> = WeakReference(null)
fun Dialog.dismissCurrentAndShow() {
currentDialog.get()?.dismiss()
currentDialog = WeakReference(this)
show()
}
fun DialogFragment.dismissCurrentAndShow(fragmentManager: FragmentManager, tag: String) {
currentDialog.get()?.dismiss()
show(fragmentManager, tag)
fragmentManager.executePendingTransactions()
currentDialog = WeakReference(this.dialog)
}

View File

@@ -21,11 +21,18 @@ package org.isoron.uhabits.utils
import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.view.WindowManager
object SystemUtils {
fun unlockScreen(activity: Activity) {
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
km.requestDismissKeyguard(activity, null)
if (SDK_INT >= Build.VERSION_CODES.O) {
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
km.requestDismissKeyguard(activity, null)
} else {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
}
}
}

View File

@@ -21,7 +21,9 @@ package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import android.view.View
import androidx.annotation.RequiresApi
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
@@ -47,6 +49,7 @@ open class CheckmarkWidget(
}
}
@RequiresApi(Build.VERSION_CODES.O)
override fun refreshData(widgetView: View) {
(widgetView as CheckmarkWidgetView).apply {
val today = DateUtils.getTodayWithOffset()

View File

@@ -49,7 +49,6 @@ class FrequencyWidget(
(widgetView.dataView as FrequencyChart).apply {
setFirstWeekday(firstWeekday)
setColor(WidgetTheme().color(habit.color).toInt())
setIsNumerical(habit.isNumerical)
setFrequency(habit.originalEntries.computeWeekdayFrequency(habit.isNumerical))
}
}

View File

@@ -73,10 +73,6 @@ class StackWidget(
StackWidgetType.getStackWidgetAdapterViewId(widgetType),
StackWidgetType.getStackWidgetEmptyViewId(widgetType)
)
remoteViews.setPendingIntentTemplate(
StackWidgetType.getStackWidgetAdapterViewId(widgetType),
StackWidgetType.getPendingIntentTemplate(pendingIntentFactory, widgetType, habits)
)
return remoteViews
}
}

View File

@@ -29,14 +29,11 @@ import android.widget.RemoteViewsService
import android.widget.RemoteViewsService.RemoteViewsFactory
import org.isoron.platform.utils.StringUtils.Companion.splitLongs
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitNotFoundException
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.intents.PendingIntentFactory
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
import java.util.ArrayList
class StackWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
@@ -57,6 +54,7 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
)
private val habitIds: LongArray
private val widgetType: StackWidgetType
private var remoteViews = ArrayList<RemoteViews>()
override fun onCreate() {}
override fun onDestroy() {}
override fun getCount(): Int {
@@ -87,26 +85,8 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
}
override fun getViewAt(position: Int): RemoteViews? {
Log.i("StackRemoteViewsFactory", "getViewAt $position started")
if (position < 0 || position >= habitIds.size) return null
val app = context.applicationContext as HabitsApplication
val prefs = app.component.preferences
val habitList = app.component.habitList
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
if (Looper.myLooper() == null) Looper.prepare()
val habits = habitIds.map { habitList.getById(it) ?: throw HabitNotFoundException() }
val h = habits[position]
val widget = constructWidget(h, prefs)
widget.setDimensions(getDimensionsFromOptions(context, options))
val landscapeViews = widget.landscapeRemoteViews
val portraitViews = widget.portraitRemoteViews
val factory = PendingIntentFactory(context, IntentFactory())
val intent = StackWidgetType.getIntentFillIn(factory, widgetType, h, habits, getToday())
landscapeViews.setOnClickFillInIntent(R.id.button, intent)
portraitViews.setOnClickFillInIntent(R.id.button, intent)
val remoteViews = RemoteViews(landscapeViews, portraitViews)
Log.i("StackRemoteViewsFactory", "getViewAt $position ended")
return remoteViews
Log.i("StackRemoteViewsFactory", "getViewAt $position")
return if (0 <= position && position < remoteViews.size) remoteViews[position] else null
}
private fun constructWidget(
@@ -151,6 +131,24 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
}
override fun onDataSetChanged() {
Log.i("StackRemoteViewsFactory", "onDataSetChanged started")
val app = context.applicationContext as HabitsApplication
val prefs = app.component.preferences
val habitList = app.component.habitList
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
val newRemoteViews = ArrayList<RemoteViews>()
if (Looper.myLooper() == null) Looper.prepare()
for (id in habitIds) {
val h = habitList.getById(id) ?: throw HabitNotFoundException()
val widget = constructWidget(h, prefs)
widget.setDimensions(getDimensionsFromOptions(context, options))
val landscapeViews = widget.landscapeRemoteViews
val portraitViews = widget.portraitRemoteViews
newRemoteViews.add(RemoteViews(landscapeViews, portraitViews))
Log.i("StackRemoteViewsFactory", "onDataSetChanged constructed widget $id")
}
remoteViews = newRemoteViews
Log.i("StackRemoteViewsFactory", "onDataSetChanged ended")
}
init {

View File

@@ -18,12 +18,7 @@
*/
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Intent
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.intents.PendingIntentFactory
import java.lang.IllegalStateException
enum class StackWidgetType(val value: Int) {
@@ -78,39 +73,5 @@ enum class StackWidgetType(val value: Int) {
else -> throw IllegalStateException()
}
}
fun getPendingIntentTemplate(
factory: PendingIntentFactory,
widgetType: StackWidgetType,
habits: List<Habit>
): PendingIntent {
val containsNumerical = habits.any { it.isNumerical }
return when (widgetType) {
CHECKMARK -> if (containsNumerical) {
factory.showNumberPickerTemplate()
} else {
factory.toggleCheckmarkTemplate()
}
FREQUENCY, SCORE, HISTORY, STREAKS, TARGET -> factory.showHabitTemplate()
}
}
fun getIntentFillIn(
factory: PendingIntentFactory,
widgetType: StackWidgetType,
habit: Habit,
allHabitsInStackWidget: List<Habit>,
timestamp: Timestamp
): Intent {
val containsNumerical = allHabitsInStackWidget.any { it.isNumerical }
return when (widgetType) {
CHECKMARK -> if (containsNumerical) {
factory.showNumberPickerFillIn(habit, timestamp)
} else {
factory.toggleCheckmarkFillIn(habit, timestamp)
}
FREQUENCY, SCORE, HISTORY, STREAKS, TARGET -> factory.showHabitFillIn(habit)
}
}
}
}

View File

@@ -1,10 +1,6 @@
2.1.1:
* Fix Tasker plugin
2.1:
* Add notes to specific dates
* Track at-most measurable habits
* Add skips to measurable habits
* Bring back custom frequencies
* Other minor improvements and bug fixes
* Other minor improvements and bug fixes

View File

@@ -0,0 +1,39 @@
<!--
~ 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/>.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:top="1dp"
android:left="1dp"
android:bottom="3dp"
android:right="3dp">
<ripple
android:color="#60ffffff">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<corners android:radius="5dp"/>
<solid android:color="?android:colorPrimary"/>
</shape>
<color android:color="@color/white"/>
</item>
</ripple>
</item>
</layer-list>

View File

@@ -17,23 +17,18 @@
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:top="1dp"
android:left="1dp"
android:bottom="3dp"
android:right="3dp">
<ripple
android:color="#60ffffff">
<item android:id="@android:id/mask">
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:state_pressed="true">
<layer-list>
<item android:bottom="3dp"
android:left="1dp"
android:right="3dp"
android:top="1dp">
<shape android:shape="rectangle">
<corners android:radius="5dp"/>
<solid android:color="?android:colorPrimary"/>
<solid android:color="#30ffffff"/>
</shape>
<color android:color="@color/white"/>
</item>
</ripple>
</layer-list>
</item>
</layer-list>
</selector>

View File

@@ -21,5 +21,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
~
~ This file is part of Loop Habit Tracker.
~
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by the
~ Free Software Foundation, either version 3 of the License, or (at your
~ option) any later version.
~
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
</resources>

View File

@@ -160,9 +160,11 @@
</Preference>
<Preference
android:key="rateApp"
android:title="@string/pref_rate_this_app"
app:iconSpaceReserved="false">
<intent
android:action="android.intent.action.VIEW"
android:data="@string/playStoreURL" />
</Preference>
<Preference

View File

@@ -1,3 +0,0 @@
HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText
Caffeine,,Coffee Consumption,2022-11-21,80,
Caffeine,,Coffee Consumption,2022-11-22,80,
1 HabitName HabitDescription HabitCategory CalendarDate Value CommentText
2 Caffeine Coffee Consumption 2022-11-21 80
3 Caffeine Coffee Consumption 2022-11-22 80

View File

@@ -43,12 +43,13 @@ kotlin {
val jvmMain by getting {
dependencies {
implementation(kotlin("stdlib-jdk8"))
compileOnly("com.google.dagger:dagger:2.44.2")
compileOnly("com.google.dagger:dagger:2.41")
implementation("com.google.guava:guava:31.1-android")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4")
implementation("androidx.annotation:annotation:1.5.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.21")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.1")
implementation("androidx.annotation:annotation:1.3.0")
implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.opencsv:opencsv:5.7.1")
implementation("com.opencsv:opencsv:5.6")
implementation("commons-codec:commons-codec:1.15")
implementation("org.apache.commons:commons-lang3:3.12.0")
}
@@ -58,7 +59,7 @@ kotlin {
dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-junit"))
implementation("org.xerial:sqlite-jdbc:3.40.0.0")
implementation("org.xerial:sqlite-jdbc:3.36.0.3")
implementation("org.hamcrest:hamcrest:2.2")
implementation("org.apache.commons:commons-io:1.3.2")
implementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")

View File

@@ -25,12 +25,6 @@ class StringUtils {
fun joinLongs(values: LongArray): String = values.joinToString(separator = ",")
fun splitLongs(str: String): LongArray {
return try {
str.split(",").map { it.toLong() }.toLongArray()
} catch (e: NumberFormatException) {
LongArray(0)
}
}
fun splitLongs(str: String): LongArray = str.split(",").map { it.toLong() }.toLongArray()
}
}

View File

@@ -86,12 +86,10 @@ class HabitBullCSVImporter
logger.info("Found a value of $value, considering this habit as numerical.")
h.type = HabitType.NUMERICAL
}
h.originalEntries.add(Entry(timestamp, value * 1000, notes))
h.originalEntries.add(Entry(timestamp, value, notes))
}
}
}
map.forEach { (_, habit) -> habit.recompute() }
}
private fun parseTimestamp(rawValue: String): Timestamp {

View File

@@ -30,7 +30,6 @@ data class FrequencyCardState(
val firstWeekday: Int,
val frequency: HashMap<Timestamp, Array<Int>>,
val theme: Theme,
val isNumerical: Boolean
)
class FrequencyCardPresenter {
@@ -41,7 +40,6 @@ class FrequencyCardPresenter {
theme: Theme
) = FrequencyCardState(
color = habit.color,
isNumerical = habit.isNumerical,
frequency = habit.originalEntries.computeWeekdayFrequency(
isNumerical = habit.isNumerical
),

View File

@@ -92,7 +92,7 @@ class BarChart(
val r = round(barWidth * 0.15)
if (2 * r < barHeight) {
canvas.fillRect(x, y + r, barWidth, barHeight - r)
canvas.fillRect(x + r, y, barWidth - 2 * r, r + 1)
canvas.fillRect(x + r, y, barWidth - 2 * r, r)
canvas.fillCircle(x + r, y + r, r)
canvas.fillCircle(x + barWidth - r, y + r, r)
} else {

View File

@@ -85,8 +85,8 @@ class ImportTest : BaseUnitTest() {
assertThat(habit.type, equalTo(HabitType.NUMERICAL))
assertThat(habit.description, equalTo(""))
assertThat(habit.frequency, equalTo(Frequency.DAILY))
assertThat(getValue(habit, 2021, 9, 1), equalTo(30000))
assertThat(getValue(habit, 2022, 1, 8), equalTo(100000))
assertThat(getValue(habit, 2021, 9, 1), equalTo(30))
assertThat(getValue(habit, 2022, 1, 8), equalTo(100))
val habit2 = habitList.getByPosition(1)
assertThat(habit2.name, equalTo("run"))
@@ -98,21 +98,6 @@ class ImportTest : BaseUnitTest() {
assertTrue(isChecked(habit2, 2022, 1, 19))
}
@Test
@Throws(IOException::class)
fun testHabitBullCSV4() {
importFromFile("habitbull4.csv")
assertThat(habitList.size(), equalTo(1))
val habit = habitList.getByPosition(0)
assertThat(habit.name, equalTo("Caffeine"))
assertThat(habit.type, equalTo(HabitType.NUMERICAL))
assertThat(habit.description, equalTo(""))
assertThat(habit.frequency, equalTo(Frequency.DAILY))
assertThat(getValue(habit, 2022, 11, 21), equalTo(80000))
assertThat(getValue(habit, 2022, 11, 22), equalTo(80000))
}
@Test
@Throws(IOException::class)
fun testLoopDB() {

View File

@@ -34,8 +34,8 @@ application {
dependencies {
val ktorVersion = "1.6.8"
val kotlinVersion = "1.7.21"
val logbackVersion = "1.4.5"
val kotlinVersion = "1.6.21"
val logbackVersion = "1.2.11"
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("io.ktor:ktor-server-netty:$ktorVersion")
implementation("ch.qos.logback:logback-classic:$logbackVersion")
@@ -43,9 +43,9 @@ dependencies {
implementation("io.ktor:ktor-html-builder:$ktorVersion")
implementation("io.ktor:ktor-jackson:$ktorVersion")
implementation("org.jetbrains:kotlin-css-jvm:1.0.0-pre.148-kotlin-1.4.30")
implementation("io.prometheus:simpleclient:0.16.0")
implementation("io.prometheus:simpleclient_httpserver:0.16.0")
implementation("io.prometheus:simpleclient_hotspot:0.16.0")
implementation("io.prometheus:simpleclient:0.15.0")
implementation("io.prometheus:simpleclient_httpserver:0.15.0")
implementation("io.prometheus:simpleclient_hotspot:0.15.0")
testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
testImplementation(kotlin("test"))