Compare commits

..

7 Commits

Author SHA1 Message Date
Jakub Kalinowski
e189d17fd5 Removing unrelated changes 2022-08-14 12:24:44 +02:00
Jakub Kalinowski
30c39853e9 Fixed double invocation of show() and target type dialog 2022-08-14 01:30:02 +02:00
Jakub Kalinowski
c1fa6c46ee Changed the approach, using an extension method 2022-08-14 01:09:36 +02:00
Jakub Kalinowski
6e4ed3837f One last ktlint problem 2022-08-13 18:25:49 +02:00
Jakub Kalinowski
cb65604581 Code style fixes 2022-08-13 18:21:13 +02:00
Jakub Kalinowski
2db4491328 Handling multiple dialogs correctly 2022-08-13 18:16:11 +02:00
Jakub Kalinowski
25a3509988 Reimplementing the multiple popups handling in the new popup solution. 2022-08-13 14:39:01 +02:00
286 changed files with 1654 additions and 1777 deletions

View File

@@ -4,8 +4,3 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "monthly" interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

View File

@@ -9,20 +9,19 @@ on:
jobs: jobs:
Test: Test:
runs-on: self-hosted runs-on: self-hosted
timeout-minutes: 30
steps: steps:
- name: Check out source code - name: Check out source code
uses: actions/checkout@v4 uses: actions/checkout@v1
- name: Build project - name: Build project
run: ./build.sh build run: ./build.sh build
- name: Run Android tests - name: Run Android tests
run: ./build.sh android-tests-parallel 28 29 30 32 33 34 run: ./build.sh android-tests-parallel 23 24 25 26 27 28 30 31
- name: Upload artifacts - name: Upload artifacts
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v2
with: with:
name: build name: build
path: | path: |

