Compare commits
73 Commits
6116ef9450
...
feature/sy
| Author | SHA1 | Date | |
|---|---|---|---|
|
b2cb54e32b
|
|||
|
e42d41ef30
|
|||
|
5498ff8a87
|
|||
|
20dcc7929b
|
|||
|
1283cf979d
|
|||
|
0a493ff065
|
|||
|
16cd249bae
|
|||
|
d136572960
|
|||
|
318aa3c821
|
|||
|
73712e0d10
|
|||
| bf504641c6 | |||
|
2154d8c192
|
|||
|
4c5a722dc5
|
|||
| ee39ff0eda | |||
|
|
6a9c3a36eb | ||
|
|
99ccb44ad3 | ||
|
|
8c0655c352 | ||
|
|
335f8c32fd | ||
|
|
f77c064722 | ||
|
|
8b0b757f04 | ||
| 458c9f3b15 | |||
| dfa74960b3 | |||
| f082842fbe | |||
| d4d818a085 | |||
|
|
f074d0331d | ||
|
|
f94bc62a94 | ||
| b0097fa45e | |||
|
|
15fa1fea8c | ||
| 31368cff45 | |||
| b44dd96dd3 | |||
|
|
08fab0cd8d | ||
|
|
a142685d2e | ||
| a7a95f2030 | |||
|
|
d3c90481be | ||
| f9bb0d7d7b | |||
| f23a1bedee | |||
| dcf31ba115 | |||
|
|
5409a324e8 | ||
|
|
504362e680 | ||
|
|
0ce2f8fae2 | ||
| 2fc6c67432 | |||
| 459cf02642 | |||
| 44cb64601f | |||
|
|
dd47d4cf08 | ||
|
|
8912a9d73c | ||
|
|
9bd1c6f685 | ||
|
|
e15e06034c | ||
|
|
a6180a5049 | ||
|
|
7c69b17e77 | ||
|
|
602a40a532 | ||
|
|
e00998f913 | ||
|
|
af5914c2db | ||
|
|
6c5f70638a | ||
|
|
ba88552919 | ||
|
|
8c90c4f68d | ||
|
|
08eb1a600d | ||
|
|
8ea0480d4a | ||
|
|
36ee39589e | ||
|
|
ac7a721940 | ||
|
|
b36ca8673a | ||
|
|
7cfac486f9 | ||
|
|
21dd413ab5 | ||
|
|
39e10638b5 | ||
| b7232b12cd | |||
| 8cd5b93b47 | |||
| 1b07efe291 | |||
| 232b25bed4 | |||
| 2ea6fe0570 | |||
| bb8b742dc4 | |||
| 85b52a9840 | |||
| 774412492f | |||
| 370ddfb8db | |||
| 1567e2c0ad |
2
.github/workflows/main.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
run: ./build.sh build
|
run: ./build.sh build
|
||||||
|
|
||||||
- name: Run Android tests
|
- name: Run Android tests
|
||||||
run: ./build.sh android-tests-parallel 23 24 25 26 27 28 30 31
|
run: ./build.sh android-tests-parallel 28 29 30 31 32 33
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
5
.gitignore
vendored
@@ -12,13 +12,8 @@
|
|||||||
.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
|
||||||
|
|||||||
@@ -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
|
|
||||||
BIN
.secret/secret
@@ -1,6 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [2.1.0] -- Unreleased
|
## [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)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
plugins {
|
plugins {
|
||||||
val kotlinVersion = "1.6.10"
|
val kotlinVersion = "1.7.10"
|
||||||
id("com.android.application") version ("7.0.3") apply (false)
|
id("com.android.application") version ("7.3.0-rc01") apply (false)
|
||||||
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.android") version kotlinVersion apply (false)
|
||||||
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false)
|
||||||
id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.android.extensions") version kotlinVersion apply (false)
|
||||||
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
|
id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false)
|
||||||
id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
|
id("org.jlleitschuh.gradle.ktlint") version "11.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply {
|
apply {
|
||||||
|
|||||||
4
build.sh
@@ -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;default;x86_64") || return 1
|
(echo "y" | $SDKMANAGER --install "system-images;android-$API;google_apis;x86_64") || return 1
|
||||||
$AVDMANAGER create avd \
|
$AVDMANAGER create avd \
|
||||||
--name $AVDNAME \
|
--name $AVDNAME \
|
||||||
--package "system-images;android-$API;default;x86_64" \
|
--package "system-images;android-$API;google_apis;x86_64" \
|
||||||
--device "Nexus 4" || return 1
|
--device "Nexus 4" || return 1
|
||||||
|
|
||||||
flock -u 10
|
flock -u 10
|
||||||
|
|||||||
@@ -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 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.
|
6. Click the menu "Run" and "uhabits-android". The application should launch.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
2
landing/.gitignore
vendored
@@ -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'" />
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 458 KiB |
7
landing/assets/lib/css/bootstrap.min.css
vendored
2
landing/assets/lib/js/jquery.min.js
vendored
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
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™ 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–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.
|
|
||||||
@@ -32,12 +32,12 @@ tasks.compileLint {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
compileSdk = 31
|
compileSdk = 32
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionCode = 20100
|
versionCode = 20100
|
||||||
versionName = "2.1.0"
|
versionName = "2.1.0"
|
||||||
minSdk = 23
|
minSdk = 28
|
||||||
targetSdk = 31
|
targetSdk = 31
|
||||||
applicationId = "org.isoron.uhabits"
|
applicationId = "org.isoron.uhabits"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
@@ -68,12 +68,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
|
||||||
isCheckReleaseBuilds = false
|
|
||||||
isAbortOnError = false
|
|
||||||
disable("GoogleAppIndexingWarning")
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
targetCompatibility(JavaVersion.VERSION_1_8)
|
targetCompatibility(JavaVersion.VERSION_1_8)
|
||||||
@@ -86,25 +80,25 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
val daggerVersion = "2.41"
|
val daggerVersion = "2.43.2"
|
||||||
val kotlinVersion = "1.6.21"
|
val kotlinVersion = "1.7.10"
|
||||||
val kxCoroutinesVersion = "1.6.1"
|
val kxCoroutinesVersion = "1.6.4"
|
||||||
val ktorVersion = "1.6.8"
|
val ktorVersion = "1.6.8"
|
||||||
val espressoVersion = "3.4.0"
|
val espressoVersion = "3.4.0"
|
||||||
|
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
|
androidTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
|
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
|
||||||
androidTestImplementation("com.google.dagger:dagger:$daggerVersion")
|
androidTestImplementation("com.google.dagger:dagger:$daggerVersion")
|
||||||
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.1")
|
androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito:2.28.3")
|
||||||
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.3.0")
|
androidTestImplementation("androidx.annotation:annotation:1.4.0")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
||||||
androidTestImplementation("androidx.test:rules:1.4.0")
|
androidTestImplementation("androidx.test:rules:1.4.0")
|
||||||
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||||
compileOnly("javax.annotation:jsr250-api:1.0")
|
compileOnly("javax.annotation:jsr250-api:1.0")
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
|
||||||
implementation("com.github.AppIntro:AppIntro:6.2.0")
|
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")
|
||||||
@@ -116,10 +110,10 @@ dependencies {
|
|||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kxCoroutinesVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kxCoroutinesVersion")
|
||||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
implementation("androidx.appcompat:appcompat:1.5.0")
|
||||||
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.5.0")
|
implementation("com.google.android.material:material:1.6.1")
|
||||||
implementation("com.opencsv:opencsv:5.6")
|
implementation("com.opencsv:opencsv:5.6")
|
||||||
implementation(project(":uhabits-core"))
|
implementation(project(":uhabits-core"))
|
||||||
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
|
kapt("com.google.dagger:dagger-compiler:$daggerVersion")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package org.isoron.uhabits.acceptance.steps
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import android.os.Build.VERSION_CODES
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.test.espresso.Espresso
|
import androidx.test.espresso.Espresso
|
||||||
@@ -73,7 +74,7 @@ object CommonSteps : BaseUserInterfaceTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun offsetHeaders() {
|
fun offsetHeaders() {
|
||||||
device.swipe(750, 160, 600, 160, 20)
|
device.swipe(500, 160, 350, 160, 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scrollToText(text: String?) {
|
fun scrollToText(text: String?) {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,14 @@
|
|||||||
android:value=".activities.habits.list.ListHabitsActivity" />
|
android:value=".activities.habits.list.ListHabitsActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activities.sync.SyncActivity"
|
||||||
|
android:label="@string/device_sync">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value=".activities.sync.SyncActivity" />
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.intro.IntroActivity"
|
android:name=".activities.intro.IntroActivity"
|
||||||
android:label=""
|
android:label=""
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ 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;
|
||||||
|
|
||||||
@@ -43,17 +42,13 @@ 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 (isJellybeanOrLater() && view != null && text != null) {
|
if (view != null && text != null) {
|
||||||
view.announceForAccessibility(text);
|
view.announceForAccessibility(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,10 +383,6 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ 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(context!!)
|
val pc = c.toPaletteColor(requireContext())
|
||||||
callback.onColorPicked(pc)
|
callback.onColorPicked(pc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = (activity!!.application as HabitsApplication).component
|
val component = (requireActivity().application as HabitsApplication).component
|
||||||
commandRunner = component.commandRunner
|
commandRunner = component.commandRunner
|
||||||
habit = component.habitList.getById(arguments!!.getLong("habit"))!!
|
habit = component.habitList.getById(requireArguments().getLong("habit"))!!
|
||||||
preferences = component.preferences
|
preferences = component.preferences
|
||||||
|
|
||||||
val themeSwitcher = AndroidThemeSwitcher(activity!!, preferences)
|
val themeSwitcher = AndroidThemeSwitcher(requireActivity(), preferences)
|
||||||
themeSwitcher.apply()
|
themeSwitcher.apply()
|
||||||
|
|
||||||
chart = HistoryChart(
|
chart = HistoryChart(
|
||||||
@@ -71,10 +71,10 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
|||||||
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
|
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
|
||||||
padding = 10.0,
|
padding = 10.0,
|
||||||
)
|
)
|
||||||
dataView = AndroidDataView(context!!, null)
|
dataView = AndroidDataView(requireContext(), null)
|
||||||
dataView.view = chart!!
|
dataView.view = chart!!
|
||||||
|
|
||||||
val dialog = Dialog(context!!).apply {
|
val dialog = Dialog(requireContext()).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)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class WeekdayPickerDialog :
|
|||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val builder = AlertDialog.Builder(
|
val builder = AlertDialog.Builder(
|
||||||
activity!!
|
requireActivity()
|
||||||
)
|
)
|
||||||
builder
|
builder
|
||||||
.setTitle(R.string.select_weekdays)
|
.setTitle(R.string.select_weekdays)
|
||||||
|
|||||||
@@ -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(activity!!, HabitType.YES_NO.value)
|
val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.YES_NO.value)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.buttonMeasurable.setOnClickListener {
|
binding.buttonMeasurable.setOnClickListener {
|
||||||
val intent = IntentFactory().startEditActivity(activity!!, HabitType.NUMERICAL.value)
|
val intent = IntentFactory().startEditActivity(requireActivity(), HabitType.NUMERICAL.value)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
ShowHabitMenuPresenter.Message.COULD_NOT_EXPORT -> {
|
ShowHabitMenuPresenter.Message.COULD_NOT_EXPORT -> {
|
||||||
showMessage(resources.getString(R.string.could_not_export))
|
showMessage(resources.getString(R.string.could_not_export))
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ import android.app.backup.BackupManager
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import android.os.Build
|
|
||||||
import android.os.Build.VERSION.SDK_INT
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -65,7 +63,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
addPreferencesFromResource(R.xml.preferences)
|
addPreferencesFromResource(R.xml.preferences)
|
||||||
val appContext = context!!.applicationContext
|
val appContext = requireContext().applicationContext
|
||||||
if (appContext is HabitsApplication) {
|
if (appContext is HabitsApplication) {
|
||||||
prefs = appContext.component.preferences
|
prefs = appContext.component.preferences
|
||||||
widgetUpdater = appContext.component.widgetUpdater
|
widgetUpdater = appContext.component.widgetUpdater
|
||||||
@@ -98,10 +96,9 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
|||||||
showRingtonePicker()
|
showRingtonePicker()
|
||||||
return true
|
return true
|
||||||
} else if (key == "reminderCustomize") {
|
} else if (key == "reminderCustomize") {
|
||||||
if (SDK_INT < Build.VERSION_CODES.O) return true
|
createAndroidNotificationChannel(requireContext())
|
||||||
createAndroidNotificationChannel(context!!)
|
|
||||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context!!.packageName)
|
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID)
|
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
return true
|
return true
|
||||||
@@ -111,7 +108,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
ringtoneManager = RingtoneManager(activity!!)
|
ringtoneManager = RingtoneManager(requireActivity())
|
||||||
sharedPrefs = preferenceManager.sharedPreferences
|
sharedPrefs = preferenceManager.sharedPreferences
|
||||||
sharedPrefs!!.registerOnSharedPreferenceChangeListener(this)
|
sharedPrefs!!.registerOnSharedPreferenceChangeListener(this)
|
||||||
if (!prefs.isDeveloper) {
|
if (!prefs.isDeveloper) {
|
||||||
@@ -120,12 +117,8 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
|||||||
}
|
}
|
||||||
updateWeekdayPreference()
|
updateWeekdayPreference()
|
||||||
|
|
||||||
if (SDK_INT < Build.VERSION_CODES.O)
|
|
||||||
findPreference("reminderCustomize").isVisible = false
|
|
||||||
else {
|
|
||||||
findPreference("reminderSound").isVisible = false
|
findPreference("reminderSound").isVisible = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateWeekdayPreference() {
|
private fun updateWeekdayPreference() {
|
||||||
val weekdayPref = findPreference("pref_first_weekday") as ListPreference
|
val weekdayPref = findPreference("pref_first_weekday") as ListPreference
|
||||||
@@ -154,8 +147,8 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
|||||||
val pref = findPreference(key)
|
val pref = findPreference(key)
|
||||||
pref.onPreferenceClickListener =
|
pref.onPreferenceClickListener =
|
||||||
Preference.OnPreferenceClickListener {
|
Preference.OnPreferenceClickListener {
|
||||||
activity!!.setResult(result)
|
requireActivity().setResult(result)
|
||||||
activity!!.finish()
|
requireActivity().finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.activities.sync
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import org.isoron.uhabits.HabitsApplication
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||||
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
|
import org.isoron.uhabits.databinding.SyncActivityBinding
|
||||||
|
import org.isoron.uhabits.utils.setupToolbar
|
||||||
|
|
||||||
|
class SyncActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: SyncActivityBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val component = (application as HabitsApplication).component
|
||||||
|
val themeSwitcher = AndroidThemeSwitcher(this, component.preferences)
|
||||||
|
themeSwitcher.apply()
|
||||||
|
|
||||||
|
binding = SyncActivityBinding.inflate(LayoutInflater.from(this))
|
||||||
|
binding.root.setupToolbar(
|
||||||
|
toolbar = binding.toolbar,
|
||||||
|
title = resources.getString(R.string.device_sync),
|
||||||
|
color = PaletteColor(11),
|
||||||
|
theme = themeSwitcher.currentTheme,
|
||||||
|
)
|
||||||
|
binding.generateButton.setOnClickListener { onGenerateCode() }
|
||||||
|
binding.enterButton.setOnClickListener {
|
||||||
|
val et = EditText(this)
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.sync_code)
|
||||||
|
.setView(et)
|
||||||
|
.setPositiveButton(R.string.save) { _, _ ->
|
||||||
|
onEnterCode(et.text.toString())
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
binding.disableButton.setOnClickListener {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.disable_sync)
|
||||||
|
.setMessage(R.string.disable_sync_description)
|
||||||
|
.setPositiveButton(R.string.disable) { _, _ ->
|
||||||
|
onDisableSync()
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.keep_enabled) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentView(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onGenerateCode() {
|
||||||
|
showCodeScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEnterCode(code: String) {
|
||||||
|
showCodeScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDisableSync() {
|
||||||
|
showIntroScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCodeScreen() {
|
||||||
|
binding.introGroup.visibility = View.GONE
|
||||||
|
binding.codeGroup.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showIntroScreen() {
|
||||||
|
binding.introGroup.visibility = View.VISIBLE
|
||||||
|
binding.codeGroup.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,7 +168,6 @@ class AndroidNotificationTray
|
|||||||
fun createAndroidNotificationChannel(context: Context) {
|
fun createAndroidNotificationChannel(context: Context) {
|
||||||
val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE)
|
val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE)
|
||||||
as NotificationManager
|
as NotificationManager
|
||||||
if (SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
REMINDERS_CHANNEL_ID,
|
REMINDERS_CHANNEL_ID,
|
||||||
context.resources.getString(R.string.reminder),
|
context.resources.getString(R.string.reminder),
|
||||||
@@ -177,5 +176,4 @@ class AndroidNotificationTray
|
|||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,18 +21,11 @@ package org.isoron.uhabits.utils
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.KeyguardManager
|
import android.app.KeyguardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.os.Build.VERSION.SDK_INT
|
|
||||||
import android.view.WindowManager
|
|
||||||
|
|
||||||
object SystemUtils {
|
object SystemUtils {
|
||||||
|
|
||||||
fun unlockScreen(activity: Activity) {
|
fun unlockScreen(activity: Activity) {
|
||||||
if (SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||||
km.requestDismissKeyguard(activity, null)
|
km.requestDismissKeyguard(activity, null)
|
||||||
} else {
|
|
||||||
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ package org.isoron.uhabits.widgets
|
|||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import org.isoron.platform.gui.toInt
|
import org.isoron.platform.gui.toInt
|
||||||
import org.isoron.uhabits.core.models.Entry
|
import org.isoron.uhabits.core.models.Entry
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
@@ -49,7 +47,6 @@ open class CheckmarkWidget(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
override fun refreshData(widgetView: View) {
|
override fun refreshData(widgetView: View) {
|
||||||
(widgetView as CheckmarkWidgetView).apply {
|
(widgetView as CheckmarkWidgetView).apply {
|
||||||
val today = DateUtils.getTodayWithOffset()
|
val today = DateUtils.getTodayWithOffset()
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
<!--
|
|
||||||
~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
~
|
|
||||||
~ This file is part of Loop Habit Tracker.
|
|
||||||
~
|
|
||||||
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
~ it under the terms of the GNU General Public License as published by the
|
|
||||||
~ Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
~ option) any later version.
|
|
||||||
~
|
|
||||||
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
~ more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License along
|
|
||||||
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item
|
|
||||||
android:top="1dp"
|
|
||||||
android:left="1dp"
|
|
||||||
android:bottom="3dp"
|
|
||||||
android:right="3dp">
|
|
||||||
<ripple
|
|
||||||
android:color="#60ffffff">
|
|
||||||
|
|
||||||
<item android:id="@android:id/mask">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<corners android:radius="5dp"/>
|
|
||||||
<solid android:color="?android:colorPrimary"/>
|
|
||||||
</shape>
|
|
||||||
<color android:color="@color/white"/>
|
|
||||||
</item>
|
|
||||||
</ripple>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
</layer-list>
|
|
||||||
55
uhabits-android/src/main/res/drawable/ic_sync_dark.xml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<!--
|
||||||
|
~ 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/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="243dp"
|
||||||
|
android:height="85dp"
|
||||||
|
android:viewportWidth="243"
|
||||||
|
android:viewportHeight="85">
|
||||||
|
<path
|
||||||
|
android:pathData="M44.354,0H7.827C3.506,0 0,3.569 0,7.969V77.031C0,81.431 3.506,85 7.827,85H44.354C48.676,85 52.182,81.431 52.182,77.031V7.969C52.182,3.569 48.676,0 44.354,0ZM26.091,79.688C23.205,79.688 20.873,77.313 20.873,74.375C20.873,71.437 23.205,69.063 26.091,69.063C28.977,69.063 31.309,71.437 31.309,74.375C31.309,77.313 28.977,79.688 26.091,79.688ZM44.354,61.758C44.354,62.854 43.474,63.75 42.398,63.75H9.784C8.708,63.75 7.827,62.854 7.827,61.758V9.961C7.827,8.865 8.708,7.969 9.784,7.969H42.398C43.474,7.969 44.354,8.865 44.354,9.961V61.758Z"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
<path
|
||||||
|
android:pathData="M234.536,0H198.009C193.688,0 190.182,3.569 190.182,7.969V77.031C190.182,81.431 193.688,85 198.009,85H234.536C238.858,85 242.364,81.431 242.364,77.031V7.969C242.364,3.569 238.858,0 234.536,0ZM216.273,79.688C213.386,79.688 211.055,77.313 211.055,74.375C211.055,71.437 213.386,69.063 216.273,69.063C219.159,69.063 221.491,71.437 221.491,74.375C221.491,77.313 219.159,79.688 216.273,79.688ZM234.536,61.758C234.536,62.854 233.656,63.75 232.58,63.75H199.966C198.89,63.75 198.009,62.854 198.009,61.758V9.961C198.009,8.865 198.89,7.969 199.966,7.969H232.58C233.656,7.969 234.536,8.865 234.536,9.961V61.758Z"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
<path
|
||||||
|
android:pathData="M68.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
<path
|
||||||
|
android:pathData="M110.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
<path
|
||||||
|
android:pathData="M96.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
<path
|
||||||
|
android:pathData="M82.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
<path
|
||||||
|
android:pathData="M124.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
<path
|
||||||
|
android:pathData="M166.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
<path
|
||||||
|
android:pathData="M152.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
<path
|
||||||
|
android:pathData="M138.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#ffffff" />
|
||||||
|
</vector>
|
||||||
36
uhabits-android/src/main/res/drawable/ic_sync_light.xml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="243dp"
|
||||||
|
android:height="85dp"
|
||||||
|
android:viewportWidth="243"
|
||||||
|
android:viewportHeight="85">
|
||||||
|
<path
|
||||||
|
android:pathData="M44.354,0H7.827C3.506,0 0,3.569 0,7.969V77.031C0,81.431 3.506,85 7.827,85H44.354C48.676,85 52.182,81.431 52.182,77.031V7.969C52.182,3.569 48.676,0 44.354,0ZM26.091,79.688C23.205,79.688 20.873,77.313 20.873,74.375C20.873,71.437 23.205,69.063 26.091,69.063C28.977,69.063 31.309,71.437 31.309,74.375C31.309,77.313 28.977,79.688 26.091,79.688ZM44.354,61.758C44.354,62.854 43.474,63.75 42.398,63.75H9.784C8.708,63.75 7.827,62.854 7.827,61.758V9.961C7.827,8.865 8.708,7.969 9.784,7.969H42.398C43.474,7.969 44.354,8.865 44.354,9.961V61.758Z"
|
||||||
|
android:fillColor="#000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M234.536,0H198.009C193.688,0 190.182,3.569 190.182,7.969V77.031C190.182,81.431 193.688,85 198.009,85H234.536C238.858,85 242.364,81.431 242.364,77.031V7.969C242.364,3.569 238.858,0 234.536,0ZM216.273,79.688C213.386,79.688 211.055,77.313 211.055,74.375C211.055,71.437 213.386,69.063 216.273,69.063C219.159,69.063 221.491,71.437 221.491,74.375C221.491,77.313 219.159,79.688 216.273,79.688ZM234.536,61.758C234.536,62.854 233.656,63.75 232.58,63.75H199.966C198.89,63.75 198.009,62.854 198.009,61.758V9.961C198.009,8.865 198.89,7.969 199.966,7.969H232.58C233.656,7.969 234.536,8.865 234.536,9.961V61.758Z"
|
||||||
|
android:fillColor="#000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M68.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M110.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M96.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M82.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M124.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M166.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M152.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M138.182,42.5a4,4.048 0,1 0,8 0a4,4.048 0,1 0,-8 0z"
|
||||||
|
android:fillColor="#000000" />
|
||||||
|
</vector>
|
||||||
@@ -17,18 +17,23 @@
|
|||||||
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:state_enabled="true" android:state_pressed="true">
|
<item
|
||||||
<layer-list>
|
android:top="1dp"
|
||||||
<item android:bottom="3dp"
|
|
||||||
android:left="1dp"
|
android:left="1dp"
|
||||||
android:right="3dp"
|
android:bottom="3dp"
|
||||||
android:top="1dp">
|
android:right="3dp">
|
||||||
|
<ripple
|
||||||
|
android:color="#60ffffff">
|
||||||
|
|
||||||
|
<item android:id="@android:id/mask">
|
||||||
<shape android:shape="rectangle">
|
<shape android:shape="rectangle">
|
||||||
<corners android:radius="5dp"/>
|
<corners android:radius="5dp"/>
|
||||||
<solid android:color="#30ffffff"/>
|
<solid android:color="?android:colorPrimary"/>
|
||||||
</shape>
|
</shape>
|
||||||
|
<color android:color="@color/white"/>
|
||||||
</item>
|
</item>
|
||||||
</layer-list>
|
</ripple>
|
||||||
</item>
|
</item>
|
||||||
</selector>
|
|
||||||
|
</layer-list>
|
||||||
|
|||||||
162
uhabits-android/src/main/res/layout/sync_activity.xml
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<!--
|
||||||
|
~ 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/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="?attr/contrast0"
|
||||||
|
android:gravity="top|center">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
app:popupTheme="?toolbarPopupTheme"
|
||||||
|
style="@style/Toolbar" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/introGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="top|center">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:srcCompat="?attr/iconSync"
|
||||||
|
android:layout_margin="32dp"
|
||||||
|
android:alpha="0.25" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/device_sync_description_1"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/device_sync_description_2"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/generate_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/generate_new_code"
|
||||||
|
app:backgroundTint="?attr/aboutScreenColor"
|
||||||
|
android:textColor="?attr/contrast0"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:id="@+id/enter_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/enter_existing_code"
|
||||||
|
android:textColor="?attr/aboutScreenColor"
|
||||||
|
app:rippleColor="?attr/aboutScreenColor"
|
||||||
|
app:strokeColor="?attr/aboutScreenColor"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp" />
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/codeGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/sync_is_enabled"
|
||||||
|
android:layout_margin="16dp" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
style="@style/FormOuterBox"
|
||||||
|
android:layout_marginLeft="12dp"
|
||||||
|
android:layout_marginRight="12dp">
|
||||||
|
|
||||||
|
<LinearLayout style="@style/FormInnerBox">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/FormLabel"
|
||||||
|
android:text="@string/sync_code" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/FormInput"
|
||||||
|
android:id="@+id/sync_code_tv"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:text="gravity trophy suspect shrimp sheriff avocado label trust tragic dove pitch title network myself task spell protect smooth diary sword brain blossom bulb under" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
style="@style/FormOuterBox"
|
||||||
|
android:layout_marginLeft="12dp"
|
||||||
|
android:layout_marginRight="12dp">
|
||||||
|
|
||||||
|
<LinearLayout style="@style/FormInnerBox">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/FormLabel"
|
||||||
|
android:text="@string/last_sync" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/FormInput"
|
||||||
|
android:id="@+id/last_sync_tv"
|
||||||
|
android:text="Jan 10, 2022 4:45:00 PM" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:id="@+id/disable_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/disable_sync"
|
||||||
|
android:textColor="?attr/aboutScreenColor"
|
||||||
|
app:rippleColor="?attr/aboutScreenColor"
|
||||||
|
app:strokeColor="?attr/aboutScreenColor"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
0
uhabits-android/src/main/res/mipmap-anydpi
Normal file
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
~
|
|
||||||
~ This file is part of Loop Habit Tracker.
|
|
||||||
~
|
|
||||||
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
~ it under the terms of the GNU General Public License as published by the
|
|
||||||
~ Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
~ option) any later version.
|
|
||||||
~
|
|
||||||
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
~ more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License along
|
|
||||||
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
<attr name="iconFilter" format="reference"/>
|
<attr name="iconFilter" format="reference"/>
|
||||||
<attr name="iconArrowUp" format="reference"/>
|
<attr name="iconArrowUp" format="reference"/>
|
||||||
<attr name="iconArrowDown" format="reference"/>
|
<attr name="iconArrowDown" format="reference"/>
|
||||||
|
<attr name="iconSync" format="reference"/>
|
||||||
<attr name="dialogFormLabelColor" format="reference"/>
|
<attr name="dialogFormLabelColor" format="reference"/>
|
||||||
|
|
||||||
<attr name="toolbarPopupTheme" format="reference"/>
|
<attr name="toolbarPopupTheme" format="reference"/>
|
||||||
|
|||||||
@@ -233,4 +233,18 @@
|
|||||||
<string name="activity_not_found">No app was found to support this action</string>
|
<string name="activity_not_found">No app was found to support this action</string>
|
||||||
<string name="pref_midnight_delay_title">Extend day a few hours past midnight</string>
|
<string name="pref_midnight_delay_title">Extend day a few hours past midnight</string>
|
||||||
<string name="pref_midnight_delay_description">Wait until 3:00 AM to show a new day. Useful if you typically go to sleep after midnight. Requires app restart.</string>
|
<string name="pref_midnight_delay_description">Wait until 3:00 AM to show a new day. Useful if you typically go to sleep after midnight. Requires app restart.</string>
|
||||||
|
<string name="device_sync">Device sync</string>
|
||||||
|
<string name="config_sync">Configure device sync</string>
|
||||||
|
<string name="config_sync_summary">Synchronize data across multiple devices. When enabled, an end-to-end encrypted copy of your data will be uploaded to Loop Habit Tracker servers.</string>
|
||||||
|
<string name="device_sync_description_1">Device sync allows you to keep your data synchronized across multiple devices. To get started, generate a new sync code below, install Loop Habit Tracker in another device, then type the generated code there.</string>
|
||||||
|
<string name="device_sync_description_2">When sync is enabled, an end-to-end encrypted copy of your data will be uploaded to Loop Habit Tracker servers. See privacy policy for more details.</string>
|
||||||
|
<string name="generate_new_code">Generate new code</string>
|
||||||
|
<string name="enter_existing_code">Enter existing code</string>
|
||||||
|
<string name="sync_is_enabled">Device sync is enabled. To get started, install Loop in another device, then type the following code there.</string>
|
||||||
|
<string name="sync_code">Sync code</string>
|
||||||
|
<string name="disable_sync">Disable sync</string>
|
||||||
|
<string name="disable_sync_description">Are you sure you want to disable sync on this device? This will not delete any data from any of your devices, but the current device will no longer be kept in sync with the others. If you disable sync from all devices, your data will be deleted from our servers in 30 days.</string>
|
||||||
|
<string name="disable">Disable</string>
|
||||||
|
<string name="keep_enabled">Keep enabled</string>
|
||||||
|
<string name="last_sync">Last sync</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
<item name="iconUnarchive">@drawable/ic_action_unarchive_dark</item>
|
<item name="iconUnarchive">@drawable/ic_action_unarchive_dark</item>
|
||||||
<item name="iconArrowUp">@drawable/ic_arrow_up_light</item>
|
<item name="iconArrowUp">@drawable/ic_arrow_up_light</item>
|
||||||
<item name="iconArrowDown">@drawable/ic_arrow_down_light</item>
|
<item name="iconArrowDown">@drawable/ic_arrow_down_light</item>
|
||||||
|
<item name="iconSync">@drawable/ic_sync_light</item>
|
||||||
<item name="contrast0">@color/white</item>
|
<item name="contrast0">@color/white</item>
|
||||||
<item name="contrast20">@color/grey_300</item>
|
<item name="contrast20">@color/grey_300</item>
|
||||||
<item name="contrast40">@color/grey_350</item>
|
<item name="contrast40">@color/grey_350</item>
|
||||||
@@ -89,6 +90,7 @@
|
|||||||
<item name="iconUnarchive">@drawable/ic_action_unarchive_dark</item>
|
<item name="iconUnarchive">@drawable/ic_action_unarchive_dark</item>
|
||||||
<item name="iconArrowUp">@drawable/ic_arrow_up_dark</item>
|
<item name="iconArrowUp">@drawable/ic_arrow_up_dark</item>
|
||||||
<item name="iconArrowDown">@drawable/ic_arrow_down_dark</item>
|
<item name="iconArrowDown">@drawable/ic_arrow_down_dark</item>
|
||||||
|
<item name="iconSync">@drawable/ic_sync_dark</item>
|
||||||
<item name="contrast0">@color/grey_900</item>
|
<item name="contrast0">@color/grey_900</item>
|
||||||
<item name="contrast20">@color/grey_800</item>
|
<item name="contrast20">@color/grey_800</item>
|
||||||
<item name="contrast40">@color/grey_750</item>
|
<item name="contrast40">@color/grey_750</item>
|
||||||
|
|||||||
@@ -107,6 +107,24 @@
|
|||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
android:key="syncCategory"
|
||||||
|
android:title="@string/device_sync">
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="configSync"
|
||||||
|
android:summary="@string/config_sync_summary"
|
||||||
|
android:title="@string/config_sync"
|
||||||
|
app:iconSpaceReserved="false">
|
||||||
|
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetClass="org.isoron.uhabits.activities.sync.SyncActivity"
|
||||||
|
android:targetPackage="org.isoron.uhabits" />
|
||||||
|
</Preference>
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:key="databaseCategory"
|
android:key="databaseCategory"
|
||||||
android:title="@string/database">
|
android:title="@string/database">
|
||||||
|
|||||||
2048
uhabits-core/assets/main/bip39/chinese_simplified.txt
Normal file
2048
uhabits-core/assets/main/bip39/chinese_traditional.txt
Normal file
2048
uhabits-core/assets/main/bip39/czech.txt
Normal file
2048
uhabits-core/assets/main/bip39/en_US.txt
Normal file
2048
uhabits-core/assets/main/bip39/french.txt
Normal file
2048
uhabits-core/assets/main/bip39/italian.txt
Normal file
2048
uhabits-core/assets/main/bip39/japanese.txt
Normal file
2048
uhabits-core/assets/main/bip39/korean.txt
Normal file
2048
uhabits-core/assets/main/bip39/portuguese.txt
Normal file
2048
uhabits-core/assets/main/bip39/spanish.txt
Normal file
@@ -43,11 +43,11 @@ kotlin {
|
|||||||
val jvmMain by getting {
|
val jvmMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
compileOnly("com.google.dagger:dagger:2.41")
|
compileOnly("com.google.dagger:dagger:2.43.2")
|
||||||
implementation("com.google.guava:guava:31.1-android")
|
implementation("com.google.guava:guava:31.1-android")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.21")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.10")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4")
|
||||||
implementation("androidx.annotation:annotation:1.3.0")
|
implementation("androidx.annotation:annotation:1.4.0")
|
||||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||||
implementation("com.opencsv:opencsv:5.6")
|
implementation("com.opencsv:opencsv:5.6")
|
||||||
implementation("commons-codec:commons-codec:1.15")
|
implementation("commons-codec:commons-codec:1.15")
|
||||||
@@ -59,7 +59,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
implementation(kotlin("test-junit"))
|
implementation(kotlin("test-junit"))
|
||||||
implementation("org.xerial:sqlite-jdbc:3.36.0.3")
|
implementation("org.xerial:sqlite-jdbc:3.39.2.1")
|
||||||
implementation("org.hamcrest:hamcrest:2.2")
|
implementation("org.hamcrest:hamcrest:2.2")
|
||||||
implementation("org.apache.commons:commons-io:1.3.2")
|
implementation("org.apache.commons:commons-io:1.3.2")
|
||||||
implementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
implementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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.platform.crypto
|
||||||
|
|
||||||
|
class Bip39(private val wordlist: List<String>, private val crypto: Crypto) {
|
||||||
|
private fun computeChecksum(entropy: List<Boolean>): List<Boolean> {
|
||||||
|
val sha256 = crypto.sha256()
|
||||||
|
var byte = 0
|
||||||
|
entropy.forEachIndexed { i, bit ->
|
||||||
|
byte = byte shl 1
|
||||||
|
if (bit) byte += 1
|
||||||
|
if (i.rem(8) == 7) {
|
||||||
|
sha256.update(byte.toByte())
|
||||||
|
byte = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sha256.finalize().toBits().subList(0, entropy.size / 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encode(entropy: ByteArray): List<String> {
|
||||||
|
val entropyBits = entropy.toBits()
|
||||||
|
val msg = entropyBits + computeChecksum(entropyBits)
|
||||||
|
var wordIndex = 0
|
||||||
|
val mnemonic = mutableListOf<String>()
|
||||||
|
msg.forEachIndexed { i, bit ->
|
||||||
|
wordIndex = wordIndex shl 1
|
||||||
|
if (bit) wordIndex += 1
|
||||||
|
if (i.rem(11) == 10) {
|
||||||
|
mnemonic.add(wordlist[wordIndex])
|
||||||
|
wordIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mnemonic
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decode(mnemonic: List<String>): ByteArray {
|
||||||
|
val bits = mutableListOf<Boolean>()
|
||||||
|
mnemonic.forEach { word ->
|
||||||
|
val wordBits = mutableListOf<Boolean>()
|
||||||
|
var wordIndex = wordlist.binarySearch(word)
|
||||||
|
if (wordIndex < 0) throw InvalidWordException(word)
|
||||||
|
for (it in 0..10) {
|
||||||
|
wordBits.add(wordIndex.rem(2) == 1)
|
||||||
|
wordIndex = wordIndex shr 1
|
||||||
|
}
|
||||||
|
bits.addAll(wordBits.reversed())
|
||||||
|
}
|
||||||
|
if (bits.size.rem(33) != 0) throw InvalidMnemonicLength()
|
||||||
|
val checksumSize = bits.size / 33
|
||||||
|
val checksum = bits.subList(bits.size - checksumSize, bits.size)
|
||||||
|
val entropy = bits.subList(0, bits.size - checksumSize)
|
||||||
|
if (computeChecksum(entropy) != checksum) throw InvalidChecksumException()
|
||||||
|
return byteArray(entropy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidChecksumException : Exception()
|
||||||
|
class InvalidWordException(word: String) : Exception(word)
|
||||||
|
class InvalidMnemonicLength : Exception()
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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.platform.crypto
|
||||||
|
|
||||||
|
class Key(val bytes: ByteArray)
|
||||||
|
|
||||||
|
interface Crypto {
|
||||||
|
fun sha256(): Sha256
|
||||||
|
fun hmacSha256(): HmacSha256
|
||||||
|
fun aesGcm(key: Key): AesGcm
|
||||||
|
fun secureRandomBytes(numBytes: Int): ByteArray
|
||||||
|
|
||||||
|
fun generateKey(): Key {
|
||||||
|
return Key(secureRandomBytes(32))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deriveKey(master: Key, subkeyName: String): Key {
|
||||||
|
val mac = hmacSha256()
|
||||||
|
mac.init(master.bytes)
|
||||||
|
mac.update(subkeyName)
|
||||||
|
return Key(mac.finalize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Sha256 {
|
||||||
|
fun update(byte: Byte)
|
||||||
|
fun finalize(): ByteArray
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HmacSha256 {
|
||||||
|
fun init(key: ByteArray)
|
||||||
|
fun update(byte: Byte)
|
||||||
|
fun finalize(): ByteArray
|
||||||
|
|
||||||
|
fun update(msg: String) {
|
||||||
|
for (b in msg.encodeToByteArray()) {
|
||||||
|
update(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AesGcm {
|
||||||
|
fun encrypt(msg: ByteArray, iv: ByteArray): ByteArray
|
||||||
|
fun decrypt(cipherText: ByteArray): ByteArray
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Byte.toBits(): List<Boolean> = (7 downTo 0).map { (toInt() and (1 shl it)) != 0 }
|
||||||
|
fun ByteArray.toBits(): List<Boolean> = flatMap { it.toBits() }
|
||||||
|
fun byteArrayOfInts(vararg b: Int) = b.map { it.toByte() }.toByteArray()
|
||||||
|
fun byteArray(bits: List<Boolean>): ByteArray {
|
||||||
|
var byte = 0
|
||||||
|
val bytes = ByteArray(bits.size / 8)
|
||||||
|
bits.forEachIndexed { i, b ->
|
||||||
|
byte = byte shl 1
|
||||||
|
if (b) byte += 1
|
||||||
|
if (i.rem(8) == 7) {
|
||||||
|
bytes[i / 8] = byte.toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
@@ -17,21 +17,9 @@
|
|||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.server
|
package org.isoron.uhabits.core.sync
|
||||||
|
|
||||||
import org.isoron.uhabits.sync.*
|
|
||||||
import org.isoron.uhabits.sync.links.*
|
|
||||||
|
|
||||||
interface AbstractSyncServer {
|
interface AbstractSyncServer {
|
||||||
/**
|
|
||||||
* Generates and returns a new sync key, which can be used to store and retrive
|
|
||||||
* data.
|
|
||||||
*
|
|
||||||
* @throws ServiceUnavailable If key cannot be generated at this time, for example,
|
|
||||||
* due to insufficient server resources, temporary server maintenance or network problems.
|
|
||||||
*/
|
|
||||||
suspend fun register(): String
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces data for a given sync key.
|
* Replaces data for a given sync key.
|
||||||
*
|
*
|
||||||
@@ -60,22 +48,4 @@ interface AbstractSyncServer {
|
|||||||
* to insufficient server resources or network problems.
|
* to insufficient server resources or network problems.
|
||||||
*/
|
*/
|
||||||
suspend fun getDataVersion(key: String): Long
|
suspend fun getDataVersion(key: String): Long
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a new temporary link (mapping to the given sync key) and returns it.
|
|
||||||
*
|
|
||||||
* @throws ServiceUnavailable If the link cannot be generated at this time due to
|
|
||||||
* insufficient server resources.
|
|
||||||
*/
|
|
||||||
suspend fun registerLink(syncKey: String): Link
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the syncKey associated with the given link id.
|
|
||||||
*
|
|
||||||
* @throws ServiceUnavailable If the link cannot be resolved at this time due to
|
|
||||||
* insufficient server resources.
|
|
||||||
* @throws KeyNotFoundException If the link id cannot be found, or if it has
|
|
||||||
* expired.
|
|
||||||
*/
|
|
||||||
suspend fun getLink(id: String): Link
|
|
||||||
}
|
}
|
||||||
@@ -17,9 +17,7 @@
|
|||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.sync
|
package org.isoron.uhabits.core.sync
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.*
|
|
||||||
|
|
||||||
data class SyncData(
|
data class SyncData(
|
||||||
val version: Long,
|
val version: Long,
|
||||||
@@ -29,7 +27,3 @@ data class SyncData(
|
|||||||
data class RegisterReponse(val key: String)
|
data class RegisterReponse(val key: String)
|
||||||
|
|
||||||
data class GetDataVersionResponse(val version: Long)
|
data class GetDataVersionResponse(val version: Long)
|
||||||
|
|
||||||
val defaultMapper = ObjectMapper()
|
|
||||||
fun SyncData.toJson(): String = defaultMapper.writeValueAsString(this)
|
|
||||||
fun GetDataVersionResponse.toJson(): String = defaultMapper.writeValueAsString(this)
|
|
||||||
@@ -17,12 +17,12 @@
|
|||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.sync
|
package org.isoron.uhabits.core.sync
|
||||||
|
|
||||||
open class SyncException: RuntimeException()
|
open class SyncException : RuntimeException()
|
||||||
|
|
||||||
class KeyNotFoundException: SyncException()
|
class KeyNotFoundException : SyncException()
|
||||||
|
|
||||||
class ServiceUnavailable: SyncException()
|
class ServiceUnavailable : SyncException()
|
||||||
|
|
||||||
class EditConflictException: SyncException()
|
class EditConflictException : SyncException()
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* 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.platform.crypto
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
class JavaCrypto : Crypto {
|
||||||
|
override fun sha256() = JavaSha256()
|
||||||
|
override fun hmacSha256() = JavaHmacSha256()
|
||||||
|
override fun aesGcm(key: Key) = JavaAesGcm(key)
|
||||||
|
|
||||||
|
override fun secureRandomBytes(numBytes: Int): ByteArray {
|
||||||
|
val sr = SecureRandom()
|
||||||
|
val bytes = ByteArray(numBytes)
|
||||||
|
sr.nextBytes(bytes)
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JavaSha256 : Sha256 {
|
||||||
|
private val md = MessageDigest.getInstance("SHA-256")
|
||||||
|
override fun update(byte: Byte) = md.update(byte)
|
||||||
|
override fun finalize(): ByteArray = md.digest()
|
||||||
|
}
|
||||||
|
|
||||||
|
class JavaHmacSha256 : HmacSha256 {
|
||||||
|
private val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
override fun init(key: ByteArray) = mac.init(SecretKeySpec(key, "HmacSHA256"))
|
||||||
|
override fun update(byte: Byte) = mac.update(byte)
|
||||||
|
override fun finalize(): ByteArray = mac.doFinal()
|
||||||
|
}
|
||||||
|
|
||||||
|
class JavaAesGcm(val key: Key) : AesGcm {
|
||||||
|
override fun encrypt(msg: ByteArray, iv: ByteArray): ByteArray {
|
||||||
|
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key.bytes, "AES"), GCMParameterSpec(128, iv))
|
||||||
|
val encrypted = cipher.doFinal(msg)
|
||||||
|
return ByteBuffer
|
||||||
|
.allocate(iv.size + encrypted.size)
|
||||||
|
.put(iv)
|
||||||
|
.put(encrypted)
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
override fun decrypt(cipherText: ByteArray): ByteArray {
|
||||||
|
val buffer = ByteBuffer.wrap(cipherText)
|
||||||
|
val iv = ByteArray(12)
|
||||||
|
buffer.get(iv)
|
||||||
|
val encrypted = ByteArray(buffer.remaining())
|
||||||
|
buffer.get(encrypted)
|
||||||
|
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key.bytes, "AES"), GCMParameterSpec(128, iv))
|
||||||
|
return cipher.doFinal(encrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.toHexString(): String {
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (b in this) sb.append(String.format("%02x", b))
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* 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.platform.crypto
|
||||||
|
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.isoron.platform.io.JavaFileOpener
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class Bip39Test {
|
||||||
|
|
||||||
|
private lateinit var bip39: Bip39
|
||||||
|
|
||||||
|
private val phrases = listOf(
|
||||||
|
listOf(
|
||||||
|
"gather",
|
||||||
|
"capable",
|
||||||
|
"since",
|
||||||
|
),
|
||||||
|
listOf(
|
||||||
|
"exit",
|
||||||
|
"churn",
|
||||||
|
"hazard",
|
||||||
|
"garage",
|
||||||
|
"hint",
|
||||||
|
"great",
|
||||||
|
),
|
||||||
|
listOf(
|
||||||
|
"exile",
|
||||||
|
"blouse",
|
||||||
|
"athlete",
|
||||||
|
"dinner",
|
||||||
|
"chef",
|
||||||
|
"home",
|
||||||
|
"destroy",
|
||||||
|
"disagree",
|
||||||
|
"select",
|
||||||
|
"eight",
|
||||||
|
"slim",
|
||||||
|
"talent",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val entropies = listOf(
|
||||||
|
byteArrayOfInts(0x60, 0x64, 0x3f, 0x24),
|
||||||
|
byteArrayOfInts(0x4f, 0xe5, 0x19, 0xa7, 0xaf, 0xb6, 0xbc, 0xcc),
|
||||||
|
byteArrayOfInts(
|
||||||
|
0x4f,
|
||||||
|
0xa3,
|
||||||
|
0x04,
|
||||||
|
0x38,
|
||||||
|
0x9f,
|
||||||
|
0x22,
|
||||||
|
0x74,
|
||||||
|
0xda,
|
||||||
|
0x0f,
|
||||||
|
0x09,
|
||||||
|
0xf6,
|
||||||
|
0xc3,
|
||||||
|
0x48,
|
||||||
|
0xdf,
|
||||||
|
0x2f,
|
||||||
|
0x6e,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() = runBlocking {
|
||||||
|
bip39 = Bip39(JavaFileOpener().openResourceFile("bip39/en_US.txt").lines(), JavaCrypto())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_encode_decode() {
|
||||||
|
phrases.zip(entropies).forEach { (phrase, entropy) ->
|
||||||
|
assertEquals(phrase, bip39.encode(entropy))
|
||||||
|
assertEquals(entropy.toHexString(), bip39.decode(phrase).toHexString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_decode_invalid_checksum() {
|
||||||
|
assertFailsWith<InvalidChecksumException> {
|
||||||
|
bip39.decode(
|
||||||
|
listOf(
|
||||||
|
"lawn",
|
||||||
|
"dirt",
|
||||||
|
"work",
|
||||||
|
"mountain",
|
||||||
|
"depth",
|
||||||
|
"loyal",
|
||||||
|
"citizen",
|
||||||
|
"theory",
|
||||||
|
"cram",
|
||||||
|
"trip",
|
||||||
|
"boil",
|
||||||
|
"about",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_decode_invalid_word() {
|
||||||
|
assertFailsWith<InvalidWordException> {
|
||||||
|
bip39.decode(listOf("dirt", "bee", "work"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* 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.platform.crypto
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotEquals
|
||||||
|
|
||||||
|
class CryptoTest {
|
||||||
|
private val crypto = JavaCrypto()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_sha256() {
|
||||||
|
val sha256 = crypto.sha256()
|
||||||
|
sha256.update(0x10.toByte())
|
||||||
|
sha256.update(0x20.toByte())
|
||||||
|
sha256.update(0x30.toByte())
|
||||||
|
val digest = sha256.finalize()
|
||||||
|
assertEquals(32, digest.size)
|
||||||
|
assertEquals(0x8e.toByte(), digest[0])
|
||||||
|
assertEquals(0x13.toByte(), digest[1])
|
||||||
|
assertEquals(0x36.toByte(), digest[2])
|
||||||
|
assertEquals(0xb9.toByte(), digest[31])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_hmacsha256() {
|
||||||
|
val hmac = crypto.hmacSha256()
|
||||||
|
hmac.init(byteArrayOfInts(0x01, 0x02, 0x03))
|
||||||
|
hmac.update(0x40.toByte())
|
||||||
|
hmac.update("AB")
|
||||||
|
val checksum = hmac.finalize()
|
||||||
|
assertEquals(32, checksum.size)
|
||||||
|
assertEquals(0x6d.toByte(), checksum[0])
|
||||||
|
assertEquals(0xc9.toByte(), checksum[1])
|
||||||
|
assertEquals(0x05.toByte(), checksum[2])
|
||||||
|
assertEquals(0xa1.toByte(), checksum[31])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_aes_gcm() {
|
||||||
|
val msg = byteArrayOfInts(
|
||||||
|
0x2f,
|
||||||
|
0xdc,
|
||||||
|
0xaa,
|
||||||
|
0x41,
|
||||||
|
0xfa,
|
||||||
|
0xb8,
|
||||||
|
0x5e,
|
||||||
|
0xe8,
|
||||||
|
0xa3,
|
||||||
|
0x12,
|
||||||
|
0x69,
|
||||||
|
0x68,
|
||||||
|
0x14,
|
||||||
|
0x31,
|
||||||
|
0xd8,
|
||||||
|
0x59,
|
||||||
|
0x74,
|
||||||
|
0x29,
|
||||||
|
0x2e,
|
||||||
|
0xae,
|
||||||
|
0xed,
|
||||||
|
0x76,
|
||||||
|
0x0a,
|
||||||
|
0x56,
|
||||||
|
0x46,
|
||||||
|
0x90,
|
||||||
|
0xb6,
|
||||||
|
0xcb,
|
||||||
|
0x9f,
|
||||||
|
0x37,
|
||||||
|
0xbe,
|
||||||
|
0xae,
|
||||||
|
)
|
||||||
|
|
||||||
|
val key = Key(
|
||||||
|
byteArrayOfInts(
|
||||||
|
0xed,
|
||||||
|
0xa8,
|
||||||
|
0xc3,
|
||||||
|
0xc6,
|
||||||
|
0x44,
|
||||||
|
0x1e,
|
||||||
|
0xa1,
|
||||||
|
0xd5,
|
||||||
|
0x71,
|
||||||
|
0x8c,
|
||||||
|
0x71,
|
||||||
|
0x45,
|
||||||
|
0xbe,
|
||||||
|
0x2d,
|
||||||
|
0xf7,
|
||||||
|
0xa4,
|
||||||
|
0x81,
|
||||||
|
0x2e,
|
||||||
|
0x0a,
|
||||||
|
0x0b,
|
||||||
|
0xa8,
|
||||||
|
0xe4,
|
||||||
|
0x20,
|
||||||
|
0x49,
|
||||||
|
0x94,
|
||||||
|
0x8a,
|
||||||
|
0x71,
|
||||||
|
0x1a,
|
||||||
|
0x15,
|
||||||
|
0xf5,
|
||||||
|
0x29,
|
||||||
|
0x78,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val iv = byteArrayOfInts(
|
||||||
|
0xa7,
|
||||||
|
0xef,
|
||||||
|
0xe1,
|
||||||
|
0xba,
|
||||||
|
0xdf,
|
||||||
|
0x4f,
|
||||||
|
0x85,
|
||||||
|
0xca,
|
||||||
|
0xc3,
|
||||||
|
0x81,
|
||||||
|
0xc1,
|
||||||
|
0x93,
|
||||||
|
)
|
||||||
|
|
||||||
|
val expected = byteArrayOfInts(
|
||||||
|
// iv
|
||||||
|
0xa7,
|
||||||
|
0xef,
|
||||||
|
0xe1,
|
||||||
|
0xba,
|
||||||
|
0xdf,
|
||||||
|
0x4f,
|
||||||
|
0x85,
|
||||||
|
0xca,
|
||||||
|
0xc3,
|
||||||
|
0x81,
|
||||||
|
0xc1,
|
||||||
|
0x93,
|
||||||
|
// msg
|
||||||
|
0x24,
|
||||||
|
0xe7,
|
||||||
|
0x26,
|
||||||
|
0x9b,
|
||||||
|
0xb8,
|
||||||
|
0x59,
|
||||||
|
0xf0,
|
||||||
|
0xe0,
|
||||||
|
0x4f,
|
||||||
|
0xda,
|
||||||
|
0xc0,
|
||||||
|
0x85,
|
||||||
|
0xc6,
|
||||||
|
0x23,
|
||||||
|
0x21,
|
||||||
|
0x61,
|
||||||
|
0x80,
|
||||||
|
0x59,
|
||||||
|
0xd6,
|
||||||
|
0x18,
|
||||||
|
0xee,
|
||||||
|
0xa0,
|
||||||
|
0xd8,
|
||||||
|
0x00,
|
||||||
|
0xe3,
|
||||||
|
0xdf,
|
||||||
|
0x6e,
|
||||||
|
0xcf,
|
||||||
|
0x89,
|
||||||
|
0x82,
|
||||||
|
0xfd,
|
||||||
|
0x63,
|
||||||
|
// verification tag
|
||||||
|
0xe9,
|
||||||
|
0xe9,
|
||||||
|
0xac,
|
||||||
|
0x92,
|
||||||
|
0xdc,
|
||||||
|
0xb1,
|
||||||
|
0x7c,
|
||||||
|
0x2d,
|
||||||
|
0x9a,
|
||||||
|
0x73,
|
||||||
|
0xda,
|
||||||
|
0x25,
|
||||||
|
0x6d,
|
||||||
|
0xda,
|
||||||
|
0xc0,
|
||||||
|
0x83,
|
||||||
|
)
|
||||||
|
val cipher = crypto.aesGcm(key)
|
||||||
|
val actual = cipher.encrypt(msg, iv)
|
||||||
|
assertEquals(actual.toHexString(), expected.toHexString())
|
||||||
|
|
||||||
|
val recovered = cipher.decrypt(actual)
|
||||||
|
assertEquals(msg.toHexString(), recovered.toHexString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_rand() {
|
||||||
|
val r1 = crypto.secureRandomBytes(8)
|
||||||
|
val r2 = crypto.secureRandomBytes(8)
|
||||||
|
assertEquals(8, r1.size)
|
||||||
|
assertNotEquals(r1.toBits(), r2.toBits())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_derive_key() {
|
||||||
|
val k1 = Key(byteArrayOfInts(0x01, 0x02, 0x03))
|
||||||
|
val k2 = crypto.deriveKey(k1, "TEST")
|
||||||
|
assertEquals(0x44.toByte(), k2.bytes[0])
|
||||||
|
assertEquals(0xd3.toByte(), k2.bytes[31])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
FROM openjdk:8-jre-alpine
|
FROM openjdk:8-jre-alpine
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
COPY uhabits-server.jar /app/uhabits-server.jar
|
COPY build/libs/uhabits-server.jar /app/uhabits-server.jar
|
||||||
ENV LOOP_REPO_PATH /data/
|
ENV LOOP_REPO_PATH /data/
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
CMD ["java", \
|
CMD ["java", \
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ plugins {
|
|||||||
application
|
application
|
||||||
id("kotlin")
|
id("kotlin")
|
||||||
id("com.github.johnrengelman.shadow") version "7.1.2"
|
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||||
|
id("org.jlleitschuh.gradle.ktlint")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
application {
|
application {
|
||||||
group = "org.isoron.uhabits"
|
group = "org.isoron.uhabits"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
@@ -34,8 +34,9 @@ application {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
val ktorVersion = "1.6.8"
|
val ktorVersion = "1.6.8"
|
||||||
val kotlinVersion = "1.6.21"
|
val kotlinVersion = "1.7.10"
|
||||||
val logbackVersion = "1.2.11"
|
val logbackVersion = "1.4.0"
|
||||||
|
implementation(project(":uhabits-core"))
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
|
||||||
implementation("io.ktor:ktor-server-netty:$ktorVersion")
|
implementation("io.ktor:ktor-server-netty:$ktorVersion")
|
||||||
implementation("ch.qos.logback:logback-classic:$logbackVersion")
|
implementation("ch.qos.logback:logback-classic:$logbackVersion")
|
||||||
@@ -43,9 +44,9 @@ dependencies {
|
|||||||
implementation("io.ktor:ktor-html-builder:$ktorVersion")
|
implementation("io.ktor:ktor-html-builder:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-jackson:$ktorVersion")
|
implementation("io.ktor:ktor-jackson:$ktorVersion")
|
||||||
implementation("org.jetbrains:kotlin-css-jvm:1.0.0-pre.148-kotlin-1.4.30")
|
implementation("org.jetbrains:kotlin-css-jvm:1.0.0-pre.148-kotlin-1.4.30")
|
||||||
implementation("io.prometheus:simpleclient:0.15.0")
|
implementation("io.prometheus:simpleclient:0.16.0")
|
||||||
implementation("io.prometheus:simpleclient_httpserver:0.15.0")
|
implementation("io.prometheus:simpleclient_httpserver:0.16.0")
|
||||||
implementation("io.prometheus:simpleclient_hotspot:0.15.0")
|
implementation("io.prometheus:simpleclient_hotspot:0.16.0")
|
||||||
testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
|
testImplementation("io.ktor:ktor-server-tests:$ktorVersion")
|
||||||
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
|
|||||||
@@ -17,17 +17,17 @@
|
|||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.app
|
package org.isoron.uhabits.server.app
|
||||||
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.response.*
|
|
||||||
import io.ktor.routing.*
|
|
||||||
import io.prometheus.client.*
|
|
||||||
import io.prometheus.client.exporter.common.*
|
|
||||||
import io.prometheus.client.hotspot.*
|
|
||||||
import java.io.*
|
|
||||||
|
|
||||||
|
import io.ktor.application.call
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.response.respond
|
||||||
|
import io.ktor.routing.Routing
|
||||||
|
import io.ktor.routing.get
|
||||||
|
import io.prometheus.client.CollectorRegistry
|
||||||
|
import io.prometheus.client.exporter.common.TextFormat
|
||||||
|
import io.prometheus.client.hotspot.DefaultExports
|
||||||
|
import java.io.StringWriter
|
||||||
|
|
||||||
fun Routing.metrics(app: SyncApplication) {
|
fun Routing.metrics(app: SyncApplication) {
|
||||||
// Register JVM metrics
|
// Register JVM metrics
|
||||||
@@ -17,14 +17,20 @@
|
|||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.app
|
package org.isoron.uhabits.server.app
|
||||||
|
|
||||||
import io.ktor.application.*
|
import io.ktor.application.call
|
||||||
import io.ktor.http.*
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.request.*
|
import io.ktor.request.receive
|
||||||
import io.ktor.response.*
|
import io.ktor.response.respond
|
||||||
import io.ktor.routing.*
|
import io.ktor.routing.Routing
|
||||||
import org.isoron.uhabits.sync.*
|
import io.ktor.routing.get
|
||||||
|
import io.ktor.routing.put
|
||||||
|
import io.ktor.routing.route
|
||||||
|
import org.isoron.uhabits.core.sync.EditConflictException
|
||||||
|
import org.isoron.uhabits.core.sync.GetDataVersionResponse
|
||||||
|
import org.isoron.uhabits.core.sync.KeyNotFoundException
|
||||||
|
import org.isoron.uhabits.core.sync.SyncData
|
||||||
|
|
||||||
fun Routing.storage(app: SyncApplication) {
|
fun Routing.storage(app: SyncApplication) {
|
||||||
route("/db/{key}") {
|
route("/db/{key}") {
|
||||||
@@ -33,7 +39,7 @@ fun Routing.storage(app: SyncApplication) {
|
|||||||
try {
|
try {
|
||||||
val data = app.server.getData(key)
|
val data = app.server.getData(key)
|
||||||
call.respond(HttpStatusCode.OK, data)
|
call.respond(HttpStatusCode.OK, data)
|
||||||
} catch(e: KeyNotFoundException) {
|
} catch (e: KeyNotFoundException) {
|
||||||
call.respond(HttpStatusCode.NotFound)
|
call.respond(HttpStatusCode.NotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,8 +49,6 @@ fun Routing.storage(app: SyncApplication) {
|
|||||||
try {
|
try {
|
||||||
app.server.put(key, data)
|
app.server.put(key, data)
|
||||||
call.respond(HttpStatusCode.OK)
|
call.respond(HttpStatusCode.OK)
|
||||||
} catch (e: KeyNotFoundException) {
|
|
||||||
call.respond(HttpStatusCode.NotFound)
|
|
||||||
} catch (e: EditConflictException) {
|
} catch (e: EditConflictException) {
|
||||||
call.respond(HttpStatusCode.Conflict)
|
call.respond(HttpStatusCode.Conflict)
|
||||||
}
|
}
|
||||||
@@ -54,7 +58,7 @@ fun Routing.storage(app: SyncApplication) {
|
|||||||
try {
|
try {
|
||||||
val version = app.server.getDataVersion(key)
|
val version = app.server.getDataVersion(key)
|
||||||
call.respond(HttpStatusCode.OK, GetDataVersionResponse(version))
|
call.respond(HttpStatusCode.OK, GetDataVersionResponse(version))
|
||||||
} catch(e: KeyNotFoundException) {
|
} catch (e: KeyNotFoundException) {
|
||||||
call.respond(HttpStatusCode.NotFound)
|
call.respond(HttpStatusCode.NotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,16 +17,20 @@
|
|||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.app
|
package org.isoron.uhabits.server.app
|
||||||
|
|
||||||
import io.ktor.application.*
|
import io.ktor.application.Application
|
||||||
import io.ktor.features.*
|
import io.ktor.application.install
|
||||||
import io.ktor.jackson.*
|
import io.ktor.features.CallLogging
|
||||||
import io.ktor.routing.*
|
import io.ktor.features.ContentNegotiation
|
||||||
import org.isoron.uhabits.sync.*
|
import io.ktor.features.DefaultHeaders
|
||||||
import org.isoron.uhabits.sync.repository.*
|
import io.ktor.jackson.jackson
|
||||||
import org.isoron.uhabits.sync.server.*
|
import io.ktor.routing.routing
|
||||||
import java.nio.file.*
|
import org.isoron.uhabits.core.sync.AbstractSyncServer
|
||||||
|
import org.isoron.uhabits.server.sync.Repository
|
||||||
|
import org.isoron.uhabits.server.sync.RepositorySyncServer
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
fun Application.main() = SyncApplication().apply { main() }
|
fun Application.main() = SyncApplication().apply { main() }
|
||||||
|
|
||||||
@@ -34,7 +38,7 @@ val REPOSITORY_PATH: Path = Paths.get(System.getenv("LOOP_REPO_PATH")!!)
|
|||||||
|
|
||||||
class SyncApplication(
|
class SyncApplication(
|
||||||
val server: AbstractSyncServer = RepositorySyncServer(
|
val server: AbstractSyncServer = RepositorySyncServer(
|
||||||
FileRepository(REPOSITORY_PATH),
|
Repository(REPOSITORY_PATH),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
fun Application.main() {
|
fun Application.main() {
|
||||||
@@ -44,9 +48,7 @@ class SyncApplication(
|
|||||||
jackson { }
|
jackson { }
|
||||||
}
|
}
|
||||||
routing {
|
routing {
|
||||||
registration(this@SyncApplication)
|
|
||||||
storage(this@SyncApplication)
|
storage(this@SyncApplication)
|
||||||
links(this@SyncApplication)
|
|
||||||
metrics(this@SyncApplication)
|
metrics(this@SyncApplication)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,17 +17,17 @@
|
|||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.repository
|
package org.isoron.uhabits.server.sync
|
||||||
|
|
||||||
import org.isoron.uhabits.sync.*
|
import org.isoron.uhabits.core.sync.KeyNotFoundException
|
||||||
import java.io.*
|
import org.isoron.uhabits.core.sync.SyncData
|
||||||
import java.nio.file.*
|
import java.io.PrintWriter
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
class FileRepository(
|
class Repository(
|
||||||
private val basepath: Path,
|
private val basepath: Path,
|
||||||
) : Repository {
|
) {
|
||||||
|
fun put(key: String, data: SyncData) {
|
||||||
override suspend fun put(key: String, data: SyncData) {
|
|
||||||
// Create directory
|
// Create directory
|
||||||
val dataPath = key.toDataPath()
|
val dataPath = key.toDataPath()
|
||||||
val dataDir = dataPath.toFile()
|
val dataDir = dataPath.toFile()
|
||||||
@@ -50,7 +50,7 @@ class FileRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(key: String): SyncData {
|
fun get(key: String): SyncData {
|
||||||
val dataPath = key.toDataPath()
|
val dataPath = key.toDataPath()
|
||||||
val contentFile = dataPath.resolve("content").toFile()
|
val contentFile = dataPath.resolve("content").toFile()
|
||||||
val versionFile = dataPath.resolve("version").toFile()
|
val versionFile = dataPath.resolve("version").toFile()
|
||||||
@@ -61,7 +61,7 @@ class FileRepository(
|
|||||||
return SyncData(version, contentFile.readText())
|
return SyncData(version, contentFile.readText())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun contains(key: String): Boolean {
|
fun contains(key: String): Boolean {
|
||||||
val dataPath = key.toDataPath()
|
val dataPath = key.toDataPath()
|
||||||
val versionFile = dataPath.resolve("version").toFile()
|
val versionFile = dataPath.resolve("version").toFile()
|
||||||
return versionFile.exists()
|
return versionFile.exists()
|
||||||
@@ -17,20 +17,19 @@
|
|||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.server
|
package org.isoron.uhabits.server.sync
|
||||||
|
|
||||||
import io.prometheus.client.*
|
import io.prometheus.client.Counter
|
||||||
import org.isoron.uhabits.sync.*
|
import org.isoron.uhabits.core.sync.AbstractSyncServer
|
||||||
import org.isoron.uhabits.sync.links.*
|
import org.isoron.uhabits.core.sync.EditConflictException
|
||||||
import org.isoron.uhabits.sync.repository.*
|
import org.isoron.uhabits.core.sync.KeyNotFoundException
|
||||||
import org.isoron.uhabits.sync.utils.*
|
import org.isoron.uhabits.core.sync.SyncData
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An AbstractSyncServer that stores all data in a [Repository].
|
* An AbstractSyncServer that stores all data in a [Repository].
|
||||||
*/
|
*/
|
||||||
class RepositorySyncServer(
|
class RepositorySyncServer(
|
||||||
private val repo: Repository,
|
private val repo: Repository,
|
||||||
private val linkManager: LinkManager = LinkManager(),
|
|
||||||
) : AbstractSyncServer {
|
) : AbstractSyncServer {
|
||||||
|
|
||||||
private val requestsCounter: Counter = Counter.build()
|
private val requestsCounter: Counter = Counter.build()
|
||||||
@@ -39,22 +38,14 @@ class RepositorySyncServer(
|
|||||||
.labelNames("method")
|
.labelNames("method")
|
||||||
.register()
|
.register()
|
||||||
|
|
||||||
override suspend fun register(): String {
|
|
||||||
requestsCounter.labels("register").inc()
|
|
||||||
val key = generateKey()
|
|
||||||
repo.put(key, SyncData(0, ""))
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun put(key: String, newData: SyncData) {
|
override suspend fun put(key: String, newData: SyncData) {
|
||||||
requestsCounter.labels("put").inc()
|
requestsCounter.labels("put").inc()
|
||||||
if (!repo.contains(key)) {
|
if (repo.contains(key)) {
|
||||||
throw KeyNotFoundException()
|
|
||||||
}
|
|
||||||
val prevData = repo.get(key)
|
val prevData = repo.get(key)
|
||||||
if (newData.version != prevData.version + 1) {
|
if (newData.version != prevData.version + 1) {
|
||||||
throw EditConflictException()
|
throw EditConflictException()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
repo.put(key, newData)
|
repo.put(key, newData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,22 +64,4 @@ class RepositorySyncServer(
|
|||||||
}
|
}
|
||||||
return repo.get(key).version
|
return repo.get(key).version
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun registerLink(syncKey: String): Link {
|
|
||||||
requestsCounter.labels("registerLink").inc()
|
|
||||||
return linkManager.register(syncKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getLink(id: String): Link {
|
|
||||||
requestsCounter.labels("getLink").inc()
|
|
||||||
return linkManager.get(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun generateKey(): String {
|
|
||||||
while (true) {
|
|
||||||
val key = randomString(64)
|
|
||||||
if (!repo.contains(key))
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -17,15 +17,12 @@
|
|||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.utils
|
package org.isoron.uhabits.server.sync
|
||||||
|
|
||||||
import java.util.*
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import kotlin.streams.*
|
import org.isoron.uhabits.core.sync.GetDataVersionResponse
|
||||||
|
import org.isoron.uhabits.core.sync.SyncData
|
||||||
|
|
||||||
fun randomString(length: Long): String {
|
val defaultMapper = ObjectMapper()
|
||||||
val chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
fun SyncData.toJson(): String = defaultMapper.writeValueAsString(this)
|
||||||
return Random().ints(length, 0, chars.length)
|
fun GetDataVersionResponse.toJson(): String = defaultMapper.writeValueAsString(this)
|
||||||
.asSequence()
|
|
||||||
.map(chars::get)
|
|
||||||
.joinToString("")
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.app
|
|
||||||
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.request.*
|
|
||||||
import io.ktor.response.*
|
|
||||||
import io.ktor.routing.*
|
|
||||||
import org.isoron.uhabits.sync.*
|
|
||||||
|
|
||||||
data class LinkRegisterRequestData(
|
|
||||||
val syncKey: String,
|
|
||||||
)
|
|
||||||
fun LinkRegisterRequestData.toJson(): String = defaultMapper.writeValueAsString(this)
|
|
||||||
|
|
||||||
fun Routing.links(app: SyncApplication) {
|
|
||||||
post("/links") {
|
|
||||||
try {
|
|
||||||
val data = call.receive<LinkRegisterRequestData>()
|
|
||||||
val link = app.server.registerLink(data.syncKey)
|
|
||||||
call.respond(HttpStatusCode.OK, link)
|
|
||||||
} catch (e: ServiceUnavailable) {
|
|
||||||
call.respond(HttpStatusCode.ServiceUnavailable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get("/links/{id}") {
|
|
||||||
try {
|
|
||||||
val id = call.parameters["id"]!!
|
|
||||||
val link = app.server.getLink(id)
|
|
||||||
call.respond(HttpStatusCode.OK, link)
|
|
||||||
} catch (e: ServiceUnavailable) {
|
|
||||||
call.respond(HttpStatusCode.ServiceUnavailable)
|
|
||||||
} catch (e: KeyNotFoundException) {
|
|
||||||
call.respond(HttpStatusCode.NotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.app
|
|
||||||
|
|
||||||
import io.ktor.application.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.response.*
|
|
||||||
import io.ktor.routing.*
|
|
||||||
import org.isoron.uhabits.sync.*
|
|
||||||
|
|
||||||
fun Routing.registration(app: SyncApplication) {
|
|
||||||
post("/register") {
|
|
||||||
try {
|
|
||||||
val key = app.server.register()
|
|
||||||
call.respond(HttpStatusCode.OK, RegisterReponse(key))
|
|
||||||
} catch (e: ServiceUnavailable) {
|
|
||||||
call.respond(HttpStatusCode.ServiceUnavailable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.links
|
|
||||||
|
|
||||||
import org.isoron.uhabits.sync.defaultMapper
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Link maps a public URL (such as https://sync.loophabits.org/links/B752A6)
|
|
||||||
* to a synchronization key. They are used to transfer sync keys between devices
|
|
||||||
* without ever exposing the original sync key. Unlike sync keys, links expire
|
|
||||||
* after a few minutes.
|
|
||||||
*/
|
|
||||||
data class Link(
|
|
||||||
val id: String,
|
|
||||||
val syncKey: String,
|
|
||||||
val createdAt: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Link.toJson(): String = defaultMapper.writeValueAsString(this)
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.links
|
|
||||||
|
|
||||||
import org.isoron.uhabits.sync.*
|
|
||||||
import org.isoron.uhabits.sync.utils.*
|
|
||||||
|
|
||||||
class LinkManager(
|
|
||||||
private val timeoutInMillis: Long = 900_000,
|
|
||||||
) {
|
|
||||||
private val links = HashMap<String, Link>()
|
|
||||||
|
|
||||||
fun register(syncKey: String): Link {
|
|
||||||
val link = Link(
|
|
||||||
id = randomString(64),
|
|
||||||
syncKey = syncKey,
|
|
||||||
createdAt = System.currentTimeMillis(),
|
|
||||||
)
|
|
||||||
links[link.id] = link
|
|
||||||
return link
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(id: String): Link {
|
|
||||||
val link = links[id] ?: throw KeyNotFoundException()
|
|
||||||
val ageInMillis = System.currentTimeMillis() - link.createdAt
|
|
||||||
if (ageInMillis > timeoutInMillis) {
|
|
||||||
links.remove(id)
|
|
||||||
throw KeyNotFoundException()
|
|
||||||
}
|
|
||||||
return link
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.sync.repository
|
|
||||||
|
|
||||||
import org.isoron.uhabits.sync.KeyNotFoundException
|
|
||||||
import org.isoron.uhabits.sync.SyncData
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class that knows how to store and retrieve a large number of [SyncData] items.
|
|
||||||
*/
|
|
||||||
interface Repository {
|
|
||||||
/**
|
|
||||||
* Stores a data item, under the provided key. The item can be later retrieved with [get].
|
|
||||||
* Replaces existing items silently.
|
|
||||||
*/
|
|
||||||
suspend fun put(key: String, data: SyncData)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a data item that was previously stored using [put].
|
|
||||||
* @throws KeyNotFoundException If no such key exists.
|
|
||||||
*/
|
|
||||||
suspend fun get(key: String): SyncData
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the repository contains a given key.
|
|
||||||
*/
|
|
||||||
suspend fun contains(key: String): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -4,6 +4,6 @@ ktor {
|
|||||||
port = ${?PORT}
|
port = ${?PORT}
|
||||||
}
|
}
|
||||||
application {
|
application {
|
||||||
modules = [ org.isoron.uhabits.sync.app.SyncApplicationKt.main ]
|
modules = [ org.isoron.uhabits.server.app.SyncApplicationKt.main ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||