Merge branch 'release/2.2.0'

pull/2197/merge v2.2.0
Alinson S. Xavier 2 years ago
commit d10538e720
Signed by: isoron
GPG Key ID: 0DA8E4B9E1109DCA

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

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

6
.gitignore vendored

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

@ -1,16 +0,0 @@
#!/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

Binary file not shown.

@ -1,5 +1,14 @@
# Changelog
## [2.2.0] -- 2024-01-30
### 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)

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

@ -1,11 +1,10 @@
plugins {
val kotlinVersion = "1.6.10"
id("com.android.application") version ("7.0.3") apply (false)
val kotlinVersion = "1.9.21"
id("com.android.application") version "8.1.4" apply (false)
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false)
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
}
apply {

@ -85,10 +85,10 @@ android_setup() {
$AVDMANAGER delete avd --name $AVDNAME
log_info "Creating new Android virtual device (API $API)..."
(echo "y" | $SDKMANAGER --install "system-images;android-$API;default;x86_64") || return 1
(echo "y" | $SDKMANAGER --install "system-images;android-$API;google_apis;x86_64") || return 1
$AVDMANAGER create avd \
--name $AVDNAME \
--package "system-images;android-$API;default;x86_64" \
--package "system-images;android-$API;google_apis;x86_64" \
--device "Nexus 4" || return 1
flock -u 10
@ -181,7 +181,7 @@ android_test() {
OUT_INSTRUMENT=${ANDROID_OUTPUTS_DIR}/instrument-${API}.txt
OUT_LOGCAT=${ANDROID_OUTPUTS_DIR}/logcat-${API}.txt
FAILED_TESTS=""
for i in {1..5}; do
for i in {1..10}; do
log_info "Running $size instrumented tests (attempt $i)..."
$ADB shell am instrument \
-r -e coverage true -e size "$size" $FAILED_TESTS \

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

@ -2,51 +2,25 @@
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:
* **Small tests:** These tests run very quickly on the developer's computer, inside a JVM, and do not need an Android emulator or device. They typically test the correctness of core functions of the application, such as the computation of scores and streaks.
* **Medium tests:** These tests require an Android emulator or device, but they are still quite fast to run, since only individual classes are tested. The app itself does not need to be launched. Examples include *view tests*, which render our custom views on the device and compare them against prerendered images.
* **Large tests:** These are end-to-end tests, which launch the application on an Android emulator and interact with it by touching the screen, much like a regular user.
- **Unit tests:** These tests run very quickly on the developer's computer, inside a JVM, and do not need an Android emulator or device. They typically test the correctness of core functions of the application, such as the computation of scores and streaks.
- **Instrumented tests:** These tests require an Android emulator or device. _Medium_ instrumented tests are still quite fast to run, since only individual classes are tested. The app itself does not need to be launched. Examples include _view tests_, which render our custom views on the device and compare them against prerendered images. _Large_ instrumented tests launch the application on an Android emulator and interact with it by touching the screen, much like a regular user.
## Running small tests
## Running unit 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.
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.
## Running medium tests
## Running instrumented 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.
./build.sh build
./build.sh medium-tests
1. Run `./build.sh android-setup API` to create the emulator, where `API` is the desired API level.
2. Run `./build.sh android-tests API` to run the tests on a single API.
3. Run `./build.sh android-tests-parallel API API...` to run the tests on multiple APIs in parallel.
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:
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.
- No additional apps should be installed on the device;
- The homescreen must look exactly like it was when the emulator was originally created, with no additional icons or widgets;
- All animations must be manually disabled.
**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.
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`.

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

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

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

@ -1,27 +0,0 @@
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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

@ -1,106 +0,0 @@
!!! 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")

@ -1,104 +0,0 @@
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

@ -1,32 +0,0 @@
!!! 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}

@ -1,14 +0,0 @@
## 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.

@ -18,11 +18,10 @@
*/
plugins {
id("com.github.triplet.play") version "3.7.0"
id("com.android.application")
id("com.github.triplet.play") version "3.8.6"
id("com.android.application") version "8.1.4"
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt")
id("org.jetbrains.kotlin.android.extensions")
id("org.jlleitschuh.gradle.ktlint")
}
@ -30,15 +29,27 @@ tasks.compileLint {
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 {
compileSdk = 31
namespace = "org.isoron.uhabits"
compileSdk = 34
defaultConfig {
versionCode = 20103
versionName = "2.1.3"
minSdk = 23
targetSdk = 31
versionCode = 20200
versionName = "2.2.0"
minSdk = 28
targetSdk = 34
applicationId = "org.isoron.uhabits"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@ -68,16 +79,13 @@ android {
}
}
lint {
isCheckReleaseBuilds = false
isAbortOnError = false
disable("GoogleAppIndexingWarning")
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
targetCompatibility(JavaVersion.VERSION_1_8)
sourceCompatibility(JavaVersion.VERSION_1_8)
targetCompatibility(JavaVersion.VERSION_11)
sourceCompatibility(JavaVersion.VERSION_11)
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
buildFeatures {
@ -86,29 +94,29 @@ android {
}
dependencies {
val daggerVersion = "2.41"
val kotlinVersion = "1.6.21"
val kxCoroutinesVersion = "1.6.1"
val daggerVersion = "2.48.1"
val kotlinVersion = "1.9.21"
val kxCoroutinesVersion = "1.7.3"
val ktorVersion = "1.6.8"
val espressoVersion = "3.4.0"
val espressoVersion = "3.5.1"
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
androidTestImplementation("com.google.dagger:dagger:$daggerVersion")
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1")
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
androidTestImplementation("io.ktor:ktor-jackson:$ktorVersion")
androidTestImplementation("androidx.annotation:annotation:1.3.0")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.annotation:annotation:1.7.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
androidTestImplementation("androidx.test:rules:1.4.0")
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
androidTestImplementation("androidx.test:rules:1.5.0")
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
compileOnly("javax.annotation:jsr250-api:1.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
implementation("com.github.AppIntro:AppIntro:6.2.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
implementation("com.github.AppIntro:AppIntro:6.3.1")
implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.google.dagger:dagger:$daggerVersion")
implementation("com.google.guava:guava:31.1-android")
implementation("com.google.guava:guava:32.1.3-android")
implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-jackson:$ktorVersion")
@ -116,17 +124,17 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.legacy:legacy-preference-v14:1.0.0")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("com.google.android.material:material:1.5.0")
implementation("com.opencsv:opencsv:5.6")
implementation("com.google.android.material:material:1.10.0")
implementation("com.opencsv:opencsv:5.9")
implementation(project(":uhabits-core"))
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
kaptAndroidTest("com.google.dagger:dagger-compiler:$daggerVersion")
testImplementation("com.google.dagger:dagger:$daggerVersion")
testImplementation("junit:junit:4.13.2")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
}
kapt {

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

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

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

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

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

@ -21,9 +21,9 @@ package org.isoron.uhabits.acceptance.steps
import android.os.Build.VERSION.SDK_INT
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.isoron.uhabits.BaseUserInterfaceTest
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
object WidgetSteps {
@Throws(Exception::class)

@ -18,6 +18,7 @@
*/
package org.isoron.uhabits.activities.common.views
import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
@ -52,7 +53,8 @@ class FrequencyChartTest : BaseViewTest() {
@Test
@Throws(Throwable::class)
fun testRender_withDataOffset() {
view.onScroll(null, null, -dpToPixels(150), 0f)
val e = MotionEvent.obtain(0, 0, 0, 0f, 0f, 0)
view.onScroll(e, e, -dpToPixels(150), 0f)
view.invalidate()
assertRenders(view, BASE_PATH + "renderDataOffset.png")
}

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

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

@ -20,15 +20,15 @@ package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.AndroidJUnit4
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.junit.Before
import org.junit.Test
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)
@MediumTest

@ -20,9 +20,6 @@ package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.AndroidJUnit4
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.MatcherAssert.assertThat
import org.isoron.uhabits.BaseViewTest
@ -30,6 +27,9 @@ import org.isoron.uhabits.core.ui.screens.habits.list.HintList
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@MediumTest

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

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

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

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

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

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

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

@ -33,6 +33,7 @@ import java.io.IOException
@MediumTest
class CheckmarkWidgetViewTest : BaseViewTest() {
private lateinit var view: CheckmarkWidgetView
@Before
override fun setUp() {
super.setUp()

@ -16,13 +16,13 @@
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.isoron.uhabits">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<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.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".HabitsApplication"
@ -30,6 +30,7 @@
android:backupAgent=".HabitsBackupAgent"
android:icon="@mipmap/ic_launcher"
android:label="@string/main_activity_title"
android:localeConfig="@xml/locales_config"
android:supportsRtl="true"
android:theme="@style/AppBaseTheme">

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

@ -22,7 +22,6 @@ import java.util.Locale;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.AttributeSet;
@ -383,10 +382,6 @@ public abstract class DayPickerView extends ListView implements OnScrollListener
if (child instanceof MonthView) {
final CalendarDay focus = ((MonthView) child).getAccessibilityFocus();
if (focus != null) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
// Clear focus to avoid ListView bug in Jelly Bean MR1.
((MonthView) child).clearAccessibilityFocus();
}
return focus;
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -60,7 +60,7 @@ class WeekdayPickerDialog :
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(
activity!!
requireActivity()
)
builder
.setTitle(R.string.select_weekdays)

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

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

@ -65,7 +65,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat
}
override fun onFling(
e1: MotionEvent,
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
@ -116,7 +116,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat
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
if (scrollerBucketSize == 0) return false
if (abs(dx) > abs(dy)) {

@ -35,11 +35,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment
import com.android.datetimepicker.time.RadialPickerLayout
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.uhabits.HabitsApplication
import org.isoron.uhabits.R
@ -271,9 +266,9 @@ class EditHabitActivity : AppCompatActivity() {
habit.copyFrom(original)
}
habit.name = nameInput.text.trim().toString()
habit.question = questionInput.text.trim().toString()
habit.description = notesInput.text.trim().toString()
habit.name = binding.nameInput.text.trim().toString()
habit.question = binding.questionInput.text.trim().toString()
habit.description = binding.notesInput.text.trim().toString()
habit.color = color
if (reminderHour >= 0) {
habit.reminder = Reminder(reminderHour, reminderMin, reminderDays)
@ -283,9 +278,9 @@ class EditHabitActivity : AppCompatActivity() {
habit.frequency = Frequency(freqNum, freqDen)
if (habitType == HabitType.NUMERICAL) {
habit.targetValue = targetInput.text.toString().toDouble()
habit.targetValue = binding.targetInput.text.toString().toDouble()
habit.targetType = targetType
habit.unit = unitInput.text.trim().toString()
habit.unit = binding.unitInput.text.trim().toString()
}
habit.type = habitType
@ -308,13 +303,13 @@ class EditHabitActivity : AppCompatActivity() {
private fun validate(): Boolean {
var isValid = true
if (nameInput.text.isEmpty()) {
nameInput.error = getFormattedValidationError(R.string.validation_cannot_be_blank)
if (binding.nameInput.text.isEmpty()) {
binding.nameInput.error = getFormattedValidationError(R.string.validation_cannot_be_blank)
isValid = false
}
if (habitType == HabitType.NUMERICAL) {
if (targetInput.text.isEmpty()) {
targetInput.error = getString(R.string.validation_cannot_be_blank)
if (binding.targetInput.text.isEmpty()) {
binding.targetInput.error = getString(R.string.validation_cannot_be_blank)
isValid = false
}
}

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

@ -19,12 +19,17 @@
package org.isoron.uhabits.activities.habits.list
import android.Manifest.permission.POST_NOTIFICATIONS
import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat.checkSelfPermission
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.isoron.uhabits.BaseExceptionHandler
@ -56,6 +61,16 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
lateinit var midnightTimer: MidnightTimer
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
override fun onQuestionMarksChanged() {
@ -101,7 +116,26 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
screen.onAttached()
rootView.postInvalidate()
midnightTimer.onResume()
appComponent.reminderScheduler.scheduleAll()
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 {
try {
AutoBackup(this@ListHabitsActivity).run()
@ -117,6 +151,10 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
super.onResume()
}
private fun scheduleReminders() {
appComponent.reminderScheduler.scheduleAll()
}
override fun onCreateOptionsMenu(m: Menu): Boolean {
menu.onCreate(menuInflater, m)
return true
@ -127,6 +165,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
return menu.onItemSelected(item)
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
super.onActivityResult(request, result, data)
screen.onResult(request, result, data)

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

@ -96,7 +96,7 @@ class ListHabitsScreen
private val colorPickerFactory: ColorPickerDialogFactory,
private val behavior: Lazy<ListHabitsBehavior>,
private val preferences: Preferences,
private val rootView: Lazy<ListHabitsRootView>,
private val rootView: Lazy<ListHabitsRootView>
) : CommandRunner.Listener,
ListHabitsBehavior.Screen,
ListHabitsMenuBehavior.Screen,
@ -321,8 +321,11 @@ class ListHabitsScreen
private fun onExportDB() {
taskRunner.execute(
exportDBFactory.create { filename ->
if (filename != null) activity.showSendFileScreen(filename)
else activity.showMessage(activity.resources.getString(R.string.could_not_export))
if (filename != null) {
activity.showSendFileScreen(filename)
} else {
activity.showMessage(activity.resources.getString(R.string.could_not_export))
}
}
)
}

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

@ -100,13 +100,19 @@ class CheckmarkButtonView(
}
override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle()
else onEdit()
if (preferences.isShortToggleEnabled) {
performToggle()
} else {
onEdit()
}
}
override fun onLongClick(v: View): Boolean {
if (preferences.isShortToggleEnabled) onEdit()
else performToggle()
if (preferences.isShortToggleEnabled) {
onEdit()
} else {
performToggle()
}
return true
}
@ -140,8 +146,11 @@ class CheckmarkButtonView(
paint.color = when (value) {
YES_MANUAL, YES_AUTO, SKIP -> color
NO -> {
if (preferences.areQuestionMarksEnabled) mediumContrastColor
else lowContrastColor
if (preferences.areQuestionMarksEnabled) {
mediumContrastColor
} else {
lowContrastColor
}
}
else -> lowContrastColor
}
@ -149,8 +158,11 @@ class CheckmarkButtonView(
SKIP -> R.string.fa_skipped
NO -> R.string.fa_times
UNKNOWN -> {
if (preferences.areQuestionMarksEnabled) R.string.fa_question
else R.string.fa_times
if (preferences.areQuestionMarksEnabled) {
R.string.fa_question
} else {
R.string.fa_times
}
}
else -> R.string.fa_check
}

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

@ -225,7 +225,6 @@ class HabitCardView(
}
private fun copyAttributesFrom(h: Habit) {
fun getActiveColor(habit: Habit): Int {
return when (habit.isArchived) {
true -> sres.getColor(R.attr.contrast60)
@ -271,7 +270,6 @@ class HabitCardView(
}
private fun updateBackground(isSelected: Boolean) {
val background = when (isSelected) {
true -> R.drawable.selected_box
false -> R.drawable.ripple

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

@ -88,7 +88,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
habit = habit,
habitList = habitList,
preferences = preferences,
screen = screen,
screen = screen
)
view = ShowHabitView(this)
@ -99,13 +99,13 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
habitList = habitList,
screen = screen,
system = HabitsDirFinder(AndroidDirFinder(this)),
taskRunner = appComponent.taskRunner,
taskRunner = appComponent.taskRunner
)
menu = ShowHabitMenu(
activity = this,
presenter = menuPresenter,
preferences = preferences,
preferences = preferences
)
view.setListener(presenter)
@ -150,7 +150,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
ShowHabitPresenter.buildState(
habit = habit,
preferences = preferences,
theme = themeSwitcher.currentTheme,
theme = themeSwitcher.currentTheme
)
)
}
@ -214,6 +214,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
ShowHabitMenuPresenter.Message.COULD_NOT_EXPORT -> {
showMessage(resources.getString(R.string.could_not_export))
}
else -> {}
}
}

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

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

@ -38,7 +38,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
fun setState(state: BarCardState) {
val androidColor = state.theme.color(state.color).toInt()
binding.chart.view = BarChart(state.theme, JavaLocalDateFormatter(Locale.US)).apply {
binding.chart.view = BarChart(state.theme, JavaLocalDateFormatter(Locale.getDefault())).apply {
series = mutableListOf(state.entries.map { it.value / 1000.0 })
colors = mutableListOf(theme.color(state.color.paletteIndex))
axis = state.entries.map { it.timestamp.toLocalDate() }
@ -63,7 +63,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long,
id: Long
) {
presenter.onBoolSpinnerPosition(position)
}
@ -77,7 +77,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context,
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long,
id: Long
) {
presenter.onNumericalSpinnerPosition(position)
}

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

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

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

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

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

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

Loading…
Cancel
Save