6
.gitignore vendored
View File

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

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,42 +1,6 @@
# Changelog # Changelog
## [2.2.0] -- 2024-01-30 ## [2.1.0] -- Unreleased
### Added
- Add support for Android 14 (@iSoron, @hiqua)
- Allow user to change app language (@leondzn)
### Fixed
- Implement workaround to make notifications non-dismissible in Android 14 (@iSoron, #1872)
- Fix splash screen background color in dark mode (@SIKV, #1888)
## [2.1.3] -- 2023-08-28
### Fixed
- Use text input on Samsung devices (@iSoron, #1719)
- Prevent crash if alarm permission is revoked (@iSoron)
- Adjust widget colors (@iSoron)
- Fix bug preventing screens from updating at midnight (@iSoron)
- Fix skip button in locales that use comma instead of dot (@iSoron, #1721)
## [2.1.2] -- 2023-05-26
### Fixed
- Fix bug that caused widget to enter checkmark on wrong date (@iSoron, #1541)
- Fix widget corners on Android 12 (@iSoron)
- Fix bug that caused notes to be lost when editing a checkmark (@iSoron, #1566)
- Prevent soft keyboard from covering entry popup (@iSoron)
- Accept comma (instead of dot) in certain locales (@iSoron)
### Changed
- Remove update delay after entering a checkmark (@iSoron)
### Removed
- Remove stack widgets (@iSoron)
## [2.1.1] -- 2022-09-24
### Fixed
- Fix Tasker plugin (@iSoron, #1503)
## [2.1.0] -- 2022-09-10
### Added ### Added
- Allow user to add notes to specific dates (@vbh, #1103) - Allow user to add notes to specific dates (@vbh, #1103)
- Allow user to track "at most" numerical habits (@KristianTashkov, #1101) - Allow user to track "at most" numerical habits (@KristianTashkov, #1101)
@@ -45,7 +9,6 @@
- Improve number picker (@hiqua, @iSoron, #1082, #1370) - Improve number picker (@hiqua, @iSoron, #1082, #1370)
- Add new checkmark and number picker (@iSoron, #1370) - Add new checkmark and number picker (@iSoron, #1370)
- Allow user to import numerical habits from HabitBull (@hiqua, #1278) - Allow user to import numerical habits from HabitBull (@hiqua, #1278)
- Add support for Android 13 themed icons (@cheeeeer, #1497)
### Removed ### Removed
- Hide snooze button Android 12 notifications (@hiqua, #1226) - Hide snooze button Android 12 notifications (@hiqua, #1226)
@@ -65,7 +28,6 @@
- Fix small issues in calendar chart (@kalina559, #1314) - Fix small issues in calendar chart (@kalina559, #1314)
- Resort habit list after edit (@hiqua, #1350) - Resort habit list after edit (@hiqua, #1350)
- Fix marker scaling in frequency display (@eduebernal, #1425) - Fix marker scaling in frequency display (@eduebernal, #1425)
- Fix widgets not working correctly on API 33 (@iSoron, #1488)
### Refactoring & Testing ### Refactoring & Testing
- Replace raster icons by vector assets (@kalina559) - Replace raster icons by vector assets (@kalina559)

View File

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

View File

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

View File

@@ -85,10 +85,10 @@ android_setup() {
$AVDMANAGER delete avd --name $AVDNAME $AVDMANAGER delete avd --name $AVDNAME
log_info "Creating new Android virtual device (API $API)..." 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 \ $AVDMANAGER create avd \
--name $AVDNAME \ --name $AVDNAME \
--package "system-images;android-$API;google_apis;x86_64" \ --package "system-images;android-$API;default;x86_64" \
--device "Nexus 4" || return 1 --device "Nexus 4" || return 1
flock -u 10 flock -u 10
@@ -181,7 +181,7 @@ android_test() {
OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt
OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt
FAILED_TESTS="" FAILED_TESTS=""
for i in {1..10}; do for i in {1..5}; do
log_info "Running $size instrumented tests (attempt $i)..." log_info "Running $size instrumented tests (attempt $i)..."
$ADB shell am instrument \ $ADB shell am instrument \
-r -e coverage true -e size "$size" $FAILED_TESTS \ -r -e coverage true -e size "$size" $FAILED_TESTS \
@@ -217,28 +217,20 @@ android_test_parallel() {
( (
LOG=build/android-test-$API.log LOG=build/android-test-$API.log
log_info "API $API: Running tests..." log_info "API $API: Running tests..."
android_test $API 1>$LOG 2>&1 if android_test $API 1>$LOG 2>&1; then
ret_code=$?
if [ $ret_code = 0 ]; then
log_info "API $API: Passed" log_info "API $API: Passed"
else else
log_error "API $API: Failed" log_error "API $API: Failed"
fi fi
pkill -9 -f ${AVD_PREFIX}${API} pkill -9 -f ${AVD_PREFIX}${API}
exit $ret_code
)& )&
PIDS+=" $!" PIDS+=" $!"
done done
# Check exit codes # Check exit codes
success=0 RET_CODE=0
for pid in $PIDS; do for pid in $PIDS; do
wait $pid wait $pid || RET_CODE=1
ret_code=$?
if [ $ret_code != 0 ]; then
success=1
fi
echo pid=$pid ret_code=$ret_code success=$success
done done
# Print all logs # Print all logs
@@ -248,7 +240,7 @@ android_test_parallel() {
echo "::endgroup::" echo "::endgroup::"
done done
return $success return $RET_CODE
} }
android_build() { android_build() {

View File

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

View File

@@ -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: 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. * **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.
- **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. * **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. ./build.sh build
2. Run `./build.sh android-tests API` to run the tests on a single API. ./build.sh medium-tests
3. Run `./build.sh android-tests-parallel API API...` to run the tests on multiple APIs in parallel.
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; 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.
- 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.
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

@@ -3,6 +3,3 @@ org.gradle.daemon=true
org.gradle.jvmargs=-Xms2048m -Xmx2048m org.gradle.jvmargs=-Xms2048m -Xmx2048m
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

@@ -18,10 +18,11 @@
*/ */
plugins { plugins {
id("com.github.triplet.play") version "3.8.6" id("com.github.triplet.play") version "3.7.0"
id("com.android.application") version "8.1.4" id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt") id("org.jetbrains.kotlin.kapt")
id("org.jetbrains.kotlin.android.extensions")
id("org.jlleitschuh.gradle.ktlint") id("org.jlleitschuh.gradle.ktlint")
} }
@@ -29,27 +30,15 @@ tasks.compileLint {
dependsOn("updateTranslators") dependsOn("updateTranslators")
} }
/*
Added on top of kotlinOptions to work around this issue:
https://youtrack.jetbrains.com/issue/KTIJ-24311/task-current-target-is-17-and-kaptGenerateStubsProductionDebugKotlin-task-current-target-is-1.8-jvm-target-compatibility-should#focus=Comments-27-6798448.0-0
Updating gradle might fix this, so try again in the future to remove this and run:
./gradlew --rerun-tasks :uhabits-android:kaptGenerateStubsReleaseKotlin
If this doesn't produce any warning, try to remove it.
*/
kotlin {
jvmToolchain(11)
}
android { android {
namespace = "org.isoron.uhabits" compileSdk = 31
compileSdk = 34
defaultConfig { defaultConfig {
versionCode = 20200 versionCode = 20100
versionName = "2.2.0" versionName = "2.1.0"
minSdk = 28 minSdk = 23
targetSdk = 34 targetSdk = 31
applicationId = "org.isoron.uhabits" applicationId = "org.isoron.uhabits"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -79,13 +68,16 @@ android {
} }
} }
lint {
isCheckReleaseBuilds = false
isAbortOnError = false
disable("GoogleAppIndexingWarning")
}
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
targetCompatibility(JavaVersion.VERSION_11) targetCompatibility(JavaVersion.VERSION_1_8)
sourceCompatibility(JavaVersion.VERSION_11) sourceCompatibility(JavaVersion.VERSION_1_8)
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
} }
buildFeatures { buildFeatures {
@@ -94,29 +86,29 @@ android {
} }
dependencies { dependencies {
val daggerVersion = "2.51.1" val daggerVersion = "2.41"
val kotlinVersion = "1.9.22" val kotlinVersion = "1.6.21"
val kxCoroutinesVersion = "1.7.3" val kxCoroutinesVersion = "1.6.1"
val ktorVersion = "1.6.8" val ktorVersion = "1.6.8"
val espressoVersion = "3.5.1" val espressoVersion = "3.4.0"
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion") androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion") androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
androidTestImplementation("com.google.dagger:dagger:$daggerVersion") androidTestImplementation("com.google.dagger:dagger:$daggerVersion")
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3") androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1")
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion") androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion") androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
androidTestImplementation("androidx.annotation:annotation:1.7.1") androidTestImplementation("androidx.annotation:annotation:1.3.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5") 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.5.0") androidTestImplementation("androidx.test:rules:1.4.0")
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") 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:2.0.4") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
implementation("com.github.AppIntro:AppIntro:6.3.1") implementation("com.github.AppIntro:AppIntro:6.2.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:33.1.0-android") implementation("com.google.guava:guava:31.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")
@@ -124,18 +116,17 @@ 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.6.1") implementation("androidx.appcompat:appcompat:1.4.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.11.0") implementation("com.google.android.material:material:1.5.0")
implementation("com.opencsv:opencsv:5.9") implementation("com.opencsv:opencsv:5.6")
implementation("nl.dionsegijn:konfetti-xml:2.0.2")
implementation(project(":uhabits-core")) implementation(project(":uhabits-core"))
kapt("com.google.dagger:dagger-compiler:$daggerVersion") kapt("com.google.dagger:dagger-compiler:$daggerVersion")
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion") kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
testImplementation("com.google.dagger:dagger:$daggerVersion") testImplementation("com.google.dagger:dagger:$daggerVersion")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
} }
kapt { kapt {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

View File

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

View File

@@ -32,11 +32,11 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import junit.framework.Assert.assertTrue
import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.isoron.uhabits.BaseUserInterfaceTest import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.list.ListHabitsActivity import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
import org.junit.Assert.assertTrue
object CommonSteps : BaseUserInterfaceTest() { object CommonSteps : BaseUserInterfaceTest() {
fun pressBack() { fun pressBack() {
@@ -73,7 +73,7 @@ object CommonSteps : BaseUserInterfaceTest() {
} }
fun offsetHeaders() { fun offsetHeaders() {
device.swipe(500, 160, 350, 160, 20) device.swipe(750, 160, 600, 160, 20)
} }
fun scrollToText(text: String?) { fun scrollToText(text: String?) {
@@ -147,19 +147,15 @@ object CommonSteps : BaseUserInterfaceTest() {
Screen.LIST_HABITS -> Screen.LIST_HABITS ->
Espresso.onView(ViewMatchers.withClassName(CoreMatchers.endsWith("ListHabitsRootView"))) Espresso.onView(ViewMatchers.withClassName(CoreMatchers.endsWith("ListHabitsRootView")))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed())) .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Screen.SHOW_HABIT -> Screen.SHOW_HABIT ->
Espresso.onView(ViewMatchers.withId(R.id.subtitleCard)) Espresso.onView(ViewMatchers.withId(R.id.subtitleCard))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed())) .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Screen.EDIT_HABIT -> Screen.EDIT_HABIT ->
Espresso.onView(ViewMatchers.withId(R.id.questionInput)) Espresso.onView(ViewMatchers.withId(R.id.questionInput))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed())) .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Screen.SELECT_HABIT_TYPE -> Screen.SELECT_HABIT_TYPE ->
Espresso.onView(ViewMatchers.withText(R.string.yes_or_no_example)) Espresso.onView(ViewMatchers.withText(R.string.yes_or_no_example))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed())) .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }

View File

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

View File

@@ -21,9 +21,9 @@ package org.isoron.uhabits.acceptance.steps
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
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.assertTrue
import org.isoron.uhabits.BaseUserInterfaceTest import org.isoron.uhabits.BaseUserInterfaceTest
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
object WidgetSteps { object WidgetSteps {
@Throws(Exception::class) @Throws(Exception::class)

View File

@@ -18,7 +18,6 @@
*/ */
package org.isoron.uhabits.activities.common.views package org.isoron.uhabits.activities.common.views
import android.view.MotionEvent
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.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
@@ -53,8 +52,7 @@ class FrequencyChartTest : BaseViewTest() {
@Test @Test
@Throws(Throwable::class) @Throws(Throwable::class)
fun testRender_withDataOffset() { fun testRender_withDataOffset() {
val e = MotionEvent.obtain(0, 0, 0, 0f, 0f, 0) view.onScroll(null, null, -dpToPixels(150), 0f)
view.onScroll(e, e, -dpToPixels(150), 0f)
view.invalidate() view.invalidate()
assertRenders(view, BASE_PATH + "renderDataOffset.png") assertRenders(view, BASE_PATH + "renderDataOffset.png")
} }

View File

@@ -18,7 +18,6 @@
*/ */
package org.isoron.uhabits.activities.common.views package org.isoron.uhabits.activities.common.views
import android.view.MotionEvent
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.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
@@ -45,7 +44,7 @@ class ScoreChartTest : BaseViewTest() {
habit = habit, habit = habit,
firstWeekday = prefs.firstWeekdayInt, firstWeekday = prefs.firstWeekdayInt,
spinnerPosition = 0, spinnerPosition = 0,
theme = LightTheme() theme = LightTheme(),
) )
view = ScoreChart(targetContext).apply { view = ScoreChart(targetContext).apply {
setScores(state.scores) setScores(state.scores)
@@ -64,8 +63,7 @@ class ScoreChartTest : BaseViewTest() {
@Test @Test
@Throws(Throwable::class) @Throws(Throwable::class)
fun testRender_withDataOffset() { fun testRender_withDataOffset() {
val e = MotionEvent.obtain(0, 0, 0, 0f, 0f, 0) view.onScroll(null, null, -dpToPixels(150), 0f)
view.onScroll(e, e, -dpToPixels(150), 0f)
view.invalidate() view.invalidate()
assertRenders(view, BASE_PATH + "renderDataOffset.png") assertRenders(view, BASE_PATH + "renderDataOffset.png")
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,11 +33,9 @@ import java.io.IOException
@MediumTest @MediumTest
class CheckmarkWidgetViewTest : BaseViewTest() { class CheckmarkWidgetViewTest : BaseViewTest() {
private lateinit var view: CheckmarkWidgetView private lateinit var view: CheckmarkWidgetView
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
similarityCutoff = 0.00025
setTheme(R.style.WidgetTheme) setTheme(R.style.WidgetTheme)
val habit = fixtures.createShortHabit() val habit = fixtures.createShortHabit()
val computedEntries = habit.computedEntries val computedEntries = habit.computedEntries

View File

@@ -16,13 +16,13 @@
~ You should have received a copy of the GNU General Public License along ~ You should have received a copy of the GNU General Public License along
~ 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"
package="org.isoron.uhabits">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_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" />
<application <application
android:name=".HabitsApplication" android:name=".HabitsApplication"
@@ -30,7 +30,6 @@
android:backupAgent=".HabitsBackupAgent" android:backupAgent=".HabitsBackupAgent"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/main_activity_title" android:label="@string/main_activity_title"
android:localeConfig="@xml/locales_config"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppBaseTheme"> android:theme="@style/AppBaseTheme">
@@ -271,7 +270,7 @@
<!-- Locale/Tasker --> <!-- Locale/Tasker -->
<receiver <receiver
android:name=".automation.FireSettingReceiver" android:name=".automation.FireSettingReceiver"
android:exported="true"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" /> <action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
</intent-filter> </intent-filter>

View File

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

View File

@@ -22,6 +22,7 @@ import java.util.Locale;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.util.AttributeSet; import android.util.AttributeSet;
@@ -382,6 +383,10 @@ public abstract class DayPickerView extends ListView implements OnScrollListener
if (child instanceof MonthView) { if (child instanceof MonthView) {
final CalendarDay focus = ((MonthView) child).getAccessibilityFocus(); final CalendarDay focus = ((MonthView) child).getAccessibilityFocus();
if (focus != null) { 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; return focus;
} }
} }

View File

@@ -23,13 +23,14 @@ import android.graphics.Paint.*;
import android.os.*; import android.os.*;
import androidx.core.view.*; import androidx.core.view.*;
import androidx.core.view.accessibility.*; import androidx.core.view.accessibility.*;
import androidx.core.widget.*;
import android.text.format.*; import android.text.format.*;
import android.view.*; import android.view.*;
import android.view.accessibility.*; import android.view.accessibility.*;
import androidx.customview.widget.ExploreByTouchHelper; import androidx.customview.widget.ExploreByTouchHelper;
import com.android.*;
import com.android.datetimepicker.*; import com.android.datetimepicker.*;
import com.android.datetimepicker.date.MonthAdapter.*; import com.android.datetimepicker.date.MonthAdapter.*;

View File

@@ -23,6 +23,7 @@ import android.graphics.Paint.*;
import android.util.*; import android.util.*;
import android.view.*; import android.view.*;
import com.android.*;
import com.android.datetimepicker.*; import com.android.datetimepicker.*;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;

View File

@@ -28,6 +28,7 @@ import android.view.View.*;
import android.view.accessibility.*; import android.view.accessibility.*;
import android.widget.*; import android.widget.*;
import com.android.*;
import com.android.datetimepicker.*; import com.android.datetimepicker.*;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;

View File

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

View File

@@ -33,7 +33,7 @@ import kotlin.math.max
*/ */
class AndroidDataView( class AndroidDataView(
context: Context, context: Context,
attrs: AttributeSet? = null attrs: AttributeSet? = null,
) : AndroidView<DataView>(context, attrs), ) : AndroidView<DataView>(context, attrs),
GestureDetector.OnGestureListener, GestureDetector.OnGestureListener,
ValueAnimator.AnimatorUpdateListener { ValueAnimator.AnimatorUpdateListener {
@@ -44,23 +44,23 @@ class AndroidDataView(
addUpdateListener(this@AndroidDataView) addUpdateListener(this@AndroidDataView)
} }
override fun onTouchEvent(event: MotionEvent) = detector.onTouchEvent(event) override fun onTouchEvent(event: MotionEvent?) = detector.onTouchEvent(event)
override fun onDown(e: MotionEvent) = true override fun onDown(e: MotionEvent?) = true
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) return handleClick(e, true)
} }
override fun onLongPress(e: MotionEvent) { override fun onLongPress(e: MotionEvent?) {
handleClick(e) handleClick(e)
} }
override fun onScroll( override fun onScroll(
e1: MotionEvent?, e1: MotionEvent?,
e2: MotionEvent, e2: MotionEvent?,
dx: Float, dx: Float,
dy: Float dy: Float,
): Boolean { ): Boolean {
if (abs(dx) > abs(dy)) { if (abs(dx) > abs(dy)) {
val parent = parent val parent = parent
@@ -80,9 +80,9 @@ class AndroidDataView(
override fun onFling( override fun onFling(
e1: MotionEvent?, e1: MotionEvent?,
e2: MotionEvent, e2: MotionEvent?,
velocityX: Float, velocityX: Float,
velocityY: Float velocityY: Float,
): Boolean { ): Boolean {
scroller.fling( scroller.fling(
scroller.currX, scroller.currX,
@@ -100,7 +100,7 @@ class AndroidDataView(
return false return false
} }
override fun onAnimationUpdate(animation: ValueAnimator) { override fun onAnimationUpdate(animation: ValueAnimator?) {
if (!scroller.isFinished) { if (!scroller.isFinished) {
scroller.computeScrollOffset() scroller.computeScrollOffset()
updateDataOffset() updateDataOffset()
@@ -127,11 +127,11 @@ class AndroidDataView(
} }
} }
private fun handleClick(e: MotionEvent, isSingleTap: Boolean = false): Boolean { private fun handleClick(e: MotionEvent?, isSingleTap: Boolean = false): Boolean {
val x: Float val x: Float
val y: Float val y: Float
try { try {
val pointerId = e.getPointerId(0) val pointerId = e!!.getPointerId(0)
x = e.getX(pointerId) x = e.getX(pointerId)
y = e.getY(pointerId) y = e.getY(pointerId)
} catch (ex: RuntimeException) { } catch (ex: RuntimeException) {
@@ -140,11 +140,8 @@ class AndroidDataView(
// e.getPointerId. // e.getPointerId.
return false return false
} }
if (isSingleTap) { if (isSingleTap) view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity)
view?.onClick(x / canvas.innerDensity, y / canvas.innerDensity) else view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
} else {
view?.onLongClick(x / canvas.innerDensity, y / canvas.innerDensity)
}
return true return true
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,10 @@
*/ */
package org.isoron.uhabits.activities.common.dialogs package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.os.Bundle
import com.android.colorpicker.ColorPickerDialog import com.android.colorpicker.ColorPickerDialog
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
import org.isoron.uhabits.utils.toPaletteColor import org.isoron.uhabits.utils.toPaletteColor
@@ -28,8 +31,14 @@ import org.isoron.uhabits.utils.toPaletteColor
class ColorPickerDialog : ColorPickerDialog() { class ColorPickerDialog : ColorPickerDialog() {
fun setListener(callback: OnColorPickedCallback) { fun setListener(callback: OnColorPickedCallback) {
super.setOnColorSelectedListener { c: Int -> super.setOnColorSelectedListener { c: Int ->
val pc = c.toPaletteColor(requireContext()) val pc = c.toPaletteColor(context!!)
callback.onColorPicked(pc) callback.onColorPicked(pc)
} }
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.dismissCurrent()
return dialog
}
} }

View File

@@ -22,6 +22,7 @@ import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
@@ -45,5 +46,7 @@ class ConfirmDeleteDialog(
BUTTON_NEGATIVE, BUTTON_NEGATIVE,
res.getString(R.string.no) res.getString(R.string.no)
) { dialog: DialogInterface?, which: Int -> } ) { dialog: DialogInterface?, which: Int -> }
this.dismissCurrent()
} }
} }

View File

@@ -22,103 +22,102 @@ 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.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RadioButton import android.widget.RadioButton
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import kotlinx.android.synthetic.main.frequency_picker_dialog.view.*
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.databinding.FrequencyPickerDialogBinding import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
class FrequencyPickerDialog( class FrequencyPickerDialog(
var freqNumerator: Int, var freqNumerator: Int,
var freqDenominator: Int var freqDenominator: Int
) : AppCompatDialogFragment() { ) : AppCompatDialogFragment() {
private var _binding: FrequencyPickerDialogBinding? = null
private val binding get() = _binding!!
lateinit var contentView: View
var onFrequencyPicked: (num: Int, den: Int) -> Unit = { _, _ -> } var onFrequencyPicked: (num: Int, den: Int) -> Unit = { _, _ -> }
constructor() : this(1, 1) constructor() : this(1, 1)
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = FrequencyPickerDialogBinding.inflate(LayoutInflater.from(requireActivity())) val inflater = LayoutInflater.from(requireActivity())
contentView = inflater.inflate(R.layout.frequency_picker_dialog, null)
addBeforeAfterText( addBeforeAfterText(
this.getString(R.string.every_x_days), this.getString(R.string.every_x_days),
binding.everyXDaysContainer contentView.everyXDaysContainer,
) )
addBeforeAfterText( addBeforeAfterText(
this.getString(R.string.x_times_per_week), this.getString(R.string.x_times_per_week),
binding.xTimesPerWeekContainer contentView.xTimesPerWeekContainer,
) )
addBeforeAfterText( addBeforeAfterText(
this.getString(R.string.x_times_per_month), this.getString(R.string.x_times_per_month),
binding.xTimesPerMonthContainer contentView.xTimesPerMonthContainer,
) )
addBeforeAfterText( addBeforeAfterText(
this.getString(R.string.x_times_per_y_days), this.getString(R.string.x_times_per_y_days),
binding.xTimesPerYDaysContainer contentView.xTimesPerYDaysContainer,
) )
binding.everyDayRadioButton.setOnClickListener { contentView.everyDayRadioButton.setOnClickListener {
check(binding.everyDayRadioButton) check(contentView.everyDayRadioButton)
} }
binding.everyXDaysRadioButton.setOnClickListener { contentView.everyXDaysRadioButton.setOnClickListener {
check(binding.everyXDaysRadioButton) check(contentView.everyXDaysRadioButton)
val everyXDaysTextView = binding.everyXDaysTextView val everyXDaysTextView = contentView.everyXDaysTextView
selectInputField(everyXDaysTextView) selectInputField(everyXDaysTextView)
} }
binding.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus -> contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) check(binding.everyXDaysRadioButton) if (hasFocus) check(contentView.everyXDaysRadioButton)
} }
binding.xTimesPerWeekRadioButton.setOnClickListener { contentView.xTimesPerWeekRadioButton.setOnClickListener {
check(binding.xTimesPerWeekRadioButton) check(contentView.xTimesPerWeekRadioButton)
selectInputField(binding.xTimesPerWeekTextView) selectInputField(contentView.xTimesPerWeekTextView)
} }
binding.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus -> contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) check(binding.xTimesPerWeekRadioButton) if (hasFocus) check(contentView.xTimesPerWeekRadioButton)
} }
binding.xTimesPerMonthRadioButton.setOnClickListener { contentView.xTimesPerMonthRadioButton.setOnClickListener {
check(binding.xTimesPerMonthRadioButton) check(contentView.xTimesPerMonthRadioButton)
selectInputField(binding.xTimesPerMonthTextView) selectInputField(contentView.xTimesPerMonthTextView)
} }
binding.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus -> contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) check(binding.xTimesPerMonthRadioButton) if (hasFocus) check(contentView.xTimesPerMonthRadioButton)
} }
binding.xTimesPerYDaysRadioButton.setOnClickListener { contentView.xTimesPerYDaysRadioButton.setOnClickListener {
check(binding.xTimesPerYDaysRadioButton) check(contentView.xTimesPerYDaysRadioButton)
selectInputField(binding.xTimesPerYDaysXTextView) selectInputField(contentView.xTimesPerYDaysXTextView)
} }
binding.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus -> contentView.xTimesPerYDaysXTextView.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) check(binding.xTimesPerYDaysRadioButton) if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
} }
binding.xTimesPerYDaysYTextView.setOnFocusChangeListener { v, hasFocus -> contentView.xTimesPerYDaysYTextView.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) check(binding.xTimesPerYDaysRadioButton) if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
} }
return AlertDialog.Builder(requireActivity()) val dialog = AlertDialog.Builder(requireActivity())
.setView(binding.root) .setView(contentView)
.setPositiveButton(R.string.save) { _, _ -> onSaveClicked() } .setPositiveButton(R.string.save) { _, _ -> onSaveClicked() }
.create() .create()
dialog.dismissCurrent()
return dialog
} }
private fun addBeforeAfterText( private fun addBeforeAfterText(
@@ -128,8 +127,7 @@ class FrequencyPickerDialog(
val parts = str.split("%d") val parts = str.split("%d")
for (i in parts.indices) { for (i in parts.indices) {
container.addView( container.addView(
TextView(activity).apply { text = parts[i].trim() }, TextView(activity).apply { text = parts[i].trim() }, 2 * i + 1,
2 * i + 1
) )
} }
} }
@@ -138,35 +136,29 @@ class FrequencyPickerDialog(
var numerator = 1 var numerator = 1
var denominator = 1 var denominator = 1
when { when {
binding.everyDayRadioButton.isChecked -> { contentView.everyDayRadioButton.isChecked -> {
// NOP // NOP
} }
contentView.everyXDaysRadioButton.isChecked -> {
binding.everyXDaysRadioButton.isChecked -> { if (contentView.everyXDaysTextView.text.isNotEmpty()) {
if (binding.everyXDaysTextView.text.isNotEmpty()) { denominator = Integer.parseInt(contentView.everyXDaysTextView.text.toString())
denominator = Integer.parseInt(binding.everyXDaysTextView.text.toString())
} }
} }
contentView.xTimesPerWeekRadioButton.isChecked -> {
binding.xTimesPerWeekRadioButton.isChecked -> { if (contentView.xTimesPerWeekTextView.text.isNotEmpty()) {
if (binding.xTimesPerWeekTextView.text.isNotEmpty()) { numerator = Integer.parseInt(contentView.xTimesPerWeekTextView.text.toString())
numerator = Integer.parseInt(binding.xTimesPerWeekTextView.text.toString())
denominator = 7 denominator = 7
} }
} }
contentView.xTimesPerYDaysRadioButton.isChecked -> {
binding.xTimesPerYDaysRadioButton.isChecked -> { if (contentView.xTimesPerYDaysXTextView.text.isNotEmpty() && contentView.xTimesPerYDaysYTextView.text.isNotEmpty()) {
if (binding.xTimesPerYDaysXTextView.text.isNotEmpty() && binding.xTimesPerYDaysYTextView.text.isNotEmpty()) { numerator = Integer.parseInt(contentView.xTimesPerYDaysXTextView.text.toString())
numerator = denominator = Integer.parseInt(contentView.xTimesPerYDaysYTextView.text.toString())
Integer.parseInt(binding.xTimesPerYDaysXTextView.text.toString())
denominator =
Integer.parseInt(binding.xTimesPerYDaysYTextView.text.toString())
} }
} }
else -> { else -> {
if (binding.xTimesPerMonthTextView.text.isNotEmpty()) { if (contentView.xTimesPerMonthTextView.text.isNotEmpty()) {
numerator = Integer.parseInt(binding.xTimesPerMonthTextView.text.toString()) numerator = Integer.parseInt(contentView.xTimesPerMonthTextView.text.toString())
denominator = 30 denominator = 30
} }
} }
@@ -193,27 +185,27 @@ class FrequencyPickerDialog(
private fun populateViews() { private fun populateViews() {
uncheckAll() uncheckAll()
if (freqDenominator == 30 || freqDenominator == 31) { if (freqDenominator == 30 || freqDenominator == 31) {
binding.xTimesPerMonthRadioButton.isChecked = true contentView.xTimesPerMonthRadioButton.isChecked = true
binding.xTimesPerMonthTextView.setText(freqNumerator.toString()) contentView.xTimesPerMonthTextView.setText(freqNumerator.toString())
selectInputField(binding.xTimesPerMonthTextView) selectInputField(contentView.xTimesPerMonthTextView)
} else { } else {
if (freqNumerator == 1) { if (freqNumerator == 1) {
if (freqDenominator == 1) { if (freqDenominator == 1) {
binding.everyDayRadioButton.isChecked = true contentView.everyDayRadioButton.isChecked = true
} else { } else {
binding.everyXDaysRadioButton.isChecked = true contentView.everyXDaysRadioButton.isChecked = true
binding.everyXDaysTextView.setText(freqDenominator.toString()) contentView.everyXDaysTextView.setText(freqDenominator.toString())
selectInputField(binding.everyXDaysTextView) selectInputField(contentView.everyXDaysTextView)
} }
} else { } else {
if (freqDenominator == 7) { if (freqDenominator == 7) {
binding.xTimesPerWeekRadioButton.isChecked = true contentView.xTimesPerWeekRadioButton.isChecked = true
binding.xTimesPerWeekTextView.setText(freqNumerator.toString()) contentView.xTimesPerWeekTextView.setText(freqNumerator.toString())
selectInputField(binding.xTimesPerWeekTextView) selectInputField(contentView.xTimesPerWeekTextView)
} else { } else {
binding.xTimesPerYDaysRadioButton.isChecked = true contentView.xTimesPerYDaysRadioButton.isChecked = true
binding.xTimesPerYDaysXTextView.setText(freqNumerator.toString()) contentView.xTimesPerYDaysXTextView.setText(freqNumerator.toString())
binding.xTimesPerYDaysYTextView.setText(freqDenominator.toString()) contentView.xTimesPerYDaysYTextView.setText(freqDenominator.toString())
} }
} }
} }
@@ -224,10 +216,10 @@ class FrequencyPickerDialog(
} }
private fun uncheckAll() { private fun uncheckAll() {
binding.everyDayRadioButton.isChecked = false contentView.everyDayRadioButton.isChecked = false
binding.everyXDaysRadioButton.isChecked = false contentView.everyXDaysRadioButton.isChecked = false
binding.xTimesPerWeekRadioButton.isChecked = false contentView.xTimesPerWeekRadioButton.isChecked = false
binding.xTimesPerMonthRadioButton.isChecked = false contentView.xTimesPerMonthRadioButton.isChecked = false
binding.xTimesPerYDaysRadioButton.isChecked = false contentView.xTimesPerYDaysRadioButton.isChecked = false
} }
} }

View File

@@ -51,12 +51,12 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
clearCurrentDialog() clearCurrentDialog()
val component = (requireActivity().application as HabitsApplication).component val component = (activity!!.application as HabitsApplication).component
commandRunner = component.commandRunner commandRunner = component.commandRunner
habit = component.habitList.getById(requireArguments().getLong("habit"))!! habit = component.habitList.getById(arguments!!.getLong("habit"))!!
preferences = component.preferences preferences = component.preferences
val themeSwitcher = AndroidThemeSwitcher(requireActivity(), preferences) val themeSwitcher = AndroidThemeSwitcher(activity!!, preferences)
themeSwitcher.apply() themeSwitcher.apply()
chart = HistoryChart( chart = HistoryChart(
@@ -69,12 +69,12 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
theme = themeSwitcher.currentTheme, theme = themeSwitcher.currentTheme,
today = DateUtils.getTodayWithOffset().toLocalDate(), today = DateUtils.getTodayWithOffset().toLocalDate(),
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {}, onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
padding = 10.0 padding = 10.0,
) )
dataView = AndroidDataView(requireContext(), null) dataView = AndroidDataView(context!!, null)
dataView.view = chart!! dataView.view = chart!!
val dialog = Dialog(requireContext()).apply { val dialog = Dialog(context!!).apply {
val metrics = resources.displayMetrics val metrics = resources.displayMetrics
val maxHeight = resources.getDimensionPixelSize(R.dimen.history_editor_max_height) val maxHeight = resources.getDimensionPixelSize(R.dimen.history_editor_max_height)
setContentView(dataView) setContentView(dataView)

View File

@@ -0,0 +1,15 @@
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import java.lang.ref.WeakReference
class MultipleDialogsHandler {
companion object {
var currentDialog: WeakReference<Dialog> = WeakReference(null)
fun Dialog.dismissCurrent() {
currentDialog.get()?.dismiss()
currentDialog = WeakReference(this)
}
}
}

View File

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

View File

@@ -0,0 +1,117 @@
/*
* 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.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
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 = { _, _ -> }
var onDismiss: () -> Unit = {}
private val originalValue = value
private lateinit var dialog: Dialog
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
// Required for round corners
container.clipToOutline = true
}
init {
view.numberButtons.visibility = VISIBLE
hideDisabledButtons()
populate()
}
private fun hideDisabledButtons() {
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = GONE
}
private fun populate() {
view.notes.setText(notes)
view.value.setText(
when {
value < 0.01 -> "0"
else -> DecimalFormat("#.##").format(value)
}
)
}
fun show() {
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
dialog.setContentView(view.root)
dialog.window?.apply {
setLayout(
view.root.dp(POPUP_WIDTH).toInt(),
view.root.dp(POPUP_HEIGHT).toInt()
)
setBackgroundDrawableResource(android.R.color.transparent)
}
dialog.setOnDismissListener {
onDismiss()
}
view.value.setOnKeyListener { _, keyCode, event ->
if (event.action == ACTION_DOWN && keyCode == KEYCODE_ENTER) {
save()
return@setOnKeyListener true
}
return@setOnKeyListener false
}
view.saveBtn.setOnClickListener {
save()
}
view.skipBtnNumber.setOnClickListener {
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
save()
}
view.value.requestFocusWithKeyboard()
dialog.setCanceledOnTouchOutside(true)
dialog.dimBehind()
dialog.dismissCurrent()
dialog.show()
}
fun save() {
val value = view.value.text.toString().toDoubleOrNull() ?: originalValue
val notes = view.notes.text.toString()
onToggle(value, notes)
dialog.dismiss()
}
}

View File

@@ -25,6 +25,7 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
import java.util.Calendar import java.util.Calendar
@@ -60,7 +61,7 @@ class WeekdayPickerDialog :
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder( val builder = AlertDialog.Builder(
requireActivity() activity!!
) )
builder builder
.setTitle(R.string.select_weekdays) .setTitle(R.string.select_weekdays)
@@ -74,7 +75,9 @@ class WeekdayPickerDialog :
android.R.string.cancel android.R.string.cancel
) { _: DialogInterface?, _: Int -> dismiss() } ) { _: DialogInterface?, _: Int -> dismiss() }
return builder.create() val dialog = builder.create()
dialog.dismissCurrent()
return dialog
} }
fun setListener(listener: OnWeekdaysPickedListener?) { fun setListener(listener: OnWeekdaysPickedListener?) {

View File

@@ -63,9 +63,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
private var isNumerical: Boolean = false
constructor(context: Context?) : super(context) { constructor(context: Context?) : super(context) {
init() init()
@@ -82,14 +80,8 @@ class FrequencyChart : ScrollableChart {
postInvalidate() postInvalidate()
} }
fun setIsNumerical(type: Boolean) {
isNumerical = type
postInvalidate()
}
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()
} }
@@ -98,15 +90,6 @@ 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()
@@ -181,9 +164,8 @@ class FrequencyChart : ScrollableChart {
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat() rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j) rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
val i = localeWeekdayList[j] % 7 val i = localeWeekdayList[j] % 7
if (values != null) { if (values != null)
drawMarker(canvas, rect, values[i], weekDaysInMonth[i]) drawMarker(canvas, rect, values[i], weekDaysInMonth[i])
}
rect.offset(0f, rowHeight) rect.offset(0f, rowHeight)
} }
drawFooter(canvas, rect, date) drawFooter(canvas, rect, date)
@@ -197,14 +179,12 @@ class FrequencyChart : ScrollableChart {
rect.centerY() - 0.1f * em, rect.centerY() - 0.1f * em,
pText!! pText!!
) )
if (date[Calendar.MONTH] == 1) { if (date[Calendar.MONTH] == 1) canvas.drawText(
canvas.drawText( dfYear!!.format(time),
dfYear!!.format(time), rect.centerX(),
rect.centerX(), rect.centerY() + 0.9f * em,
rect.centerY() + 0.9f * em, pText!!
pText!! )
)
}
} }
private fun drawGrid(canvas: Canvas, rGrid: RectF?) { private fun drawGrid(canvas: Canvas, rGrid: RectF?) {
@@ -233,7 +213,7 @@ 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?, weekdayFrequency: Int) { private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?, frequency: Int) {
// value can be negative when the entry is skipped // value can be negative when the entry is skipped
val valueCopy = value?.let { max(0, it) } val valueCopy = value?.let { max(0, it) }
@@ -241,8 +221,8 @@ class FrequencyChart : ScrollableChart {
// 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 scalingFactor = if (isNumerical) maxFreq else weekdayFrequency
val scale = 1.0f / scalingFactor * valueCopy!! 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]
@@ -305,6 +285,5 @@ class FrequencyChart : ScrollableChart {
frequency[Timestamp(date)] = values frequency[Timestamp(date)] = values
date.add(Calendar.MONTH, -1) date.add(Calendar.MONTH, -1)
} }
maxFreq = getMaxFreq(frequency)
} }
} }

View File

@@ -58,7 +58,6 @@ class RingView : View {
private var em = 0f private var em = 0f
private var text: String? private var text: String?
private var textSize: Float private var textSize: Float
private var isStrokedTextEnabled: Boolean = false
private var enableFontAwesome = false private var enableFontAwesome = false
private var internalDrawingCache: Bitmap? = null private var internalDrawingCache: Bitmap? = null
private var cacheCanvas: Canvas? = null private var cacheCanvas: Canvas? = null
@@ -132,10 +131,6 @@ class RingView : View {
invalidate() invalidate()
} }
fun setIsStrokedTextEnabled(isStroked: Boolean) {
this.isStrokedTextEnabled = isStroked
}
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
val activeCanvas: Canvas? val activeCanvas: Canvas?
@@ -153,23 +148,13 @@ class RingView : View {
pRing!!.color = inactiveColor!! pRing!!.color = inactiveColor!!
activeCanvas.drawArc(rect!!, angle - 90, 360 - angle, true, pRing!!) activeCanvas.drawArc(rect!!, angle - 90, 360 - angle, true, pRing!!)
if (thickness > 0) { if (thickness > 0) {
if (isTransparencyEnabled) { if (isTransparencyEnabled) pRing!!.xfermode = XFERMODE_CLEAR else pRing!!.color =
pRing!!.xfermode = XFERMODE_CLEAR backgroundColor!!
} else {
pRing!!.color =
backgroundColor!!
}
rect!!.inset(thickness, thickness) rect!!.inset(thickness, thickness)
activeCanvas.drawArc(rect!!, 0f, 360f, true, pRing!!) activeCanvas.drawArc(rect!!, 0f, 360f, true, pRing!!)
pRing!!.xfermode = null pRing!!.xfermode = null
pRing!!.color = color pRing!!.color = color
pRing!!.textSize = textSize pRing!!.textSize = textSize
if (isStrokedTextEnabled) {
pRing!!.style = Paint.Style.STROKE
pRing!!.strokeWidth = textSize / 15f
}
if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context) if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context)
activeCanvas.drawText( activeCanvas.drawText(
text!!, text!!,

View File

@@ -65,7 +65,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat
} }
override fun onFling( override fun onFling(
e1: MotionEvent?, e1: MotionEvent,
e2: MotionEvent, e2: MotionEvent,
velocityX: Float, velocityX: Float,
velocityY: Float velocityY: Float
@@ -116,7 +116,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat
return BundleSavedState(superState, bundle) return BundleSavedState(superState, bundle)
} }
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float): Boolean { override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, dx: Float, dy: Float): Boolean {
var dx = dx var dx = dx
if (scrollerBucketSize == 0) return false if (scrollerBucketSize == 0) return false
if (abs(dx) > abs(dy)) { if (abs(dx) > abs(dy)) {

View File

@@ -35,12 +35,18 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.android.datetimepicker.time.RadialPickerLayout import com.android.datetimepicker.time.RadialPickerLayout
import com.android.datetimepicker.time.TimePickerDialog import com.android.datetimepicker.time.TimePickerDialog
import kotlinx.android.synthetic.main.activity_edit_habit.nameInput
import kotlinx.android.synthetic.main.activity_edit_habit.notesInput
import kotlinx.android.synthetic.main.activity_edit_habit.questionInput
import kotlinx.android.synthetic.main.activity_edit_habit.targetInput
import kotlinx.android.synthetic.main.activity_edit_habit.unitInput
import org.isoron.platform.gui.toInt import org.isoron.platform.gui.toInt
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.common.dialogs.ColorPickerDialogFactory import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.FrequencyPickerDialog import org.isoron.uhabits.activities.common.dialogs.FrequencyPickerDialog
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog
import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateHabitCommand import org.isoron.uhabits.core.commands.CreateHabitCommand
@@ -54,7 +60,6 @@ import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.uhabits.databinding.ActivityEditHabitBinding import org.isoron.uhabits.databinding.ActivityEditHabitBinding
import org.isoron.uhabits.utils.ColorUtils import org.isoron.uhabits.utils.ColorUtils
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.formatTime import org.isoron.uhabits.utils.formatTime
import org.isoron.uhabits.utils.toFormattedString import org.isoron.uhabits.utils.toFormattedString
@@ -152,23 +157,23 @@ class EditHabitActivity : AppCompatActivity() {
val colorPickerDialogFactory = ColorPickerDialogFactory(this) val colorPickerDialogFactory = ColorPickerDialogFactory(this)
binding.colorButton.setOnClickListener { binding.colorButton.setOnClickListener {
val picker = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme) val dialog = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
picker.setListener { paletteColor -> dialog.setListener { paletteColor ->
this.color = paletteColor this.color = paletteColor
updateColors() updateColors()
} }
picker.dismissCurrentAndShow(supportFragmentManager, "colorPicker") dialog.show(supportFragmentManager, "colorPicker")
} }
populateFrequency() populateFrequency()
binding.booleanFrequencyPicker.setOnClickListener { binding.booleanFrequencyPicker.setOnClickListener {
val picker = FrequencyPickerDialog(freqNum, freqDen) val dialog = FrequencyPickerDialog(freqNum, freqDen)
picker.onFrequencyPicked = { num, den -> dialog.onFrequencyPicked = { num, den ->
freqNum = num freqNum = num
freqDen = den freqDen = den
populateFrequency() populateFrequency()
} }
picker.dismissCurrentAndShow(supportFragmentManager, "frequencyPicker") dialog.show(supportFragmentManager, "frequencyPicker")
} }
populateTargetType() populateTargetType()
@@ -186,7 +191,8 @@ class EditHabitActivity : AppCompatActivity() {
dialog.dismiss() dialog.dismiss()
} }
val dialog = builder.create() val dialog = builder.create()
dialog.dismissCurrentAndShow() dialog.dismissCurrent()
dialog.show()
} }
binding.numericalFrequencyPicker.setOnClickListener { binding.numericalFrequencyPicker.setOnClickListener {
@@ -232,7 +238,7 @@ class EditHabitActivity : AppCompatActivity() {
is24HourMode, is24HourMode,
androidColor androidColor
) )
dialog.dismissCurrentAndShow(supportFragmentManager, "timePicker") dialog.show(supportFragmentManager, "timePicker")
} }
binding.reminderDatePicker.setOnClickListener { binding.reminderDatePicker.setOnClickListener {
@@ -244,7 +250,7 @@ class EditHabitActivity : AppCompatActivity() {
populateReminder() populateReminder()
} }
dialog.setSelectedDays(reminderDays) dialog.setSelectedDays(reminderDays)
dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker") dialog.show(supportFragmentManager, "dayPicker")
} }
binding.buttonSave.setOnClickListener { binding.buttonSave.setOnClickListener {
@@ -266,9 +272,9 @@ class EditHabitActivity : AppCompatActivity() {
habit.copyFrom(original) habit.copyFrom(original)
} }
habit.name = binding.nameInput.text.trim().toString() habit.name = nameInput.text.trim().toString()
habit.question = binding.questionInput.text.trim().toString() habit.question = questionInput.text.trim().toString()
habit.description = binding.notesInput.text.trim().toString() habit.description = notesInput.text.trim().toString()
habit.color = color habit.color = color
if (reminderHour >= 0) { if (reminderHour >= 0) {
habit.reminder = Reminder(reminderHour, reminderMin, reminderDays) habit.reminder = Reminder(reminderHour, reminderMin, reminderDays)
@@ -278,9 +284,9 @@ class EditHabitActivity : AppCompatActivity() {
habit.frequency = Frequency(freqNum, freqDen) habit.frequency = Frequency(freqNum, freqDen)
if (habitType == HabitType.NUMERICAL) { if (habitType == HabitType.NUMERICAL) {
habit.targetValue = binding.targetInput.text.toString().toDouble() habit.targetValue = targetInput.text.toString().toDouble()
habit.targetType = targetType habit.targetType = targetType
habit.unit = binding.unitInput.text.trim().toString() habit.unit = unitInput.text.trim().toString()
} }
habit.type = habitType habit.type = habitType
@@ -303,13 +309,13 @@ class EditHabitActivity : AppCompatActivity() {
private fun validate(): Boolean { private fun validate(): Boolean {
var isValid = true var isValid = true
if (binding.nameInput.text.isEmpty()) { if (nameInput.text.isEmpty()) {
binding.nameInput.error = getFormattedValidationError(R.string.validation_cannot_be_blank) nameInput.error = getFormattedValidationError(R.string.validation_cannot_be_blank)
isValid = false isValid = false
} }
if (habitType == HabitType.NUMERICAL) { if (habitType == HabitType.NUMERICAL) {
if (binding.targetInput.text.isEmpty()) { if (targetInput.text.isEmpty()) {
binding.targetInput.error = getString(R.string.validation_cannot_be_blank) targetInput.error = getString(R.string.validation_cannot_be_blank)
isValid = false isValid = false
} }
} }

View File

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

View File

@@ -19,17 +19,11 @@
package org.isoron.uhabits.activities.habits.list package org.isoron.uhabits.activities.habits.list
import android.Manifest.permission.POST_NOTIFICATIONS
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat.checkSelfPermission
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import org.isoron.uhabits.BaseExceptionHandler import org.isoron.uhabits.BaseExceptionHandler
@@ -45,7 +39,6 @@ import org.isoron.uhabits.inject.ActivityContextModule
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
import org.isoron.uhabits.inject.HabitsActivityComponent import org.isoron.uhabits.inject.HabitsActivityComponent
import org.isoron.uhabits.inject.HabitsApplicationComponent import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.isoron.uhabits.utils.dismissCurrentDialog
import org.isoron.uhabits.utils.restartWithFade import org.isoron.uhabits.utils.restartWithFade
class ListHabitsActivity : AppCompatActivity(), Preferences.Listener { class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
@@ -61,16 +54,6 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
lateinit var midnightTimer: MidnightTimer lateinit var midnightTimer: MidnightTimer
private val scope = CoroutineScope(Dispatchers.Main) private val scope = CoroutineScope(Dispatchers.Main)
private var permissionAlreadyRequested = false
private val permissionLauncher =
registerForActivityResult(RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
scheduleReminders()
} else {
Log.i("ListHabitsActivity", "POST_NOTIFICATIONS denied")
}
}
private lateinit var menu: ListHabitsMenu private lateinit var menu: ListHabitsMenu
override fun onQuestionMarksChanged() { override fun onQuestionMarksChanged() {
@@ -101,13 +84,13 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this)) Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this))
component.listHabitsBehavior.onStartup() component.listHabitsBehavior.onStartup()
setContentView(rootView) setContentView(rootView)
parseIntents()
} }
override fun onPause() { override fun onPause() {
midnightTimer.onPause() midnightTimer.onPause()
screen.onDetached() screen.onDetached()
adapter.cancelRefresh() adapter.cancelRefresh()
dismissCurrentDialog()
super.onPause() super.onPause()
} }
@@ -116,45 +99,15 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
screen.onAttached() screen.onAttached()
rootView.postInvalidate() rootView.postInvalidate()
midnightTimer.onResume() midnightTimer.onResume()
if (appComponent.reminderScheduler.hasHabitsWithReminders()) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
scheduleReminders()
} else {
if (checkSelfPermission(this, POST_NOTIFICATIONS) == PERMISSION_GRANTED) {
scheduleReminders()
} else {
// If we have not requested the permission yet, request it. Otherwide do
// nothing. This check is necessary to avoid an infinite onResume loop in case
// the user denies the permission.
if (!permissionAlreadyRequested) {
Log.i("ListHabitsActivity", "Requestion permission: POST_NOTIFICATIONS")
permissionLauncher.launch(POST_NOTIFICATIONS)
permissionAlreadyRequested = true
}
}
}
}
taskRunner.run { taskRunner.run {
try { AutoBackup(this@ListHabitsActivity).run()
AutoBackup(this@ListHabitsActivity).run()
appComponent.widgetUpdater.updateWidgets()
} catch (e: Exception) {
Log.e("ListHabitActivity", "TaskRunner failed", e)
}
} }
if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) { if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) {
restartWithFade(ListHabitsActivity::class.java) restartWithFade(ListHabitsActivity::class.java)
} }
parseIntents()
super.onResume() super.onResume()
} }
private fun scheduleReminders() {
appComponent.reminderScheduler.scheduleAll()
}
override fun onCreateOptionsMenu(m: Menu): Boolean { override fun onCreateOptionsMenu(m: Menu): Boolean {
menu.onCreate(menuInflater, m) menu.onCreate(menuInflater, m)
return true return true
@@ -165,14 +118,12 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
return menu.onItemSelected(item) return menu.onItemSelected(item)
} }
@Deprecated("Deprecated in Java")
override fun onActivityResult(request: Int, result: Int, data: Intent?) { override fun onActivityResult(request: Int, result: Int, data: Intent?) {
super.onActivityResult(request, result, data) super.onActivityResult(request, result, data)
screen.onResult(request, result, data) screen.onResult(request, result, data)
} }
private fun parseIntents() { private fun parseIntents() {
if (intent == null) return
if (intent.action == ACTION_EDIT) { if (intent.action == ACTION_EDIT) {
val habitId = intent.extras?.getLong("habit") val habitId = intent.extras?.getLong("habit")
val timestamp = intent.extras?.getLong("timestamp") val timestamp = intent.extras?.getLong("timestamp")
@@ -181,12 +132,6 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
component.listHabitsBehavior.onEdit(habit, Timestamp(timestamp)) component.listHabitsBehavior.onEdit(habit, Timestamp(timestamp))
} }
} }
intent = null
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
} }
companion object { companion object {

View File

@@ -23,7 +23,6 @@ import android.content.Context
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import nl.dionsegijn.konfetti.xml.KonfettiView
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.ScrollableChart import org.isoron.uhabits.activities.common.views.ScrollableChart
import org.isoron.uhabits.activities.common.views.TaskProgressBar import org.isoron.uhabits.activities.common.views.TaskProgressBar
@@ -70,9 +69,6 @@ class ListHabitsRootView @Inject constructor(
val listView: HabitCardListView = habitCardListViewFactory.create() val listView: HabitCardListView = habitCardListViewFactory.create()
val llEmpty = EmptyListView(context) val llEmpty = EmptyListView(context)
val tbar = buildToolbar() val tbar = buildToolbar()
val konfettiView = KonfettiView(context).apply {
translationZ = 10f
}
val progressBar = TaskProgressBar(context, runner) val progressBar = TaskProgressBar(context, runner)
val hintView: HintView val hintView: HintView
val header = HeaderView(context, preferences, midnightTimer) val header = HeaderView(context, preferences, midnightTimer)
@@ -84,7 +80,6 @@ class ListHabitsRootView @Inject constructor(
val rootView = RelativeLayout(context).apply { val rootView = RelativeLayout(context).apply {
background = sres.getDrawable(R.attr.windowBackgroundColor) background = sres.getDrawable(R.attr.windowBackgroundColor)
addAtTop(konfettiView)
addAtTop(tbar) addAtTop(tbar)
addBelow(header, tbar) addBelow(header, tbar)
addBelow(listView, header, height = MATCH_PARENT) addBelow(listView, header, height = MATCH_PARENT)
@@ -99,7 +94,7 @@ class ListHabitsRootView @Inject constructor(
title = resources.getString(R.string.main_activity_title), title = resources.getString(R.string.main_activity_title),
color = PaletteColor(17), color = PaletteColor(17),
displayHomeAsUpEnabled = false, displayHomeAsUpEnabled = false,
theme = currentTheme() theme = currentTheme(),
) )
addView(rootView, MATCH_PARENT, MATCH_PARENT) addView(rootView, MATCH_PARENT, MATCH_PARENT)
listAdapter.setListView(listView) listAdapter.setListView(listView)

View File

@@ -22,18 +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.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy import dagger.Lazy
import nl.dionsegijn.konfetti.core.Party
import nl.dionsegijn.konfetti.core.Position
import nl.dionsegijn.konfetti.core.emitter.Emitter
import org.isoron.platform.gui.toInt import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog 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.NumberDialog import org.isoron.uhabits.activities.common.dialogs.NumberPopup
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
@@ -66,17 +62,14 @@ import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.tasks.ExportDBTaskFactory 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.ColorUtils
import org.isoron.uhabits.utils.copyTo import org.isoron.uhabits.utils.copyTo
import org.isoron.uhabits.utils.currentTheme import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dismissCurrentAndShow
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
import org.isoron.uhabits.utils.showSendFileScreen import org.isoron.uhabits.utils.showSendFileScreen
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
const val RESULT_IMPORT_DATA = 101 const val RESULT_IMPORT_DATA = 101
@@ -101,7 +94,7 @@ class ListHabitsScreen
private val colorPickerFactory: ColorPickerDialogFactory, private val colorPickerFactory: ColorPickerDialogFactory,
private val behavior: Lazy<ListHabitsBehavior>, private val behavior: Lazy<ListHabitsBehavior>,
private val preferences: Preferences, private val preferences: Preferences,
private val rootView: Lazy<ListHabitsRootView> private val rootView: Lazy<ListHabitsRootView>,
) : CommandRunner.Listener, ) : CommandRunner.Listener,
ListHabitsBehavior.Screen, ListHabitsBehavior.Screen,
ListHabitsMenuBehavior.Screen, ListHabitsMenuBehavior.Screen,
@@ -170,7 +163,7 @@ class ListHabitsScreen
} }
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) { override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) {
ConfirmDeleteDialog(activity, callback, quantity).dismissCurrentAndShow() ConfirmDeleteDialog(activity, callback, quantity).show()
} }
override fun showEditHabitsScreen(selected: List<Habit>) { override fun showEditHabitsScreen(selected: List<Habit>) {
@@ -223,28 +216,6 @@ class ListHabitsScreen
activity.showSendFileScreen(filename) activity.showSendFileScreen(filename)
} }
override fun showConfetti(color: PaletteColor, x: Float, y: Float) {
val baseColor = themeSwitcher.currentTheme!!.color(color).toInt()
rootView.get().konfettiView.start(
Party(
speed = 0f,
maxSpeed = 16f,
damping = 0.9f,
spread = 360,
angle = 0,
colors = listOf(
ColorUtils.changeHue(baseColor, 180f),
ColorUtils.changeHue(baseColor, 20f),
ColorUtils.changeHue(baseColor, -20f),
baseColor
),
position = Position.Absolute(x, y),
emitter = Emitter(duration = 25, TimeUnit.MILLISECONDS).max(25),
timeToLive = 0
)
)
}
override fun showSettingsScreen() { override fun showSettingsScreen() {
val intent = intentFactory.startSettingsActivity(activity) val intent = intentFactory.startSettingsActivity(activity)
activity.startActivityForResult(intent, REQUEST_SETTINGS) activity.startActivityForResult(intent, REQUEST_SETTINGS)
@@ -253,7 +224,7 @@ class ListHabitsScreen
override fun showColorPicker(defaultColor: PaletteColor, callback: OnColorPickedCallback) { override fun showColorPicker(defaultColor: PaletteColor, callback: OnColorPickedCallback) {
val picker = colorPickerFactory.create(defaultColor, themeSwitcher.currentTheme!!) val picker = colorPickerFactory.create(defaultColor, themeSwitcher.currentTheme!!)
picker.setListener(callback) picker.setListener(callback)
picker.dismissCurrentAndShow(activity.supportFragmentManager, "picker") picker.show(activity.supportFragmentManager, "picker")
} }
override fun showNumberPopup( override fun showNumberPopup(
@@ -261,14 +232,17 @@ class ListHabitsScreen
notes: String, notes: String,
callback: ListHabitsBehavior.NumberPickerCallback callback: ListHabitsBehavior.NumberPickerCallback
) { ) {
val fm = (context as AppCompatActivity).supportFragmentManager val view = rootView.get()
val dialog = NumberDialog() NumberPopup(
dialog.arguments = Bundle().apply { context = context,
putDouble("value", value) prefs = preferences,
putString("notes", notes) anchor = view,
notes = notes,
value = value,
).apply {
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
show()
} }
dialog.onToggle = { v, n, x, y -> callback.onNumberPicked(v, n, x, y) }
dialog.dismissCurrentAndShow(fm, "numberDialog")
} }
override fun showCheckmarkPopup( override fun showCheckmarkPopup(
@@ -277,16 +251,18 @@ class ListHabitsScreen
color: PaletteColor, color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback callback: ListHabitsBehavior.CheckMarkDialogCallback
) { ) {
val theme = rootView.get().currentTheme() val view = rootView.get()
val fm = (context as AppCompatActivity).supportFragmentManager CheckmarkPopup(
val dialog = CheckmarkDialog() context = context,
dialog.arguments = Bundle().apply { prefs = preferences,
putInt("color", theme.color(color).toInt()) anchor = view,
putInt("value", selectedValue) color = view.currentTheme().color(color).toInt(),
putString("notes", notes) notes = notes,
value = selectedValue,
).apply {
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
show()
} }
dialog.onToggle = { v, n, x, y -> callback.onNotesSaved(v, n, x, y) }
dialog.dismissCurrentAndShow(fm, "checkmarkDialog")
} }
private fun getExecuteString(command: Command): String? { private fun getExecuteString(command: Command): String? {
@@ -348,11 +324,8 @@ class ListHabitsScreen
private fun onExportDB() { private fun onExportDB() {
taskRunner.execute( taskRunner.execute(
exportDBFactory.create { filename -> exportDBFactory.create { filename ->
if (filename != null) { if (filename != null) activity.showSendFileScreen(filename)
activity.showSendFileScreen(filename) else activity.showMessage(activity.resources.getString(R.string.could_not_export))
} else {
activity.showMessage(activity.resources.getString(R.string.could_not_export))
}
} }
) )
} }

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