Compare commits

..

292 commits

Author SHA1 Message Date
Marius Lindvall
6b3d8dcbec
Merge pull request #169 from tuffnerdstuff/bugfix/null-acc-session-expired
setting accuracy circle also when acc == null
2024-05-24 23:05:24 +02:00
Marius Lindvall
0bbea4fe64
Code style consistency 2024-05-24 22:59:48 +02:00
Marius Lindvall
63f901b17a
Merge pull request #183 from mansguiche/fix-assumes-ldap-on-no-htpasswd-file-found
Fix incorrectly reporting LDAP errors when cannot find HTPASSWD file
2024-05-24 22:48:50 +02:00
Marius Lindvall
7dea87aef9
Fix indentation 2024-05-24 22:28:22 +02:00
Marius Lindvall
50f93c1246
Merge pull request #196 from otbutz/patch-1
Use preferred OSM tile URL
2024-05-24 22:22:45 +02:00
Marius Lindvall
2a7e9ac0b0
Fix stable tag 2023-08-13 16:23:48 +02:00
Marius Lindvall
26d8a2e13a
Disable on-commit CI/CD for now 2023-08-13 15:49:31 +02:00
Marius Lindvall
62cac722d3
Update to v1.6.2 2023-08-13 15:48:54 +02:00
Marius Lindvall
5376f938d4
Add libssl-dev dependency for Debian Bookworm 2023-08-13 15:32:08 +02:00
Marius Lindvall
52d48fa319
Fix Docker pipeline 2023-08-13 15:22:25 +02:00
Marius Lindvall
1b6f6217f3
Add Docker build pipeline 2023-08-13 15:21:10 +02:00
Marius Lindvall
f5fb57c56e
Update to API level 33 (Android 13) 2023-08-13 14:51:09 +02:00
otbutz
085ca4ee97
Use preferred OSM tile URL 2023-06-30 14:40:24 +02:00
Luke Manson
56e216375c Fixed translation key 2022-04-06 21:56:09 +01:00
Luke Manson
ec223ba4e6 Adds translations for password file missing error 2022-04-06 21:44:19 +01:00
Luke Manson
a0287e2c13 Drop out of authenticated() function when cannot find htpasswd file. 2022-04-06 21:34:30 +01:00
tuffnerdstuff
02ccb4fcd2 Checking accuracy circle !== null when setting color 2021-04-01 14:55:08 +02:00
Marius Lindvall
55d2f8b8fd
Set Content-Security-Policy 2021-03-30 18:03:55 +02:00
Marius Lindvall
26bd32504d
Merge pull request #171 from tuffnerdstuff/feature/incremental-fetch
Incremental fetch
2021-03-30 15:47:51 +02:00
tuffnerdstuff
08a5050b7c Implemented incremental fetch 2021-03-30 00:01:28 +02:00
tuffnerdstuff
9d637c5b58 setting accuracy circle also when acc == null 2021-03-29 23:16:40 +02:00
sanvit
b95f517ada Translated using Weblate (Korean)
Currently translated at 100.0% (34 of 34 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/ko/
2021-02-15 08:53:14 +01:00
sanvit
ce6b064f48 Added translation using Weblate (Korean) 2021-02-14 03:28:33 +01:00
sanvit
eb04608ca5 Added translation using Weblate (Korean) 2021-02-14 03:00:13 +01:00
Marius Lindvall
5f671667cf Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (146 of 146 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/
2020-12-15 01:51:30 +01:00
Marius Lindvall
1e5217b1d7 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (146 of 146 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2020-12-15 01:51:30 +01:00
Marius Lindvall
bc2e9fdcce
Merge pull request #158 from maximbaz/fix-showing-initial-active-users
Fix showing initial active users
2020-11-12 11:08:26 +01:00
Maxim Baz
8ada63b85e
Fix showing initial active users 2020-11-08 14:16:49 +01:00
bwisn
899e525844 Translated using Weblate (Polish)
Currently translated at 100.0% (25 of 25 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/pl/
2020-09-20 12:50:26 +02:00
bwisn
1a241900bb Translated using Weblate (Polish)
Currently translated at 100.0% (34 of 34 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/pl/
2020-09-20 12:50:26 +02:00
bwisn
f6220ac07c Translated using Weblate (Polish)
Currently translated at 100.0% (146 of 146 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/pl/
2020-09-20 12:49:27 +02:00
bwisn
a56ddc274e Added translation using Weblate (Polish) 2020-09-19 12:09:22 +02:00
bwisn
aba2be5445 Added translation using Weblate (Polish) 2020-09-19 12:01:01 +02:00
David
cbc1278587 Translated using Weblate (Spanish)
Currently translated at 44.5% (65 of 146 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/es/
2020-09-16 19:49:19 +02:00
Licaon_Kter
3dd594df55 Translated using Weblate (Romanian)
Currently translated at 100.0% (146 of 146 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2020-08-13 20:44:58 +02:00
Nick Bouwhuis
0b6defc382 Translated using Weblate (Dutch)
Currently translated at 100.0% (143 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nl/
2020-08-13 20:44:58 +02:00
Nick Bouwhuis
3d2c4321b7 Translated using Weblate (Dutch)
Currently translated at 100.0% (34 of 34 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nl/
2020-08-12 17:44:56 +02:00
Nick Bouwhuis
6d7dcc3bfe Translated using Weblate (Dutch)
Currently translated at 100.0% (25 of 25 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/nl/
2020-08-12 17:44:56 +02:00
Marius Lindvall
9dcf6a536f Hide keyboard on launch; fixes #146 2020-07-31 23:12:54 +02:00
Marius Lindvall
f3b94ce567 Allow logo to be toggled; see #146 2020-07-31 23:04:38 +02:00
Marius Lindvall
148d64b0bc Add notes on upgrading; fixes #133 2020-07-31 22:32:46 +02:00
Marius Lindvall
b443bb9689 Adoption dialog layout bug; fixes #145 2020-07-30 20:11:37 +02:00
Marius Lindvall
bbca5862f3 Handle expired sessions gracefully; fixes #148 2020-07-30 20:06:59 +02:00
Marius Lindvall
5556018609 Merge branch 'master' of https://github.com/bilde2910/Hauk 2020-07-30 20:00:43 +02:00
Marius Lindvall
3ab6f72879 Update Gradle and dependencies 2020-07-30 20:00:41 +02:00
Marius Lindvall
ccf8503562
Merge pull request #141 from rickvanderzwet/freebsd_compatible_installer
Add FreeBSD support to installer
2020-05-25 15:14:00 +02:00
Rick van der Zwet
396302a197
Update install.sh
Old habbits dies slow, explicit guarding is not longer required.

Co-authored-by: Marius Lindvall <marius@varden.info>
2020-05-25 15:09:03 +02:00
Marius Lindvall
076dc9ac84
Merge pull request #140 from rickvanderzwet/fix_redis_unix_socket_connect
Fix unable to connect to redis socket
2020-05-25 14:50:14 +02:00
Rick van der Zwet
a7e241ade1 Add FreeBSD support to installer
Configuration directory /etc is reserved for system configuration files.
/usr/local/etc is reserved for local (user) installed configuation files.
2020-05-25 12:37:08 +00:00
Rick van der Zwet
3235f789f7 Fix unable to connect to redis socket
If second argument is given, the connection is always parsed as TCP host.
2020-05-25 12:30:31 +00:00
Marius Lindvall
a9379ff24b
Merge pull request #139 from rickvanderzwet/fix_new_link_error
Fix php syntax error
2020-05-25 14:30:23 +02:00
Rick van der Zwet
6bea156d39 Fix php syntax error 2020-05-25 12:25:11 +00:00
Marius Lindvall
f44a7eb36c
Add note relating to #137 2020-05-20 22:02:55 +02:00
Mert
b1761913a2 Translated using Weblate (Turkish)
Currently translated at 34.3% (49 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/tr/
2020-03-24 19:14:52 +01:00
Marius Lindvall
51241fa872 Update to v1.6.1 2020-03-24 18:58:40 +01:00
Marius Lindvall
4ee03b3479 Register new languages 2020-03-24 18:56:09 +01:00
Marius Lindvall
f7ae59e7c0 Update translation credits 2020-03-24 18:38:16 +01:00
Marius Lindvall
7f21ffb761 Return 404 if link is invalid; fixes #131 2020-03-24 18:34:29 +01:00
Marius Lindvall
7b7bbb0767 Update Gradle 2020-03-24 18:26:25 +01:00
Ali Yasir Yılmaz
532ad1c633 Translated using Weblate (Turkish)
Currently translated at 33.6% (48 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/tr/
2020-03-19 09:41:09 +01:00
Mert
9ec5a2c1b7 Translated using Weblate (Turkish)
Currently translated at 33.6% (48 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/tr/
2020-03-19 09:41:09 +01:00
Mert
576cb540fc Translated using Weblate (Turkish)
Currently translated at 100.0% (25 of 25 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/tr/
2020-03-18 12:41:08 +01:00
Mert
d986074c00 Translated using Weblate (Turkish)
Currently translated at 100.0% (34 of 34 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/tr/
2020-03-18 12:41:08 +01:00
Mert
66c7b2e00b Added translation using Weblate (Turkish) 2020-03-17 15:16:58 +01:00
Mert
f3a99dfcfe Added translation using Weblate (Turkish) 2020-03-17 12:13:57 +01:00
Kyle
09ab078fa7 Added translation using Weblate (Turkish) 2020-03-17 09:54:23 +01:00
Brujerizmo90
b5e71bdc04 Translated using Weblate (Russian)
Currently translated at 95.1% (136 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ru/
2020-03-14 11:41:02 +01:00
Brujerizmo90
f69ff64a72 Translated using Weblate (Russian)
Currently translated at 88.2% (30 of 34 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/ru/
2020-03-14 07:41:01 +01:00
Marius Lindvall
2211fbfb47 Keep fine location listener alive; fixes #124 2020-03-04 13:46:51 +01:00
Marius Lindvall
cfa0032d98 Text wrapping; fixes #127 2020-03-04 12:40:18 +01:00
Marius Lindvall
a2b0936026 Update Gradle 2020-03-04 12:21:32 +01:00
Vieler Hyloks
d37313c901 Translated using Weblate (Italian)
Currently translated at 100.0% (25 of 25 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/it/
2020-03-02 16:40:45 +01:00
Vieler Hyloks
8e401e837f Translated using Weblate (Italian)
Currently translated at 100.0% (34 of 34 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/it/
2020-03-02 16:40:45 +01:00
Vieler Hyloks
acab476e19 Translated using Weblate (Italian)
Currently translated at 99.3% (142 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/it/
2020-03-02 14:40:46 +01:00
Vieler Hyloks
87a8ec42f3 Added translation using Weblate (Italian) 2020-03-01 16:21:58 +01:00
Vieler Hyloks
58bc9bee12 Added translation using Weblate (Italian) 2020-03-01 16:11:04 +01:00
Vieler Hyloks
0fb03f45b3 Added translation using Weblate (Italian) 2020-03-01 09:09:42 +01:00
Marius Lindvall
1ca89c560f
Update translation credits 2020-02-16 20:49:44 +01:00
Luke Marlin
2e912e5d5f Translated using Weblate (French)
Currently translated at 100.0% (143 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/fr/
2020-02-16 20:43:46 +01:00
Luke Marlin
d2856bfdb0 Translated using Weblate (French)
Currently translated at 100.0% (25 of 25 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/fr/
2020-02-16 20:43:32 +01:00
Luke Marlin
f387577807 Translated using Weblate (French)
Currently translated at 100.0% (34 of 34 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/fr/
2020-02-16 20:43:32 +01:00
Marius Lindvall
d60e53b7a4 Merge pull request #128 from VictorArajooj/patch-1
Created and translated file to portuguese
2020-02-09 22:15:33 +01:00
Marius Lindvall
f5cb0ded1e
Use proper F-Droid badge link 2020-02-09 22:10:15 +01:00
Stefan
8c438fc2e6 Translated using Weblate (German)
Currently translated at 100.0% (25 of 25 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/de/
2020-02-09 22:06:21 +01:00
Victor P. Araújo
8e4e33bc9a
Update pt_BR.json 2020-02-06 15:58:42 -03:00
Cristian
19c6da754f Translated using Weblate (Spanish)
Currently translated at 24.5% (35 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/es/
2020-02-02 17:38:34 +01:00
Stefan
d7371d41eb Translated using Weblate (German)
Currently translated at 100.0% (143 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/de/
2020-01-22 21:38:16 +01:00
Marius Lindvall
7ad8686098
Merge pull request #125 from Hello71/patch-1
redis: connect before authenticating
2020-01-20 22:54:47 +01:00
Alex Xu
5c59910e54
redis: connect before authenticating
otherwise we get "Uncaught RedisException: Redis server went away" on Redis->auth()
2020-01-19 02:21:23 +00:00
Victor P. Araújo
5ac1a87ded
Created and translated file to portuguese
English:

It was translated as completion or file, plus edit image links to also translated images (the F-Droid image was created by me on GooglePlay themselves available)

Português:

Foi traduzido completamento o arquivo, alem de ter editar os links das imagens para imagens também traduzidas (F-Droid image foi criado por mim, a o GooglePlay eles mesmo disponibiliza)
2020-01-15 08:43:59 -03:00
Marius Lindvall
5ea2ff473e
Update translation credits 2020-01-14 21:31:38 +01:00
Marmo
6dff205239 Translated using Weblate (German)
Currently translated at 97.2% (139 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/de/
2020-01-12 10:37:58 +01:00
code-surfer
d69feaca90 Translated using Weblate (German)
Currently translated at 96.5% (138 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/de/
2020-01-12 10:37:58 +01:00
code-surfer
d1b3a158e1 Translated using Weblate (German)
Currently translated at 100.0% (34 of 34 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/de/
2020-01-04 19:37:46 +01:00
Jdekoning141
7547029e4b Translated using Weblate (Dutch)
Currently translated at 88.8% (127 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nl/
2020-01-03 02:37:41 +01:00
Spencer Stolworthy
a2d5b6536f Translated using Weblate (Spanish)
Currently translated at 12.6% (18 of 143 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/es/
2019-12-30 06:37:36 +01:00
Marius Lindvall
cc4c43ba49 Escape LDAP string 2019-12-29 21:39:26 +01:00
Marius Lindvall
b169caf778 Add AUR package 2019-12-29 20:24:46 +01:00
Marius Lindvall
060a81429a Add additional identifiers to log data 2019-12-29 17:23:00 +01:00
Marius Lindvall
539bc6940e Add more detailed logging 2019-12-29 17:15:35 +01:00
Spencer Stolworthy
d6c338611d Added translation using Weblate (Spanish) 2019-12-29 05:53:53 +01:00
Marius Lindvall
a9860fd433 Update to v1.6
Fixes #116
2019-12-28 19:48:04 +01:00
Marius Lindvall
ce47e2d84f Update dependency 2019-12-28 19:46:50 +01:00
Marius Lindvall
9d27fc819f Fix "unable to connect to background" error when session ends 2019-12-28 19:29:44 +01:00
Marius Lindvall
ad6eb7ce76 Remove redundant UI; fixes #118 2019-12-28 19:23:24 +01:00
Marius Lindvall
b86c0e7b7d Update notification with status, see #74 2019-12-28 19:21:41 +01:00
Licaon_Kter
cf7cb4af54 Translated using Weblate (Romanian)
Currently translated at 100.0% (144 of 144 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-28 18:39:37 +01:00
Licaon_Kter
1dd863d15e Translated using Weblate (Romanian)
Currently translated at 100.0% (25 of 25 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/ro/
2019-12-28 18:39:29 +01:00
Marius Lindvall
a92de3409e
Merge pull request #117 from licaon-kter/patch-6
Consistency fix
2019-12-28 17:27:04 +01:00
Marius Lindvall
9fe3e3e373 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (144 of 144 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/
2019-12-28 17:26:43 +01:00
Marius Lindvall
e33ebaf06d Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (144 of 144 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-28 17:26:43 +01:00
Marius Lindvall
3dd6386a16 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (34 of 34 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nn/
2019-12-28 17:26:30 +01:00
Marius Lindvall
44ce280e4c Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (34 of 34 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nb_NO/
2019-12-28 17:26:30 +01:00
Marius Lindvall
374c155516 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (25 of 25 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/nn/
2019-12-28 17:26:30 +01:00
Marius Lindvall
9c51f39506 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (25 of 25 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/nb_NO/
2019-12-28 17:26:30 +01:00
Licaon_Kter
bfa9ed6c81
Consistency fix
...as the word is not used elsewhere, but "sharing" is.

Also tracking has rather bad connotations. ;)
2019-12-28 16:07:34 +00:00
Licaon_Kter
5894aaec11 Translated using Weblate (Romanian)
Currently translated at 100.0% (144 of 144 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-28 16:57:34 +01:00
Marius Lindvall
94426fb5b9 Support LDAP authentication 2019-12-28 16:56:59 +01:00
Marius Lindvall
9c886b5002 Detect fine location unavailable, see #74 2019-12-28 14:52:34 +01:00
Marius Lindvall
312bec7faf Change label if no active links; fixes #72 2019-12-28 12:51:20 +01:00
Licaon_Kter
d52a80c51c Translated using Weblate (Romanian)
Currently translated at 100.0% (141 of 141 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-26 23:37:30 +01:00
Marius Lindvall
69bbd1d78a
Add privacy statement for demo server 2019-12-26 14:38:04 +01:00
Marius Lindvall
d160b9438a Fix character wrapping on "stop" and "share" buttons (#74) 2019-12-25 17:51:20 +01:00
Marius Lindvall
afde9c15aa Fix fields editable on UI reload; fixes #108 2019-12-25 13:21:49 +01:00
Licaon_Kter
a221979d64 Translated using Weblate (Romanian)
Currently translated at 100.0% (141 of 141 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-18 12:47:29 +01:00
Licaon_Kter
f728447d5d Translated using Weblate (Romanian)
Currently translated at 100.0% (34 of 34 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/ro/
2019-12-17 17:55:09 +01:00
Marius Lindvall
8c4a893a25 Only download store logos in index; fixes #114 2019-12-17 17:24:39 +01:00
Marius Lindvall
088e4b5570 Change wording regarding shares vs person (#112) 2019-12-17 17:17:54 +01:00
Marius Lindvall
51c3c496a2
Merge pull request #113 from licaon-kter/patch-5
Don't hardcode F-Droid, typo in my nick
2019-12-17 17:06:43 +01:00
Marius Lindvall
88c46e5f06
Remove italics 2019-12-17 17:04:29 +01:00
Licaon_Kter
fa241fdc59
Don't hardcode F-Droid, typo in my nick 2019-12-17 15:54:35 +00:00
Licaon_Kter
eefc7029b9 Translated using Weblate (Romanian)
Currently translated at 100.0% (141 of 141 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-17 16:37:22 +01:00
Marius Lindvall
fe2ad64bb1 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (141 of 141 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/
2019-12-17 16:37:22 +01:00
Marius Lindvall
fb9dc7c7c9 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (141 of 141 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-17 16:37:22 +01:00
Licaon_Kter
1dc5d73911 Translated using Weblate (Romanian)
Currently translated at 100.0% (32 of 32 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/ro/
2019-12-17 16:37:07 +01:00
Marius Lindvall
ba90e59965 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (32 of 32 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nb_NO/
2019-12-17 16:37:07 +01:00
Marius Lindvall
30190715b6 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (32 of 32 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nn/
2019-12-17 16:37:01 +01:00
Licaon_Kter
0dd94dcf82 Translated using Weblate (Romanian)
Currently translated at 100.0% (32 of 32 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/ro/
2019-12-17 16:23:00 +01:00
Licaon_Kter
c0f732ae32 Translated using Weblate (Romanian)
Currently translated at 100.0% (134 of 134 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-17 15:47:59 +01:00
Marius Lindvall
2c79f7209f DIsable proxy settings by default 2019-12-17 15:47:39 +01:00
Marius Lindvall
5b61666de0 Don't show users with no known location in list (#101) 2019-12-17 15:35:59 +01:00
Marius Lindvall
80313532e1 Require nickname for group shares (#74) 2019-12-17 15:31:54 +01:00
Marius Lindvall
adf829cb1a Fix duration being stored as seconds 2019-12-17 15:28:25 +01:00
Marius Lindvall
3dcd31caaa Open browser when clicking logo (#74) 2019-12-17 15:24:52 +01:00
Marius Lindvall
548b9c962f Add icons to settings menu 2019-12-17 15:17:02 +01:00
Marius Lindvall
d58e92946b Add version to settings (#74) 2019-12-17 15:16:52 +01:00
Marius Lindvall
ef90630350 Don't show dialog when resuming; fixes #111 2019-12-17 14:14:25 +01:00
Marius Lindvall
388fd91151 Fix integer overflow and parsing issues (#100) 2019-12-17 14:10:08 +01:00
Marius Lindvall
3383c713e0 Fix crash when logging 2019-12-17 14:09:12 +01:00
Marius Lindvall
9db4a66813 Confirm before stopping each link; fixes #78 2019-12-17 12:39:00 +01:00
Marius Lindvall
4258eb2d41 Fix dark theme for group shares 2019-12-17 12:25:18 +01:00
Marius Lindvall
190347af48 Add user list button; fixes #101 2019-12-17 12:23:32 +01:00
Marius Lindvall
9935a4b1c1 Configurable minimum distance; fixes #107 2019-12-16 22:33:22 +01:00
Marius Lindvall
413e6bca77 Confirm before stopping shares; fixes #78 2019-12-16 22:15:22 +01:00
Licaon_Kter
256c621446 Translated using Weblate (Romanian)
Currently translated at 100.0% (127 of 127 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-15 18:49:47 +01:00
Jdekoning141
b28692ebbc Translated using Weblate (Dutch)
Currently translated at 100.0% (127 of 127 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nl/
2019-12-15 18:49:47 +01:00
Marius Lindvall
eb5b47e3f2 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (127 of 127 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/
2019-12-15 18:49:47 +01:00
Marius Lindvall
4cd021b63b Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (127 of 127 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-15 18:49:47 +01:00
Jdekoning141
bc1b7d0a4e Translated using Weblate (Dutch)
Currently translated at 100.0% (26 of 26 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nl/
2019-12-15 18:49:35 +01:00
Jdekoning141
90b0ec1d97 Translated using Weblate (Dutch)
Currently translated at 100.0% (19 of 19 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/nl/
2019-12-15 18:49:35 +01:00
Marius Lindvall
c0fd845799 Add detailed request logging; fixes #106 2019-12-14 18:47:55 +01:00
Licaon_Kter
73f94af397 Translated using Weblate (Romanian)
Currently translated at 100.0% (121 of 121 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-14 00:55:26 +01:00
Marius Lindvall
129a85641a Fix crash if UI was recreated during a share 2019-12-14 00:37:22 +01:00
Marius Lindvall
be6cee8f1f Add night mode setting, see #74 2019-12-14 00:27:44 +01:00
Marius Lindvall
f7a430f039 More abstraction for preference enum resolvers 2019-12-13 23:38:01 +01:00
Marius Lindvall
36f22f4085 Set auto night mode 2019-12-13 18:23:45 +01:00
Marius Lindvall
e0918f25bd Fix E2E not working properly 2019-12-13 17:51:25 +01:00
Marius Lindvall
edf74b5cea Warn user if server not set, see #74 2019-12-13 17:50:25 +01:00
Marius Lindvall
ebd71f0300 Update Gradle 2019-12-13 17:43:07 +01:00
Marius Lindvall
bf21d3f025 Restructure I18N keys 2019-12-13 17:40:54 +01:00
Marius Lindvall
77bb8943e5 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (120 of 120 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/
2019-12-13 17:29:42 +01:00
Marius Lindvall
65a4f2708f Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (120 of 120 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-13 17:29:42 +01:00
Licaon_Kter
9d7ae8d9b5 Translated using Weblate (Romanian)
Currently translated at 100.0% (120 of 120 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-13 15:20:44 +01:00
Marius Lindvall
60bef413be Allow enums to be stored in preferences 2019-12-13 15:17:37 +01:00
Licaon_Kter
178dbd05c3 Translated using Weblate (Romanian)
Currently translated at 100.0% (115 of 115 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-13 13:45:18 +01:00
Marius Lindvall
f08aa7cc46 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (115 of 115 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/
2019-12-13 13:45:18 +01:00
Marius Lindvall
bec5ac5557 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (115 of 115 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-13 13:45:18 +01:00
Marius Lindvall
ead0b55e16 Allow insecure TLS certs for .onion, see #75 2019-12-13 13:44:23 +01:00
Marius Lindvall
e51d628fe8 Refactor proxy TypeIndexResolver 2019-12-13 12:51:08 +01:00
Marius Lindvall
4f1ac92b9a Add configurable connection timeout, see #75 2019-12-13 12:38:36 +01:00
Marius Lindvall
ea398fda16 Add credits for Romanian translation 2019-12-12 22:55:49 +01:00
Marius Lindvall
3236a98a7e Add proxy support; fixes #75 2019-12-12 22:37:59 +01:00
Marius Lindvall
f54718364d Miscellaneous cleanup 2019-12-12 22:34:25 +01:00
Marius Lindvall
2e933a319e Remove unused preference 2019-12-12 22:32:44 +01:00
Licaon_Kter
86a1badccb Translated using Weblate (Romanian)
Currently translated at 100.0% (105 of 105 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-12 21:26:00 +01:00
Stefan
17e9df1998 Translated using Weblate (German)
Currently translated at 100.0% (105 of 105 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/de/
2019-12-12 21:26:00 +01:00
Stefan
010b46f0bc Translated using Weblate (German)
Currently translated at 100.0% (19 of 19 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/de/
2019-12-12 21:25:50 +01:00
Stefan
89f85d2dfb Translated using Weblate (German)
Currently translated at 100.0% (26 of 26 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/de/
2019-12-12 21:25:50 +01:00
Marius Lindvall
e01e58d830 Remove empty strings, see #97 2019-12-12 21:11:25 +01:00
Marius Lindvall
13ac15a57b Match Xiaomi better; fixes #104 2019-12-12 20:08:16 +01:00
Licaon_Kter
2ec464c9c3 Translated using Weblate (Romanian)
Currently translated at 100.0% (105 of 105 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-12 18:33:04 +01:00
Marius Lindvall
068aa2a9c6 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (105 of 105 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/
2019-12-12 18:33:04 +01:00
Marius Lindvall
fb47240df6 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (105 of 105 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-12 18:33:04 +01:00
Licaon_Kter
36b9245b7f Translated using Weblate (Romanian)
Currently translated at 100.0% (26 of 26 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/ro/
2019-12-12 18:32:53 +01:00
Licaon_Kter
9b811869cf Translated using Weblate (Romanian)
Currently translated at 100.0% (19 of 19 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/ro/
2019-12-12 18:32:53 +01:00
Licaon_Kter
f3eada1548 Translated using Weblate (Romanian)
Currently translated at 83.8% (88 of 105 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-12 12:18:33 +01:00
Weblate
94491f7118 Fix merge conflict in Weblate 2019-12-12 12:13:41 +01:00
Licaon_Kter
4113fe73d3 Translated using Weblate (Romanian)
Currently translated at 94.7% (18 of 19 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/ro/
2019-12-12 12:04:27 +01:00
Licaon_Kter
ca5afdb9a8 Translated using Weblate (Romanian)
Currently translated at 94.7% (18 of 19 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/ro/
2019-12-12 11:58:15 +01:00
Marius Lindvall
a891ae445f Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (19 of 19 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/nn/
2019-12-12 11:58:00 +01:00
Marius Lindvall
5ccd3f8694 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (27 of 27 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nn/
2019-12-12 11:58:00 +01:00
Licaon_Kter
5c21d4bb7d Translated using Weblate (Romanian)
Currently translated at 89.5% (17 of 19 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/ro/
2019-12-12 11:58:00 +01:00
Marius Lindvall
579e0eb5ac Remove app name from I18N, see #98 2019-12-12 11:57:43 +01:00
Marius Lindvall
2a5ae0f757
Merge pull request #98 from licaon-kter/patch-1
Don't allow app name translation
2019-12-12 11:48:31 +01:00
Marius Lindvall
1445131b58
Merge pull request #99 from licaon-kter/patch-2
Fix typo
2019-12-12 11:47:10 +01:00
Marius Lindvall
4ce104642e Remove app name from other languages 2019-12-12 11:45:45 +01:00
Licaon_Kter
99d7dce435
Fix typo 2019-12-12 10:42:29 +00:00
Marius Lindvall
afc72551d8 Merge branch 'master' of https://github.com/bilde2910/Hauk 2019-12-12 11:35:04 +01:00
Marius Lindvall
99b5dccf16 Don't use O and 0 in upper/mixed case IDs; fixes #91 2019-12-12 11:34:56 +01:00
Licaon_Kter
c1bd1e8395
Don't allow app name translation 2019-12-12 10:31:16 +00:00
Marius Lindvall
d0814ce437 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (107 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-12 11:29:47 +01:00
Marius Lindvall
c30740367d Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (107 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/
2019-12-12 11:29:47 +01:00
Licaon_Kter
012ec636b8 Translated using Weblate (Romanian)
Currently translated at 81.3% (87 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ro/
2019-12-12 11:29:47 +01:00
Licaon_Kter
682e11ea18 Added translation using Weblate (Romanian) 2019-12-12 11:06:02 +01:00
Marius Lindvall
95546a8238 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (27 of 27 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nb_NO/
2019-12-12 11:05:58 +01:00
Marius Lindvall
7d4059f3ed Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (19 of 19 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/nb_NO/
2019-12-12 11:05:58 +01:00
Licaon_Kter
336b1509fd Added translation using Weblate (Romanian) 2019-12-12 11:05:58 +01:00
Licaon_Kter
313b393ae5 Added translation using Weblate (Romanian) 2019-12-12 11:05:10 +01:00
Marius Lindvall
0ca32c46f8 Improve end-to-end encryption UX; fixes #96 2019-12-12 11:02:56 +01:00
Marius Lindvall
fdb35db2ce Translated using Weblate (Catalan)
Currently translated at 91.6% (98 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ca/
2019-12-12 00:54:58 +01:00
Marius Lindvall
cde81fdacd Translated using Weblate (Ukrainian)
Currently translated at 85.0% (91 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/uk/
2019-12-12 00:54:58 +01:00
Marius Lindvall
4f2c99bb84 Translated using Weblate (Russian)
Currently translated at 85.0% (91 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ru/
2019-12-12 00:54:58 +01:00
Marius Lindvall
22392a4099 Translated using Weblate (Polish)
Currently translated at 73.8% (79 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/pl/
2019-12-12 00:54:58 +01:00
Marius Lindvall
ddc23a2736 Translated using Weblate (Dutch)
Currently translated at 85.0% (91 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nl/
2019-12-12 00:54:58 +01:00
Marius Lindvall
56929dc930 Translated using Weblate (German)
Currently translated at 88.8% (95 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/de/
2019-12-12 00:54:58 +01:00
Marius Lindvall
b90585fd8f Translated using Weblate (Basque)
Currently translated at 74.8% (80 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/eu/
2019-12-12 00:54:57 +01:00
Marius Lindvall
c5b4b05a46 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (107 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/
2019-12-12 00:54:57 +01:00
Marius Lindvall
b21f73de6b Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (107 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-12 00:54:57 +01:00
Marius Lindvall
f8ece683a7 Translated using Weblate (Catalan)
Currently translated at 91.6% (98 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ca/
2019-12-12 00:48:48 +01:00
Marius Lindvall
9fd20cb7a7 Translated using Weblate (Dutch)
Currently translated at 85.0% (91 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nl/
2019-12-12 00:48:48 +01:00
Marius Lindvall
c0c6917abc Translated using Weblate (French)
Currently translated at 83.2% (89 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/fr/
2019-12-12 00:48:48 +01:00
Marius Lindvall
88bfcd1eb9 Translated using Weblate (Basque)
Currently translated at 74.8% (80 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/eu/
2019-12-12 00:48:48 +01:00
Marius Lindvall
a658f57644 Merge branch 'master' of https://github.com/bilde2910/Hauk 2019-12-12 00:47:09 +01:00
Marius Lindvall
7ec9c12acc Another colon 2019-12-12 00:47:02 +01:00
Marius Lindvall
4088ea5947 Translated using Weblate (Catalan)
Currently translated at 86.9% (93 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ca/
2019-12-12 00:44:55 +01:00
Marius Lindvall
16b92c0c03 Translated using Weblate (Basque)
Currently translated at 74.8% (80 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/eu/
2019-12-12 00:44:55 +01:00
Marius Lindvall
6d4327f0c9 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (107 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-12 00:44:55 +01:00
Marius Lindvall
b380456849 Merge branch 'master' of https://github.com/bilde2910/Hauk 2019-12-12 00:44:40 +01:00
Marius Lindvall
ef6a1747dc Add another missing colon 2019-12-12 00:44:33 +01:00
Marius Lindvall
201165d227 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (107 of 107 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-12 00:42:34 +01:00
Marius Lindvall
1d0d8767de Re-add missing colon 2019-12-12 00:42:19 +01:00
Marius Lindvall
4e660f1fe4 Change "session expired" wording; fixes #95 2019-12-12 00:25:11 +01:00
Marius Lindvall
31b5daaa12 Rework UI; see #74 2019-12-12 00:21:03 +01:00
Marius Lindvall
c8db154a36
Merge pull request #93 from RuralYak/92-apache-fails-to-start-in-docker-container-after-stop
apache pid file deletion
2019-12-11 20:19:01 +01:00
dms
a1018fa7a7 apache pid file deletion 2019-12-10 15:24:22 -08:00
Marius Lindvall
122304a1be
Provide tag descriptions for Docker Hub images
See discussion in #90
2019-12-10 19:07:14 +01:00
Marius Lindvall
9c6e5e6362 Update to v1.5.2 2019-12-10 17:01:16 +01:00
Marius Lindvall
b94dacdb6a Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (105 of 105 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/
2019-12-10 10:57:50 +01:00
Marius Lindvall
1e1d44be9f Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (105 of 105 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-10 10:57:50 +01:00
Marius Lindvall
bf4eac8d2a Open settings if location services disabled; fixes #87 2019-12-10 10:38:26 +01:00
Marius Lindvall
58e5ac9414 Use server time; fixes #86
I reject your reality and substitute my own!
2019-12-09 19:30:21 +01:00
Marius Lindvall
62d28f42fe Indicate missing backend connection; fixes #85 2019-12-09 18:52:58 +01:00
Marius Lindvall
88e5419792 Fix decryption error input when polling is active
This fixes an issue where the password input would constantly clear itself if there was a decryption error while polling was active.
2019-12-09 18:18:07 +01:00
Marius Lindvall
405e552c43 Fix errors when decrypting points with missing data
E.g. missing speed or accuracy. This could happen if the client is pushing data from the network location provider, for example.
2019-12-09 18:16:06 +01:00
Marius Lindvall
6fa10a14f3 Localization cleanup 2019-12-09 16:42:58 +01:00
Marius Lindvall
7703ec10de Stop session if stopping last remaining share
Fixes #82
2019-12-09 16:29:19 +01:00
Marius Lindvall
9ecb7c08a9 Don't log sensitive information; fixes #83 2019-12-09 16:11:30 +01:00
Marius Lindvall
319200ea54 Fixed location service issues when recreating MainActivity
Fixes #77
Fixes #80
2019-12-09 16:01:41 +01:00
Marius Lindvall
21446811ab Credits for Catalan translation 2019-12-09 13:59:52 +01:00
Marius Lindvall
9f430a445e Use bulleted list of translators 2019-12-09 13:58:09 +01:00
Marius Lindvall
ed08ece16a Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (104 of 104 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nn/
2019-12-09 13:54:38 +01:00
Marius Lindvall
c6c919f272 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (104 of 104 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/nb_NO/
2019-12-09 13:54:38 +01:00
Jordi Borràs i Vivó
fa88bb2d7e Translated using Weblate (Catalan)
Currently translated at 100.0% (27 of 27 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/ca/
2019-12-09 13:54:28 +01:00
Jordi Borràs i Vivó
eddd168b16 Translated using Weblate (Catalan)
Currently translated at 100.0% (19 of 19 strings)

Translation: Hauk/PHP backend
Translate-URL: https://traduki.varden.info/projects/hauk/backend-php/ca/
2019-12-09 13:54:27 +01:00
Marius Lindvall
0affc83c0f Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (27 of 27 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nn/
2019-12-09 13:54:27 +01:00
Marius Lindvall
fb7bc0f4c7 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (27 of 27 strings)

Translation: Hauk/Web frontend
Translate-URL: https://traduki.varden.info/projects/hauk/frontend/nb_NO/
2019-12-09 13:54:27 +01:00
Jordi Borràs i Vivó
384dc826ca Added translation using Weblate (Catalan) 2019-12-09 13:38:08 +01:00
Jordi Borràs i Vivó
394cb90491 Added translation using Weblate (Catalan) 2019-12-09 13:12:42 +01:00
Jordi Borràs i Vivó
3449cba1a2 Translated using Weblate (Catalan)
Currently translated at 100.0% (104 of 104 strings)

Translation: Hauk/Android client
Translate-URL: https://traduki.varden.info/projects/hauk/android/ca/
2019-12-09 13:12:28 +01:00
Marius Lindvall
49aabf478c Add last update timer; fixes #76 2019-12-09 13:11:52 +01:00
Jordi Borràs i Vivó
04c0f63dc8 Added translation using Weblate (Catalan) 2019-12-09 12:00:11 +01:00
Marius Lindvall
dbfb58a502 Add different link styles; fixes #73 2019-12-09 00:00:54 +01:00
Marius Lindvall
cacc65b481 Remove x-icon favicon 2019-12-08 14:50:03 +01:00
Marius Lindvall
ae19fbf5d5 Drop high ico resolutions, see #68
Dropping 256x256 and 192x192 sizes.
2019-12-08 13:41:40 +01:00
Marius Lindvall
c2fb3d4a80 Allow disabling E2E without clearing key; fixes #71 2019-12-08 13:23:15 +01:00
Marius Lindvall
a6ccdbb857 Add x-icon favicon; fixes #68 2019-12-08 12:20:21 +01:00
Marius Lindvall
f350a7defe Don't use CDN; fixes #67 2019-12-08 10:46:24 +01:00
Marius Lindvall
8179836e8d Clean up multiarch Docker setup
Remove jq as it is no longer needed, and add warnings to the Dockerfiles meant for Docker Hub.
2019-12-07 23:18:10 +01:00
Marius Lindvall
fc6eed091b Just use the environment variable instead (#65) 2019-12-07 21:59:49 +01:00
Marius Lindvall
82ee59c366 Don't reset the config, actually (#65) 2019-12-07 21:29:56 +01:00
Marius Lindvall
37d7e4727e Reset CLI config after annotating (#65)
Permission to push was denied
2019-12-07 21:02:25 +01:00
Marius Lindvall
925b7b83c0 Fix directory creation (#65) 2019-12-07 20:26:33 +01:00
Marius Lindvall
a1564ec0f4 Check if config exists first (#65) 2019-12-07 19:39:49 +01:00
Marius Lindvall
c43119a396 Enable experimental CLI for cross-compilation (#65) 2019-12-07 19:05:38 +01:00
Marius Lindvall
0d4081f6a9 Use latest Docker for builds (#65) 2019-12-07 18:22:31 +01:00
Marius Lindvall
eaaec730d8 Put QEMU inside the container for build (#65) 2019-12-07 17:42:41 +01:00
Marius Lindvall
4f0e62c557 Register before installing QEMU (#65) 2019-12-07 17:14:24 +01:00
Marius Lindvall
59c8ba5748 Fetch QEMU from apt instead (#65) 2019-12-07 17:00:43 +01:00
Marius Lindvall
3ae9ae4dee Use register tag for QEMU (#65) 2019-12-07 16:30:16 +01:00
Marius Lindvall
9340f78c6a Add multiarch build hooks for Docker; see #65 2019-12-07 16:15:15 +01:00
Marius Lindvall
86c4718055 Fix notice-level error, ref. #64 2019-12-07 12:37:58 +01:00
Marius Lindvall
4c88a26e14 Update README 2019-12-05 20:59:26 +01:00
193 changed files with 6507 additions and 1061 deletions

53
.github/workflows/docker-image.yml vendored Normal file
View file

@ -0,0 +1,53 @@
name: Build Docker image
on:
push:
# branches:
# - 'master'
tags:
- 'v*'
pull_request:
branches:
- 'master'
jobs:
build:
runs-on: ubuntu-latest
steps:
- # Check out repository
name: Checkout
uses: actions/checkout@v3
- # Set up QEMU for multiarch support
name: Set up QEMU
uses: docker/setup-qemu-action@v1
- # Set up Docker BuildX for Docker image building
name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- # Set proper tags
name: Docker metadata
id: docker_meta # you'll use this in the next step
uses: docker/metadata-action@v3
with:
images: bilde2910/hauk
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern=v{{version}}
type=semver,pattern=stable-{{major}}.x
- # Log in to Docker Hub
name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- # Build and push
name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm/v7,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

View file

@ -4,9 +4,11 @@ COPY frontend/ /var/www/html/
COPY docker/start.sh .
RUN apt-get update && \
apt-get install -y memcached libmemcached-dev zlib1g-dev && \
apt-get install -y memcached libmemcached-dev zlib1g-dev libldap2-dev libssl-dev && \
pecl install memcached && \
docker-php-ext-enable memcached
docker-php-ext-enable memcached && \
docker-php-ext-configure ldap --with-libdir=lib/*-linux-gnu*/ && \
docker-php-ext-install ldap
EXPOSE 80/tcp
VOLUME /etc/hauk

View file

@ -24,8 +24,9 @@ phone, and you're good to go!
## System Requirements
- Web server running LEMP or LAMP
- PHP Memcached or Memcache extension installed on websever.
- Web server running PHP and Memcached or Redis.
- PHP `memcached`, `memcache` or `redis` extension installed on the web server.
- PHP `ldap` extension if using LDAP authentication.
- Android 6 or above to run the [companion Android app](https://f-droid.org/packages/info.varden.hauk/).
## Installation instructions
@ -35,13 +36,11 @@ phone, and you're good to go!
to install Hauk in, for example `/var/www/html`. Follow the instructions
given by the install script. Make sure to set a secure hashed password and
edit your site's domain in the configuration file after installation.
3. Start the webserver and make sure Memcached is running.
4. Install the [companion Android app](https://f-droid.org/packages/info.varden.hauk/)
3. Start the web server and make sure Memcached or Redis is running and
[properly configured and firewalled](https://github.com/bilde2910/Hauk/wiki/FAQ#how-do-i-securely-configure-memcachedredis).
4. Install the companion Android app (from your favourite store linked above)
on your phone and enter your server's settings.
When you visit the webroot you may see an experation notice. Hauk uses randomly
generated URL which will be provided by the app.
## Manual installation
If you prefer not to use the install script, you can instead choose to copy the
@ -52,12 +51,32 @@ files manually.
in your web root, for example `/var/www/html`.
3. Modify `include/config.php` to your liking. Make sure to set a secure hashed
password and edit your site's domain in this file.
4. Start the webserver and make sure Memcached is running.
5. Install the [companion Android app](https://f-droid.org/packages/info.varden.hauk/)
4. Start the web server and make sure Memcached or Redis is running and
[properly configured and firewalled](https://github.com/bilde2910/Hauk/wiki/FAQ#how-do-i-securely-configure-memcachedredis).
5. Install the companion Android app (from your favourite store linked above)
on your phone and enter your server's settings.
## Distribution-specific packages
The Hauk backend is available as packages for the following distributions:
### Arch Linux
Install [`hauk-server`](https://aur.archlinux.org/packages/hauk-server/) from
AUR. The backend will be installed to `/usr/share/webapps/hauk-server`.
## Via Docker Compose
The official Docker image on Docker Hub is `bilde2910/hauk`. It comes with several different tags:
| Tag | Description |
| --- | ----------- |
| `latest` | Updated with each commit to this repository and always has the latest changes. |
| `stable-1.x` | The latest tagged [release](https://github.com/bilde2910/Hauk/releases) of version 1.x. |
| `X.Y.Z` | A specific release of the Hauk backend. Note that old versions are not supported and are provided for your convenience only. |
`latest`, `stable-1.x` and all releases from `1.5.2` and up are multi-arch and compiled for x86_64, armv7l and aarch64. `1.5.1` and older are x86_64 only. You can use any of these tags for all architectures, and Docker will automatically pick the correct one. If you need the image for a specific architecture, however, you can fetch them using `*-amd64` (x86_64), `*-arm32v7` (armv7l) or `*-arm64v8` (aarch64) versions of any of the tags (e.g. `latest-arm32v7`).
**docker-compose.yml**
```yaml
@ -109,6 +128,22 @@ server {
}
```
## Upgrading to newer versions
Hauk is versioned according to [Semantic Versioning 2.0.0](https://semver.org/). Any update that is **not a major update** is guaranteed to be without breaking changes, and you can keep the same configuration file for the updated release.
- Major updates add breaking changes that either require manual intervention, or breaks backward compatibility. Update instructions for major versions will be listed in the release notes, as well as either this README or in the wiki. To date there have been no major updates.
- Minor updates add functionality, but does not break backward compatibility. You can still use an older client on a newer server, or a newer client on an older server, though some functionality may be missing. This will be dynamically detected by the client and server, which could e.g. lead to some UI elements being disabled in the app, or a notification made if a user tries to use new functionality that the other endpoint does not support.
- Patch updates are primarily bugfixes.
Aside from certain major changes, you can keep your configuration file. New options may have been added to the config, but these will have sane defaults applied automatically. If you wish to change any new options, you can either reconfigure Hauk from the new config.php template, or copy and paste the relevant options from the new template to your existing file and change the appropriate values.
Installations done using either the installer (`install.sh`) or via manual file copy can be upgraded simply by pulling the latest version of this repository and running the installer again, or overwriting the installation with the new files.
Installations done via distribution-specific packages will be updated to the latest version by your package manager.
Docker installations will be updated whenever you pull the image. If you're using Docker, you can reserve yourself from receiving major updates (which may contain breaking changes) by using the `stable-*` tag instead of `latest`. If you use a specific versioned tag, your installation will be locked at that specific version and you will not receive feature updates or bugfixes unless you manually change the tag and pull.
## Demo server
If you'd like to see what Hauk can do, download the app and insert connection details for the demo server:
@ -118,21 +153,41 @@ Password: `demo`
Location shares on the demo server is limited to 2 minutes and is only meant for demonstration purposes. Set up your own server to use Hauk to its full extent.
<details>
<summary>Demo server privacy policy - Last updated December 26, 2019</summary>
**Last updated: December 26, 2019**
The demo server is limited by configuration to shares no longer than 2 minutes. This means that no matter what happens, the location data you send to the demo server will be deleted automatically after at most 2 minutes from session initiation. Location data is never logged to disk in any way and only stays in RAM for this time. After the session ends, the data is no longer available. It is a vanilla installation of Hauk from GitHub and the code has not been altered in any way.
The server currently uses CloudFlare for DDoS protection, hence CloudFlare can see the data in transit. You may refer to their privacy policy as well.
The HTTP daemon keeps a standard access log for 7 days. This log contains the link ID (which is useless after the 2 minute session expiration), full URLs, user agents, timestamps, and referring URL (if any). It also logs the IP addresses of the CloudFlare proxy server you connect through. It does *not* contain *your* IP address, only that of a CloudFlare data center somewhere. It's thus not possible to track individuals using it, and not possible to get any meaningful data from it. This log file is used for abuse prevention only.
The server itself is located in Norway and is thus covered under Norwegian privacy regulations.
</details>
## Translators
Hauk depends on volunteers to translate the project. Want to help out? Head over to the [translation portal](https://traduki.varden.info/engage/hauk/) to get started.
[![Translation status](https://traduki.varden.info/widgets/hauk/-/287x66-white.png)](https://traduki.varden.info/engage/hauk/)
**Basque** - osoitz
**Dutch** - Jdekoning141
**French** - thifranc
**German** - natrius, hurradiegams and lemmerk
**Norwegian Bokmål** - bilde2910
**Norwegian Nynorsk** - bilde2910
**Polish** - krystiancha and RuralYak
**Russian** - RuralYak
**Ukrainian** - RuralYak
- **Basque** - osoitz
- **Catalan** - xordiet
- **Dutch** - Jdekoning141
- **French** - thifranc and LukeMarlin
- **German** - natrius, hurradiegams, lemmerk, code-surfer and Marmo
- **Italian** - Vieler
- **Norwegian Bokmål** - bilde2910
- **Norwegian Nynorsk** - bilde2910
- **Polish** - krystiancha and RuralYak
- **Portugese (Brazil)** - arajooj
- **Romanian** - Licaon_Kter
- **Russian** - RuralYak, Brujerizmo90
- **Spanish** - sdstolworthy
- **Turkish** - kylethedeveloper, ayyilmaz
- **Ukrainian** - RuralYak
### Translation status

View file

@ -1,9 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="JD_KEEP_INVALID_TAGS" value="false" />
</JavaCodeStyleSettings>

View file

@ -1,21 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<compositeConfiguration>
<compositeBuild compositeDefinitionSource="SCRIPT" />
</compositeConfiguration>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="/usr/share/java/gradle" />
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="testRunner" value="PLATFORM" />
</GradleProjectSettings>
</option>
</component>

View file

@ -20,6 +20,7 @@
<inspection_tool class="AndroidLintGoogleAppIndexingWarning" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintLogConditional" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintMangledCRLF" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="AndroidLintMissingTranslation" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintSelectableText" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintUnusedIds" enabled="true" level="WARNING" enabled_by_default="true" />

25
android/.idea/jarRepositories.xml generated Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

10
android/.idea/misc.xml generated
View file

@ -5,7 +5,7 @@
<option name="myDefaultNotNull" value="android.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="12">
<list size="14">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
@ -18,12 +18,14 @@
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="11" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
<item index="12" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="13" class="java.lang.String" itemvalue="org.jspecify.nullness.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="11">
<list size="13">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
@ -35,11 +37,13 @@
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="10" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
<item index="11" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="12" class="java.lang.String" itemvalue="org.jspecify.nullness.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

View file

@ -1,14 +1,13 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
compileSdkVersion 33
defaultConfig {
applicationId "info.varden.hauk"
minSdkVersion 23
targetSdkVersion 29
versionCode 10
versionName "1.5.1"
minSdkVersion 24
targetSdkVersion 33
versionCode 14
versionName "1.6.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
@ -17,13 +16,15 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
namespace 'info.varden.hauk'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.preference:preference:1.2.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

View file

@ -1,4 +1,4 @@
package info.varden.hauk.utils;
package info.varden.hauk.system.preferences;
import android.content.Context;
import android.content.SharedPreferences;
@ -9,6 +9,8 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import info.varden.hauk.system.preferences.Preference;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

View file

@ -4,22 +4,36 @@
package="info.varden.hauk">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@drawable/ic_icon"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security"
android:roundIcon="@drawable/ic_icon"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:fullBackupContent="@xml/backup_descriptor">
android:theme="@style/AppTheme">
<activity
android:name=".system.preferences.ui.SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".ui.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="info.varden.hauk.ui.MainActivity" />
</activity>
<activity
android:name=".ui.MainActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait">
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden"
android:theme="@style/HomeTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -29,7 +43,9 @@
<activity
android:name=".global.ui.AuthorizationActivity"
android:screenOrientation="portrait">
</activity>
<receiver
android:name=".global.Receiver"
android:exported="true"
@ -39,23 +55,30 @@
<action android:name="info.varden.hauk.START_ALONE_THEN_MAKE_TOAST" />
</intent-filter>
</receiver>
<receiver android:name=".notify.CopyLinkReceiver" android:exported="false">
<receiver
android:name=".notify.CopyLinkReceiver"
android:exported="false">
<intent-filter>
<action android:name="info.varden.hauk.COPY_LINK" />
</intent-filter>
</receiver>
<receiver android:name=".notify.StopSharingReceiver" android:exported="false">
<receiver
android:name=".notify.StopSharingReceiver"
android:exported="false">
<intent-filter>
<action android:name="info.varden.hauk.STOP_SHARING" />
</intent-filter>
</receiver>
<service android:name=".service.LocationPushService" android:exported="false" android:enabled="true" android:foregroundServiceType="location">
<service
android:name=".service.LocationPushService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location">
<intent-filter>
<action android:name="info.varden.hauk.LOCATION_SERVICE" />
</intent-filter>
</service>
</application>
</manifest>

View file

@ -1,7 +1,10 @@
package info.varden.hauk;
import info.varden.hauk.http.security.CertificateValidationPolicy;
import info.varden.hauk.struct.Version;
import info.varden.hauk.utils.Preference;
import info.varden.hauk.system.preferences.Preference;
import info.varden.hauk.system.preferences.indexresolver.NightModeStyle;
import info.varden.hauk.system.preferences.indexresolver.ProxyTypeResolver;
/**
* Constants used in the Hauk app.
@ -25,16 +28,26 @@ public enum Constants {
// Keys for use in stored server preferences.
public static final Preference<String> PREF_SERVER_ENCRYPTED = new Preference.EncryptedString("cryptServer", "");
public static final Preference<ProxyTypeResolver> PREF_PROXY_TYPE = new Preference.Enum<>("proxyType", ProxyTypeResolver.SYSTEM_DEFAULT);
public static final Preference<String> PREF_PROXY_HOST = new Preference.String("proxyHost", "localhost");
public static final Preference<Integer> PREF_PROXY_PORT = new Preference.Integer("proxyPort", 9050);
public static final Preference<Integer> PREF_CONNECTION_TIMEOUT = new Preference.Integer("connectTimeout", 10);
public static final Preference<CertificateValidationPolicy> PREF_CERTIFICATE_VALIDATION = new Preference.Enum<>("tlsCertValidation", CertificateValidationPolicy.VALIDATE_ALL);
public static final Preference<String> PREF_USERNAME_ENCRYPTED = new Preference.EncryptedString("cryptUsername", "");
public static final Preference<String> PREF_PASSWORD_ENCRYPTED = new Preference.EncryptedString("cryptPassword", "");
public static final Preference<Integer> PREF_DURATION = new Preference.Integer("duration", 30);
public static final Preference<Integer> PREF_INTERVAL = new Preference.Integer("interval", 1);
public static final Preference<Integer> PREF_NO_GNSS_FALLBACK = new Preference.Integer("noGnssFallback", 45);
public static final Preference<Float> PREF_UPDATE_DISTANCE = new Preference.Float("minUpdateDistance", 0.0F);
public static final Preference<String> PREF_CUSTOM_ID = new Preference.String("requestLink", "");
public static final Preference<Boolean> PREF_ENABLE_E2E = new Preference.Boolean("enableE2E", false);
public static final Preference<String> PREF_E2E_PASSWORD = new Preference.EncryptedString("e2ePassword", "");
public static final Preference<String> PREF_NICKNAME = new Preference.String("nickname", "");
public static final Preference<Integer> PREF_DURATION_UNIT = new Preference.Integer("durUnit", Constants.DURATION_UNIT_MINUTES);
public static final Preference<Boolean> PREF_REMEMBER_PASSWORD = new Preference.Boolean("rememberPassword", false);
public static final Preference<Boolean> PREF_ALLOW_ADOPTION = new Preference.Boolean("allowAdoption", true);
public static final Preference<NightModeStyle> PREF_NIGHT_MODE = new Preference.Enum<>("nightMode", NightModeStyle.FOLLOW_SYSTEM);
public static final Preference<Boolean> PREF_CONFIRM_STOP = new Preference.Boolean("confirmStop", true);
public static final Preference<Boolean> PREF_HIDE_LOGO = new Preference.Boolean("hideLogo", false);
@Deprecated // Use PREF_SERVER_ENCRYPTED instead
public static final Preference<String> PREF_SERVER = new Preference.String("server", "");
@ -55,12 +68,16 @@ public enum Constants {
// Regular expression for extracting a share ID from a URL when adopting a share.
public static final String REGEX_ADOPT_ID_FROM_LINK = "\\?([A-Za-z0-9-]+)";
// Default date format.
public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss z";
// Formatting and input validation.
public static final String DATE_FORMAT_UI = "yyyy-MM-dd HH:mm:ss z";
public static final String DATE_FORMAT_LOG = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
public static final int PORT_MIN = 0;
public static final int PORT_MAX = 65536;
// Keys for intent extras.
public static final String EXTRA_SHARE = "share";
public static final String EXTRA_STOP_TASK = "stopTask";
public static final String EXTRA_HANDLER = "handler";
public static final String EXTRA_GNSS_ACTIVE_TASK = "gnssActiveTask";
public static final String EXTRA_BROADCAST_RECEIVER_REGISTRY_INDEX = "dataRegistryIndex";
public static final String EXTRA_BROADCAST_AUTHORIZATION_IDENTIFIER = "source";
@ -71,6 +88,7 @@ public enum Constants {
public static final String EXTRA_SESSION_CUSTOM_ID = "requestLink";
public static final String EXTRA_SESSION_E2E_PASSWORD = "e2ePassword";
public static final String EXTRA_SESSION_INTERVAL = "interval";
public static final String EXTRA_SESSION_MIN_DISTANCE = "minDistance";
public static final String EXTRA_SESSION_ALLOW_ADOPT = "adoptable";
// Content types for intents.
@ -96,6 +114,7 @@ public enum Constants {
public static final String PACKET_PARAM_LONGITUDE = "lon";
public static final String PACKET_PARAM_NICKNAME = "nic";
public static final String PACKET_PARAM_PASSWORD = "pwd";
public static final String PACKET_PARAM_PROVIDER_ACCURACY = "prv";
public static final String PACKET_PARAM_SALT = "salt";
public static final String PACKET_PARAM_SESSION_ID = "sid";
public static final String PACKET_PARAM_SHARE_ID = "lid";

View file

@ -7,34 +7,76 @@ import info.varden.hauk.R;
*
* @author Marius Lindvall
*/
public enum Buttons {
OK_CANCEL (R.string.btn_ok, R.string.btn_cancel),
YES_NO (R.string.btn_yes, R.string.btn_no),
CREATE_CANCEL (R.string.btn_create, R.string.btn_cancel),
SETTINGS_DISMISS (R.string.btn_dismiss, R.string.btn_show_settings),
OK_SHARE (R.string.btn_ok, R.string.btn_share_short);
// The dialog has one positive and one negative button.
private final int positive;
private final int negative;
Buttons(int positive, int negative) {
this.positive = positive;
this.negative = negative;
public final class Buttons {
private Buttons() {
}
/**
* Returns a strings resource ID that corresponds to this button set's positive button.
*/
public int getPositiveButton() {
return this.positive;
public enum Two {
OK_CANCEL(R.string.btn_ok, R.string.btn_cancel),
YES_NO(R.string.btn_yes, R.string.btn_no),
CREATE_CANCEL(R.string.btn_create, R.string.btn_cancel),
SETTINGS_DISMISS(R.string.btn_dismiss, R.string.btn_show_settings),
SETTINGS_OK(R.string.btn_ok, R.string.btn_show_settings),
OK_SHARE(R.string.btn_ok, R.string.btn_share_short);
// The dialog has one positive and one negative button.
private final int positive;
private final int negative;
Two(int positive, int negative) {
this.positive = positive;
this.negative = negative;
}
/**
* Returns a strings resource ID that corresponds to this button set's positive button.
*/
public int getPositiveButton() {
return this.positive;
}
/**
* Returns a strings resource ID that corresponds to this button set's negative button.
*/
public int getNegativeButton() {
return this.negative;
}
}
/**
* Returns a strings resource ID that corresponds to this button set's negative button.
*/
public int getNegativeButton() {
return this.negative;
public enum Three {
YES_NO_REMEMBER(R.string.btn_yes, R.string.btn_remember, R.string.btn_no);
// The dialog has one positive, one neutral and one negative button.
private final int positive;
private final int neutral;
private final int negative;
Three(int positive, int neutral, int negative) {
this.positive = positive;
this.neutral = neutral;
this.negative = negative;
}
/**
* Returns a strings resource ID that corresponds to this button set's positive button.
*/
public int getPositiveButton() {
return this.positive;
}
/**
* Returns a strings resource ID that corresponds to this button set's neutral button.
*/
public int getNeutralButton() {
return this.neutral;
}
/**
* Returns a strings resource ID that corresponds to this button set's negative button.
*/
public int getNegativeButton() {
return this.negative;
}
}
}
}

View file

@ -30,4 +30,11 @@ public interface CustomDialogBuilder {
*/
@Nullable
View createView(Context ctx);
interface Three extends CustomDialogBuilder {
/**
* Fires when the neutral button is clicked in the dialog.
*/
void onNeutral();
}
}

View file

@ -135,7 +135,7 @@ public final class DialogService {
* @param buttons The buttons to display on the dialog.
* @param builder A dialog builder that builds a View and handles the dialog buttons.
*/
public void showDialog(int title, int message, Buttons buttons, CustomDialogBuilder builder) {
public void showDialog(int title, int message, Buttons.Two buttons, CustomDialogBuilder builder) {
showDialog(title, this.ctx.getString(message), buttons, builder);
}
@ -147,7 +147,7 @@ public final class DialogService {
* @param buttons The buttons to display on the dialog.
* @param builder A dialog builder that builds a View and handles the dialog buttons.
*/
public void showDialog(int title, String message, Buttons buttons, CustomDialogBuilder builder) {
public void showDialog(int title, String message, Buttons.Two buttons, CustomDialogBuilder builder) {
showDialog(this.ctx.getString(title), message, buttons, builder);
}
@ -159,7 +159,7 @@ public final class DialogService {
* @param buttons The buttons to display on the dialog.
* @param builder A dialog builder that builds a View and handles the dialog buttons.
*/
private void showDialog(String title, String message, Buttons buttons, CustomDialogBuilder builder) {
private void showDialog(String title, String message, Buttons.Two buttons, CustomDialogBuilder builder) {
View view = builder.createView(this.ctx);
if (view != null) {
TypedValue tv = new TypedValue();
@ -184,6 +184,64 @@ public final class DialogService {
dlgAlert.create().show();
}
/**
* Shows a dialog box with a custom rendered View.
*
* @param title A string resource representing the title of the dialog box.
* @param message A string resource representing the body of the dialog box.
* @param buttons The buttons to display on the dialog.
* @param builder A dialog builder that builds a View and handles the dialog buttons.
*/
public void showDialog(int title, int message, Buttons.Three buttons, CustomDialogBuilder.Three builder) {
showDialog(title, this.ctx.getString(message), buttons, builder);
}
/**
* Shows a dialog box with a custom rendered View.
*
* @param title A string resource representing the title of the dialog box.
* @param message A string representing the body of the dialog box.
* @param buttons The buttons to display on the dialog.
* @param builder A dialog builder that builds a View and handles the dialog buttons.
*/
public void showDialog(int title, String message, Buttons.Three buttons, CustomDialogBuilder.Three builder) {
showDialog(this.ctx.getString(title), message, buttons, builder);
}
/**
* Shows a dialog box with a custom rendered View.
*
* @param title A string representing the title of the dialog box.
* @param message A string representing the body of the dialog box.
* @param buttons The buttons to display on the dialog.
* @param builder A dialog builder that builds a View and handles the dialog buttons.
*/
private void showDialog(String title, String message, Buttons.Three buttons, CustomDialogBuilder.Three builder) {
View view = builder.createView(this.ctx);
if (view != null) {
TypedValue tv = new TypedValue();
int padding = 0;
if (this.ctx.getTheme().resolveAttribute(R.attr.dialogPreferredPadding, tv, true)) {
padding = TypedValue.complexToDimensionPixelSize(tv.data, this.ctx.getResources().getDisplayMetrics());
}
view.setPadding(padding, padding, padding, 0);
}
Log.d("Showing dialog with title=%s, message=%s, builder=%s, view=%s", title, message, builder, view); //NON-NLS
AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this.ctx);
dlgAlert.setMessage(message);
dlgAlert.setTitle(title);
if (view != null) dlgAlert.setView(view);
dlgAlert.setPositiveButton(this.ctx.getString(buttons.getPositiveButton()), new PositiveClickListener(builder));
dlgAlert.setNegativeButton(this.ctx.getString(buttons.getNegativeButton()), new NegativeClickListener(builder));
dlgAlert.setNeutralButton(this.ctx.getString(buttons.getNeutralButton()), new NeutralClickListener(builder));
dlgAlert.setCancelable(false);
dlgAlert.create().show();
}
/**
* A click listener for a dialog button that calls a given {@link Runnable} when the button is
* clicked.
@ -218,6 +276,24 @@ public final class DialogService {
}
}
/**
* A click listener for a dialog button that calls the neutral action handler of the given
* {@link CustomDialogBuilder} when the button is clicked.
*/
private static final class NeutralClickListener implements DialogInterface.OnClickListener {
private final CustomDialogBuilder.Three builder;
private NeutralClickListener(CustomDialogBuilder.Three builder) {
this.builder = builder;
}
@Override
public void onClick(DialogInterface dialogInterface, int which) {
Log.d("Closing dialog, which=%s (neutral)", which); //NON-NLS
this.builder.onNeutral();
}
}
/**
* A click listener for a dialog button that calls the positive action handler of the given
* {@link CustomDialogBuilder} when the button is clicked.

View file

@ -0,0 +1,64 @@
package info.varden.hauk.dialog;
import android.content.Context;
import android.view.View;
import androidx.annotation.Nullable;
import info.varden.hauk.Constants;
import info.varden.hauk.manager.SessionManager;
import info.varden.hauk.struct.Share;
import info.varden.hauk.system.preferences.PreferenceManager;
import info.varden.hauk.utils.Log;
/**
* Prompt that confirms with the user if they really intended to stop sharing their location.
*
* @author Marius Lindvall
*/
public final class StopSharingConfirmationPrompt implements CustomDialogBuilder.Three {
private final PreferenceManager prefs;
private final SessionManager manager;
private final Share share;
public StopSharingConfirmationPrompt(PreferenceManager prefs, SessionManager manager) {
this(prefs, manager, null);
}
public StopSharingConfirmationPrompt(PreferenceManager prefs, SessionManager manager, Share share) {
this.prefs = prefs;
this.manager = manager;
this.share = share;
}
@Override
public void onNeutral() {
Log.i("Disabling future confirmation prompts when stopping shares"); //NON-NLS
this.prefs.set(Constants.PREF_CONFIRM_STOP, false);
if (this.share == null) {
this.manager.stopSharing();
} else {
this.manager.stopSharing(this.share);
}
}
@Override
public void onPositive() {
if (this.share == null) {
this.manager.stopSharing();
} else {
this.manager.stopSharing(this.share);
}
}
@Override
public void onNegative() {
// Do nothing.
}
@Nullable
@Override
public View createView(Context ctx) {
return null;
}
}

View file

@ -6,6 +6,10 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.widget.Toast;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketAddress;
import info.varden.hauk.Constants;
import info.varden.hauk.R;
import info.varden.hauk.global.ui.AuthorizationActivity;
@ -13,13 +17,15 @@ import info.varden.hauk.global.ui.DisplayShareDialogListener;
import info.varden.hauk.global.ui.toast.GNSSStatusUpdateListenerImpl;
import info.varden.hauk.global.ui.toast.SessionInitiationResponseHandlerImpl;
import info.varden.hauk.global.ui.toast.ShareListenerImpl;
import info.varden.hauk.http.ConnectionParameters;
import info.varden.hauk.http.SessionInitiationPacket;
import info.varden.hauk.http.security.CertificateValidationPolicy;
import info.varden.hauk.manager.SessionManager;
import info.varden.hauk.struct.AdoptabilityPreference;
import info.varden.hauk.system.LocationPermissionsNotGrantedException;
import info.varden.hauk.system.LocationServicesDisabledException;
import info.varden.hauk.system.preferences.PreferenceManager;
import info.varden.hauk.utils.DeprecationMigrator;
import info.varden.hauk.utils.PreferenceManager;
import info.varden.hauk.utils.TimeUtils;
/**
@ -169,12 +175,34 @@ public final class Receiver extends BroadcastReceiver {
String password = intent.hasExtra(Constants.EXTRA_SESSION_PASSWORD) ? intent.getStringExtra(Constants.EXTRA_SESSION_PASSWORD) : fallback.get(Constants.PREF_PASSWORD_ENCRYPTED);
int duration = intent.hasExtra(Constants.EXTRA_SESSION_DURATION) ? intent.getIntExtra(Constants.EXTRA_SESSION_DURATION, 0) : TimeUtils.timeUnitsToSeconds(fallback.get(Constants.PREF_DURATION), fallback.get(Constants.PREF_DURATION_UNIT));
int interval = intent.hasExtra(Constants.EXTRA_SESSION_INTERVAL) ? intent.getIntExtra(Constants.EXTRA_SESSION_INTERVAL, 0) : fallback.get(Constants.PREF_INTERVAL);
float minDistance = intent.hasExtra(Constants.EXTRA_SESSION_MIN_DISTANCE) ? intent.getIntExtra(Constants.EXTRA_SESSION_MIN_DISTANCE, 0) : fallback.get(Constants.PREF_UPDATE_DISTANCE);
String customID = intent.hasExtra(Constants.EXTRA_SESSION_CUSTOM_ID) ? intent.getStringExtra(Constants.EXTRA_SESSION_CUSTOM_ID) : fallback.get(Constants.PREF_CUSTOM_ID);
String e2ePass = intent.hasExtra(Constants.EXTRA_SESSION_E2E_PASSWORD) ? intent.getStringExtra(Constants.EXTRA_SESSION_E2E_PASSWORD) : fallback.get(Constants.PREF_E2E_PASSWORD);
String e2ePass = "";
if (intent.hasExtra(Constants.EXTRA_SESSION_E2E_PASSWORD)) {
e2ePass = intent.getStringExtra(Constants.EXTRA_SESSION_E2E_PASSWORD);
} else if (fallback.get(Constants.PREF_ENABLE_E2E)) {
e2ePass = fallback.get(Constants.PREF_E2E_PASSWORD);
}
assert server != null;
server = server.endsWith("/") ? server : server + "/";
return new SessionInitiationPacket.InitParameters(server, username, password, duration, interval, customID, e2ePass);
int timeout = fallback.get(Constants.PREF_CONNECTION_TIMEOUT) * (int) TimeUtils.MILLIS_PER_SECOND;
CertificateValidationPolicy tlsPolicy = fallback.get(Constants.PREF_CERTIFICATE_VALIDATION);
ConnectionParameters connParams;
Proxy.Type proxyType = fallback.get(Constants.PREF_PROXY_TYPE).resolve();
if (proxyType == Proxy.Type.DIRECT) {
connParams = new ConnectionParameters(Proxy.NO_PROXY.type(), Proxy.NO_PROXY.address(), timeout, tlsPolicy);
} else if (proxyType != null) {
SocketAddress proxyAddr = new InetSocketAddress(fallback.get(Constants.PREF_PROXY_HOST).trim(), fallback.get(Constants.PREF_PROXY_PORT));
connParams = new ConnectionParameters(proxyType, proxyAddr, timeout, tlsPolicy);
} else {
connParams = new ConnectionParameters(null, null, timeout, tlsPolicy);
}
SessionInitiationPacket.InitParameters initParams = new SessionInitiationPacket.InitParameters(server, username, password, duration, interval, minDistance, customID, e2ePass);
initParams.setConnectionParameters(connParams);
return initParams;
}
}

View file

@ -32,6 +32,11 @@ public final class GNSSStatusUpdateListenerImpl implements GNSSStatusUpdateListe
Toast.makeText(this.ctx, R.string.label_status_wait, Toast.LENGTH_LONG).show();
}
@Override
public void onGNSSConnectionLost() {
Toast.makeText(this.ctx, R.string.label_status_lost_gnss, Toast.LENGTH_LONG).show();
}
@Override
public void onCoarseLocationReceived() {
Toast.makeText(this.ctx, R.string.label_status_coarse, Toast.LENGTH_LONG).show();
@ -41,4 +46,14 @@ public final class GNSSStatusUpdateListenerImpl implements GNSSStatusUpdateListe
public void onAccurateLocationReceived() {
Toast.makeText(this.ctx, R.string.label_status_ok, Toast.LENGTH_LONG).show();
}
@Override
public void onServerConnectionLost() {
// Silently ignore
}
@Override
public void onServerConnectionRestored() {
// Silently ignore
}
}

View file

@ -12,7 +12,7 @@ import info.varden.hauk.struct.Share;
*
* @author Marius Lindvall
*/
public class ShareListenerImpl implements ShareListener {
public final class ShareListenerImpl implements ShareListener {
/**
* Android application context.
*/

View file

@ -33,8 +33,8 @@ public abstract class AdoptSharePacket extends Packet {
* @param origin The share ID of the share to adopt.
* @param nickname The nickname that should be assigned to the user when adopted.
*/
public AdoptSharePacket(Context ctx, Share target, String origin, String nickname) {
super(ctx, target.getSession().getServerURL(), Constants.URL_PATH_ADOPT_SHARE);
protected AdoptSharePacket(Context ctx, Share target, String origin, String nickname) {
super(ctx, target.getSession().getServerURL(), target.getSession().getConnectionParameters(), Constants.URL_PATH_ADOPT_SHARE);
this.nickname = nickname;
setParameter(Constants.PACKET_PARAM_SESSION_ID, target.getSession().getID());
setParameter(Constants.PACKET_PARAM_NICKNAME, nickname);

View file

@ -0,0 +1,68 @@
package info.varden.hauk.http;
import androidx.annotation.Nullable;
import java.io.Serializable;
import java.net.Proxy;
import java.net.SocketAddress;
import info.varden.hauk.http.security.CertificateValidationPolicy;
/**
* Structure used to store connection parameters for backend connections, e.g. proxy details.
*
* @author Marius Lindvall
*/
public final class ConnectionParameters implements Serializable {
private static final long serialVersionUID = -6275381322711990147L;
/**
* The type of proxy to use for the connection.
*/
private final Proxy.Type proxyType;
/**
* The proxy endpoint address.
*/
private final SocketAddress proxyAddress;
/**
* The maximum connection timeout, in milliseconds.
*/
private final int connectTimeout;
/**
* TLS certificate validation policy for the connection.
*/
private final CertificateValidationPolicy tlsPolicy;
public ConnectionParameters(Proxy.Type proxyType, SocketAddress proxyAddress, int connectTimeout, CertificateValidationPolicy tlsPolicy) {
this.proxyType = proxyType;
this.proxyAddress = proxyAddress;
this.connectTimeout = connectTimeout;
this.tlsPolicy = tlsPolicy;
}
@Nullable
Proxy getProxy() {
return this.proxyType == null || this.proxyAddress == null ? null : new Proxy(this.proxyType, this.proxyAddress);
}
int getTimeout() {
return this.connectTimeout;
}
CertificateValidationPolicy getTLSPolicy() {
return this.tlsPolicy;
}
@Override
public String toString() {
return "ConnectionParameters{"
+ "proxyType=" + this.proxyType
+ ",proxyAddress=" + this.proxyAddress
+ ",connectTimeout=" + this.connectTimeout
+ ",tlsPolicy=" + this.tlsPolicy
+ "}";
}
}

View file

@ -10,18 +10,27 @@ import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import javax.net.ssl.HttpsURLConnection;
import info.varden.hauk.BuildConfig;
import info.varden.hauk.Constants;
import info.varden.hauk.R;
import info.varden.hauk.http.security.CertificateValidationPolicy;
import info.varden.hauk.http.security.InsecureHostnameVerifier;
import info.varden.hauk.http.security.InsecureTrustManager;
import info.varden.hauk.struct.Version;
import info.varden.hauk.utils.Log;
/**
* An asynchronous task that HTTP-POSTs data to a given URL with the given POST fields.
@ -29,11 +38,6 @@ import info.varden.hauk.struct.Version;
* @author Marius Lindvall
*/
public class ConnectionThread extends AsyncTask<ConnectionThread.Request, String, ConnectionThread.Response> {
/**
* The maximum time to wait for the request to complete before the request times out.
*/
private static final int TIMEOUT = 10000;
/**
* A callback that is called after the request is completed. Contains received data, or errors,
* if applicable.
@ -62,11 +66,30 @@ public class ConnectionThread extends AsyncTask<ConnectionThread.Request, String
@Override
@SuppressWarnings("HardCodedStringLiteral")
protected final Response doInBackground(Request... params) {
int seq = new Random().nextInt();
try {
// Open a connection to the Hauk server and post the data.
URL url = new URL(params[0].getURL());
HttpURLConnection client = (HttpURLConnection) url.openConnection();
client.setConnectTimeout(TIMEOUT);
Request req = params[0];
Log.v("Assigning seq=%s for request %s", seq, req);
// Configure and open the connection.
Proxy proxy = req.getParameters().getProxy();
URL url = new URL(req.getURL());
HttpURLConnection client = (HttpURLConnection) (proxy == null ? url.openConnection() : url.openConnection(proxy));
if (url.getHost().endsWith(".onion") && url.getProtocol().equals("https")) {
// Check if TLS validation should be disabled for .onion addresses over HTTPS.
if (req.getParameters().getTLSPolicy().equals(CertificateValidationPolicy.DISABLE_TRUST_ANCHOR_ONION)) {
Log.v("[seq:%s] Setting insecure SSL socket factory for connection to comply with TLS policy", seq);
((HttpsURLConnection) client).setSSLSocketFactory(InsecureTrustManager.getSocketFactory());
} else if (req.getParameters().getTLSPolicy().equals(CertificateValidationPolicy.DISABLE_ALL_ONION)) {
Log.v("[seq:%s] Setting insecure SSL socket factory and disabling hostname validation for connection to comply with TLS policy", seq);
((HttpsURLConnection) client).setSSLSocketFactory(InsecureTrustManager.getSocketFactory());
((HttpsURLConnection) client).setHostnameVerifier(new InsecureHostnameVerifier());
}
}
// Post the data.
Log.v("[seq:%s] Setting connection parameters", seq);
client.setConnectTimeout(req.getParameters().getTimeout());
client.setRequestMethod("POST");
client.setRequestProperty("Accept-Language", Locale.getDefault().getLanguage());
client.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
@ -74,6 +97,7 @@ public class ConnectionThread extends AsyncTask<ConnectionThread.Request, String
client.setDoInput(true);
client.setDoOutput(true);
Log.v("[seq:%s] Writing data to socket", seq);
OutputStream os = client.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
writer.write(params[0].getURLEncodedData());
@ -81,6 +105,7 @@ public class ConnectionThread extends AsyncTask<ConnectionThread.Request, String
os.close();
int response = client.getResponseCode();
Log.v("[seq:%s] Response code for request is %s", seq, response);
if (response == HttpURLConnection.HTTP_OK) {
// The response should be returned as an array of strings where each element of the
// array is one line of output. Hauk uses this array as an argument array when
@ -89,16 +114,20 @@ public class ConnectionThread extends AsyncTask<ConnectionThread.Request, String
ArrayList<String> lines = new ArrayList<>();
BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8));
while ((line = br.readLine()) != null) {
Log.v("[seq:%s] resp += \"%s\"", seq, line);
lines.add(line);
}
br.close();
Log.v("[seq:%s] Returning success response", seq);
return new Response(null, lines.toArray(new String[0]), new Version(client.getHeaderField(Constants.HTTP_HEADER_HAUK_VERSION)));
} else {
// Hauk only returns HTTP 200; any other response should be considered an error.
Log.v("[seq:%s] Returning HTTP code failure response", seq);
return new Response(new ServerException(String.format(params[0].getContext().getString(R.string.err_response_code), String.valueOf(response))), null, null);
}
} catch (Exception ex) {
// If an exception occurred, return no data.
Log.v("[seq:%s] Returning exception failure response", ex, seq);
return new Response(ex, null, null);
}
}
@ -122,18 +151,21 @@ public class ConnectionThread extends AsyncTask<ConnectionThread.Request, String
private final Context ctx;
private final String url;
private final Map<String, String> data;
private final ConnectionParameters params;
/**
* Constructs an HTTP request.
* Constructs an HTTP request that should be passed through a proxy.
*
* @param ctx Android application context.
* @param url The URL to POST data to.
* @param data A set of key-value pairs consisting of data to be sent in the POST request.
* @param ctx Android application context.
* @param url The URL to POST data to.
* @param data A set of key-value pairs consisting of data to be sent in the POST request.
* @param params The parameters that should be used when establishing the connection.
*/
Request(Context ctx, String url, Map<String, String> data) {
Request(Context ctx, String url, Map<String, String> data, ConnectionParameters params) {
this.ctx = ctx;
this.url = url;
this.data = Collections.unmodifiableMap(data);
this.params = params;
}
private Context getContext() {
@ -144,6 +176,10 @@ public class ConnectionThread extends AsyncTask<ConnectionThread.Request, String
return this.url;
}
private ConnectionParameters getParameters() {
return this.params;
}
private String getURLEncodedData() throws UnsupportedEncodingException {
// Create a URL-encoded data body for the HTTP request.
StringBuilder sb = new StringBuilder();
@ -157,6 +193,22 @@ public class ConnectionThread extends AsyncTask<ConnectionThread.Request, String
}
return sb.toString();
}
@Override
public final String toString() {
String body;
try {
body = getURLEncodedData();
} catch (UnsupportedEncodingException e) {
Log.e("Unsupported encoding used in Request#toString()", e);
body = "<exception>";
}
return "Request{"
+ "url=" + this.url
+ ",body=" + body
+ ",params=" + this.params
+ "}";
}
}
/**
@ -196,6 +248,15 @@ public class ConnectionThread extends AsyncTask<ConnectionThread.Request, String
Version getServerVersion() {
return this.ver;
}
@Override
public String toString() {
return "Response{"
+ "ex=" + this.ex
+ ",data=" + Arrays.toString(this.data)
+ ",ver=" + this.ver
+ "}";
}
}
/**

View file

@ -11,6 +11,7 @@ import javax.crypto.Cipher;
import info.varden.hauk.Constants;
import info.varden.hauk.R;
import info.varden.hauk.http.parameter.LocationProvider;
import info.varden.hauk.struct.Session;
import info.varden.hauk.struct.Version;
import info.varden.hauk.utils.Log;
@ -43,14 +44,15 @@ public abstract class LocationUpdatePacket extends Packet {
* @param session The session for which location is being updated.
* @param location The updated location data obtained from GNSS/network sensors.
*/
protected LocationUpdatePacket(Context ctx, Session session, Location location) {
super(ctx, session.getServerURL(), Constants.URL_PATH_POST_LOCATION);
protected LocationUpdatePacket(Context ctx, Session session, Location location, LocationProvider accuracy) {
super(ctx, session.getServerURL(), session.getConnectionParameters(), Constants.URL_PATH_POST_LOCATION);
setParameter(Constants.PACKET_PARAM_SESSION_ID, session.getID());
if (session.getDerivableE2EKey() == null) {
// If not using end-to-end encryption, send parameters in plain text.
setParameter(Constants.PACKET_PARAM_LATITUDE, String.valueOf(location.getLatitude()));
setParameter(Constants.PACKET_PARAM_LONGITUDE, String.valueOf(location.getLongitude()));
setParameter(Constants.PACKET_PARAM_PROVIDER_ACCURACY, String.valueOf(accuracy.getMode()));
setParameter(Constants.PACKET_PARAM_TIMESTAMP, String.valueOf(System.currentTimeMillis() / (double) TimeUtils.MILLIS_PER_SECOND));
// Not all devices provide these parameters:
@ -66,6 +68,7 @@ public abstract class LocationUpdatePacket extends Packet {
setParameter(Constants.PACKET_PARAM_LATITUDE, Base64.encodeToString(cipher.doFinal(String.valueOf(location.getLatitude()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT));
setParameter(Constants.PACKET_PARAM_LONGITUDE, Base64.encodeToString(cipher.doFinal(String.valueOf(location.getLongitude()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT));
setParameter(Constants.PACKET_PARAM_PROVIDER_ACCURACY, Base64.encodeToString(cipher.doFinal(String.valueOf(accuracy.getMode()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT));
setParameter(Constants.PACKET_PARAM_TIMESTAMP, Base64.encodeToString(cipher.doFinal(String.valueOf(System.currentTimeMillis() / (double) TimeUtils.MILLIS_PER_SECOND).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT));
// Not all devices provide these parameters:
@ -77,8 +80,9 @@ public abstract class LocationUpdatePacket extends Packet {
}
}
@SuppressWarnings("DesignForExtension")
@Override
protected final void onSuccess(String[] data, Version backendVersion) throws ServerException {
protected void onSuccess(String[] data, Version backendVersion) throws ServerException {
// Somehow the data array can be empty? Check for this.
if (data.length < 1) {
throw new ServerException(getContext(), R.string.err_empty);

View file

@ -35,7 +35,7 @@ public abstract class NewLinkPacket extends Packet {
* @param allowAdoption Whether or not this share should be adoptable.
*/
protected NewLinkPacket(Context ctx, Session session, boolean allowAdoption) {
super(ctx, session.getServerURL(), Constants.URL_PATH_CREATE_NEW_LINK);
super(ctx, session.getServerURL(), session.getConnectionParameters(), Constants.URL_PATH_CREATE_NEW_LINK);
this.session = session;
setParameter(Constants.PACKET_PARAM_SESSION_ID, session.getID());
setParameter(Constants.PACKET_PARAM_ADOPTABLE, allowAdoption ? "1" : "0");

View file

@ -19,6 +19,7 @@ public abstract class Packet {
private final Context ctx;
private final String server;
private final String path;
private final ConnectionParameters connParams;
/**
* Called if the request is successful.
@ -47,11 +48,12 @@ public abstract class Packet {
* @param server The full Hauk server base URL, including trailing slash.
* @param path The path underneath the base URL that should be called.
*/
Packet(Context ctx, String server, String path) {
Packet(Context ctx, String server, ConnectionParameters connParams, String path) {
this.params = new HashMap<>();
this.ctx = ctx;
this.server = server;
this.path = path;
this.connParams = connParams;
}
/**
@ -94,6 +96,6 @@ public abstract class Packet {
onFailure(e);
}
}
}).execute(new ConnectionThread.Request(this.ctx, this.server + this.path, this.params));
}).execute(new ConnectionThread.Request(this.ctx, this.server + this.path, this.params, this.connParams));
}
}

View file

@ -8,7 +8,7 @@ import android.content.Context;
*
* @author Marius Lindvall
*/
class ServerException extends Exception {
public class ServerException extends Exception {
private static final long serialVersionUID = 2879124634145201633L;
/**

View file

@ -38,7 +38,7 @@ public class SessionInitiationPacket extends Packet {
private final byte[] salt;
private SessionInitiationPacket(Context ctx, InitParameters params, ResponseHandler handler) {
super(ctx, params.getServerURL(), Constants.URL_PATH_CREATE_SHARE);
super(ctx, params.getServerURL(), params.getConnectionParameters(), Constants.URL_PATH_CREATE_SHARE);
this.params = params;
this.handler = handler;
if (params.getUsername() != null) {
@ -155,7 +155,16 @@ public class SessionInitiationPacket extends Packet {
}
// Create a share and pass it upstream.
Session session = new Session(this.params.getServerURL(), backendVersion, sessionID, this.params.getDuration() * TimeUtils.MILLIS_PER_SECOND + System.currentTimeMillis(), this.params.getInterval(), e2eParams);
Session session = new Session(
this.params.getServerURL(),
this.params.getConnectionParameters(),
backendVersion,
sessionID,
this.params.getDuration() * TimeUtils.MILLIS_PER_SECOND + System.currentTimeMillis(),
this.params.getInterval(),
this.params.getMinimumDistance(),
e2eParams
);
Share share = new Share(session, viewURL, viewID, joinCode, this.mode);
this.handler.onSessionInitiated(share);
@ -219,9 +228,12 @@ public class SessionInitiationPacket extends Packet {
private final String password;
private final int duration;
private final int interval;
private final float minDistance;
private final String customID;
private final String e2ePass;
private ConnectionParameters connParams;
/**
* Declares initialization parameters for a session initiation request.
*
@ -231,12 +243,14 @@ public class SessionInitiationPacket extends Packet {
* @param duration The duration, in seconds, to run the share for.
* @param interval The interval, in seconds, between each sent location update.
*/
public InitParameters(String server, String username, String password, int duration, int interval, String customID, String e2ePass) {
public InitParameters(String server, String username, String password, int duration, int interval, float minDistance, String customID, String e2ePass) {
this.server = server;
this.connParams = null;
this.username = username == null || username.isEmpty() ? null : username;
this.password = password;
this.duration = duration;
this.interval = interval;
this.minDistance = minDistance;
this.customID = customID == null || customID.isEmpty() ? null : customID;
this.e2ePass = e2ePass == null || e2ePass.isEmpty() ? null : e2ePass;
}
@ -245,6 +259,14 @@ public class SessionInitiationPacket extends Packet {
return this.server;
}
public void setConnectionParameters(ConnectionParameters connParams) {
this.connParams = connParams;
}
ConnectionParameters getConnectionParameters() {
return this.connParams;
}
@Nullable
String getUsername() {
return this.username;
@ -262,6 +284,10 @@ public class SessionInitiationPacket extends Packet {
return this.interval;
}
float getMinimumDistance() {
return this.minDistance;
}
@Nullable
String getCustomID() {
return this.customID;

View file

@ -26,7 +26,7 @@ public abstract class StopSharingPacket extends Packet {
* @param session The session to delete.
*/
protected StopSharingPacket(Context ctx, Session session) {
super(ctx, session.getServerURL(), Constants.URL_PATH_STOP_SHARING);
super(ctx, session.getServerURL(), session.getConnectionParameters(), Constants.URL_PATH_STOP_SHARING);
setParameter(Constants.PACKET_PARAM_SESSION_ID, session.getID());
}
@ -37,7 +37,7 @@ public abstract class StopSharingPacket extends Packet {
* @param share The share to stop.
*/
protected StopSharingPacket(Context ctx, Share share) {
super(ctx, share.getSession().getServerURL(), Constants.URL_PATH_STOP_SHARING);
super(ctx, share.getSession().getServerURL(), share.getSession().getConnectionParameters(), Constants.URL_PATH_STOP_SHARING);
setParameter(Constants.PACKET_PARAM_SESSION_ID, share.getSession().getID());
setParameter(Constants.PACKET_PARAM_SHARE_ID, share.getID());
}

View file

@ -0,0 +1,24 @@
package info.varden.hauk.http.parameter;
/**
* An enum that identifies the currently active location provider on the device.
*/
public enum LocationProvider {
FINE(0),
COARSE(1);
private final int mode;
LocationProvider(int mode) {
this.mode = mode;
}
public int getMode() {
return this.mode;
}
@Override
public String toString() {
return "LocationProvider<mode=" + this.mode + ">";
}
}

View file

@ -0,0 +1,110 @@
package info.varden.hauk.http.proxy;
import android.os.AsyncTask;
import androidx.annotation.Nullable;
import java.net.InetSocketAddress;
import java.net.Proxy;
import info.varden.hauk.Constants;
import info.varden.hauk.http.FailureHandler;
import info.varden.hauk.system.preferences.PreferenceManager;
import info.varden.hauk.utils.Log;
/**
* Async task that checks whether or not a proxy address must be resolved, resolves it if necessary,
* and passes execution back to callbacks. This is necessary because proxy hostname resolution is
* network-dependent and therefore not permitted on the UI thread.
*
* @author Marius Lindvall
*/
public abstract class NameResolverTask extends AsyncTask<Void, Void, Proxy> implements FailureHandler {
/**
* Called if hostname resolution is required.
*
* @param hostname The hostname of the proxy.
*/
protected abstract void onResolutionStarted(String hostname);
/**
* Called if the hostname could not be resolved. This is a failure state.
*
* @param hostname The hostname that could not be resolved.
*/
protected abstract void onHostUnresolved(String hostname);
/**
* Called if proxy hostname resolution was required and was successful, or if resolution was
* skipped because no proxy is in use.
*
* @param proxy A proxy that can be used when connecting to the backend. May be null if no proxy
* should be used.
*/
protected abstract void onSuccess(@Nullable Proxy proxy);
private final Proxy.Type proxyType;
private final String proxyHost;
private final int proxyPort;
private boolean wasSuccessful = true;
protected NameResolverTask(PreferenceManager prefs) {
this.proxyType = prefs.get(Constants.PREF_PROXY_TYPE).resolve();
this.proxyHost = prefs.get(Constants.PREF_PROXY_HOST).trim();
this.proxyPort = prefs.get(Constants.PREF_PROXY_PORT);
}
/**
* Starts the proxy resolution process.
*/
public final void resolve() {
if (this.proxyType == Proxy.Type.DIRECT) {
// If explicitly using no proxy, forward the NO_PROXY upstream.
Log.i("Using direct connection to backend server"); //NON-NLS
onSuccess(Proxy.NO_PROXY);
} else if (this.proxyType == null) {
// If system default is set, forward null proxy upstream.
Log.i("Using system default proxy to backend server"); //NON-NLS
onSuccess(null);
} else {
// Otherwise, a proxy is in use and a hostname may need to be resolved.
Log.i("Using a proxy; resolving the hostname for %s", this.proxyHost); //NON-NLS
onResolutionStarted(this.proxyHost);
this.execute();
}
}
@Nullable
@Override
protected final Proxy doInBackground(Void... params) {
try {
Log.i("Resolving proxy hostname %s...", this.proxyHost); //NON-NLS
InetSocketAddress proxyAddr = new InetSocketAddress(this.proxyHost, this.proxyPort);
// Check if the hostname could be resolved.
if (proxyAddr.isUnresolved()) {
Log.e("Proxy hostname %s is unresolved", this.proxyHost); //NON-NLS
this.wasSuccessful = false;
onHostUnresolved(this.proxyHost);
return null;
} else {
Log.v("Proxy hostname resolution was successful!"); //NON-NLS
return new Proxy(this.proxyType, proxyAddr);
}
} catch (Exception ex) {
Log.e("Proxy setup failed for proxy %s:%s", ex, this.proxyHost, this.proxyPort); //NON-NLS
this.wasSuccessful = false;
onFailure(ex);
return null;
}
}
@Override
protected final void onPostExecute(Proxy result) {
if (this.wasSuccessful) onSuccess(result);
}
}

View file

@ -0,0 +1,26 @@
package info.varden.hauk.http.security;
import info.varden.hauk.system.preferences.IndexedEnum;
/**
* An enum representing the various types of proxies available on the system, and their ID when
* stored in preferences.
*
* @author Marius Lindvall
*/
public final class CertificateValidationPolicy extends IndexedEnum<CertificateValidationPolicy> {
private static final long serialVersionUID = -1906017485528370776L;
public static final CertificateValidationPolicy VALIDATE_ALL = new CertificateValidationPolicy(0);
public static final CertificateValidationPolicy DISABLE_TRUST_ANCHOR_ONION = new CertificateValidationPolicy(1);
public static final CertificateValidationPolicy DISABLE_ALL_ONION = new CertificateValidationPolicy(2);
private CertificateValidationPolicy(int index) {
super(index);
}
@Override
public String toString() {
return "CertificateValidationPolicy{" + super.toString() + "}";
}
}

View file

@ -0,0 +1,17 @@
package info.varden.hauk.http.security;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
/**
* Intentionally insecure hostname verifier used to ignore invalid hostnames if hostname validation
* is disabled for a domain in preferences. Should be used with caution.
*
* @author Marius Lindvall
*/
public final class InsecureHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
}

View file

@ -0,0 +1,40 @@
package info.varden.hauk.http.security;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import info.varden.hauk.utils.Log;
/**
* Intentionally insecure trust manager that accepts all trust anchors. Should be used with caution.
*/
public final class InsecureTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
Log.v("Client certificate presented for %s", s); //NON-NLS
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
Log.v("Server certificate presented for %s", x509Certificates[0]); //NON-NLS
}
@Override
public X509Certificate[] getAcceptedIssuers() {
Log.v("Got request for accepted issuers"); //NON-NLS
return null;
}
public static SSLSocketFactory getSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
SSLContext context = SSLContext.getInstance("TLS"); //NON-NLS
context.init(null, new TrustManager[] {new InsecureTrustManager()}, new SecureRandom());
return context.getSocketFactory();
}
}

View file

@ -71,7 +71,7 @@ public final class AutoResumptionPrompter implements ResumeHandler {
Log.i("Resuming shares..."); //NON-NLS
AutoResumptionPrompter.this.resumptionHandler.clearResumableSession();
for (Share share : this.shares) {
AutoResumptionPrompter.this.manager.shareLocation(share);
AutoResumptionPrompter.this.manager.shareLocation(share, SessionInitiationReason.USER_RESUMED);
}
}

View file

@ -17,6 +17,13 @@ public interface GNSSStatusUpdateListener {
*/
void onStarted();
/**
* Called if the accurate GNSS location listener stops working. This implies that the coarse
* location listener is now back in use and {@link #onCoarseLocationReceived()} may be called
* again.
*/
void onGNSSConnectionLost();
/**
* <p>Called on first reception of low-accuracy location data from the network. Should be
* available almost instantly if the user device has network-based or other non-GNSS location
@ -32,4 +39,14 @@ public interface GNSSStatusUpdateListener {
* adequate GNSS signal reception.
*/
void onAccurateLocationReceived();
/**
* Called if the backend server is unreachable.
*/
void onServerConnectionLost();
/**
* Called if the backend server was unreachable, but is now reachable again.
*/
void onServerConnectionRestored();
}

View file

@ -0,0 +1,48 @@
package info.varden.hauk.manager;
import android.content.Context;
import info.varden.hauk.caching.ResumableSessions;
import info.varden.hauk.caching.ResumeHandler;
import info.varden.hauk.struct.Session;
import info.varden.hauk.struct.Share;
import info.varden.hauk.utils.Log;
/**
* {@link ResumeHandler} implementation used by {@link SessionManager} to automatically resume
* sessions following a service relaunch. This can happen if the main activity is terminated, but
* the share itself keeps running in the background.
*
* @author Marius Lindvall
*/
public final class ServiceRelauncher implements ResumeHandler {
/**
* The session manager to call to resume the shares.
*/
private final SessionManager manager;
/**
* The manager's resumption handler. This is used to clear the resumption data before the shares
* are resumed by the session manager, as the session manager will re-flag the shares as
* resumable when it adds them to its internal share list.
*/
private final ResumableSessions resumptionHandler;
ServiceRelauncher(SessionManager manager, ResumableSessions resumptionHandler) {
this.manager = manager;
this.resumptionHandler = resumptionHandler;
}
@Override
public void onSharesFetched(Context ctx, Session session, Share[] shares) {
Log.i("Resuming %s share(s) automatically found for session %s", shares.length, session); //NON-NLS
// The shares provided by ResumableSessions do not have a session attached to them. Attach
// it to the shares so that they can be shown properly by the prompt and so that the updates
// have a backend to be broadcast to when the shares are resumed.
this.resumptionHandler.clearResumableSession();
for (Share share : shares) {
share.setSession(session);
this.manager.shareLocation(share, SessionInitiationReason.SERVICE_RELAUNCH);
}
}
}

View file

@ -0,0 +1,34 @@
package info.varden.hauk.manager;
import info.varden.hauk.struct.Session;
import info.varden.hauk.struct.Share;
/**
* Describes the reason a session was initiated.
*
* @author Marius Lindvall
*/
public enum SessionInitiationReason {
/**
* The user requested to start a new sharing session.
*/
USER_STARTED,
/**
* The user requested to resume a previous sharing session.
*/
USER_RESUMED,
/**
* The sharing session is automatically resumed as a result of a relaunch of the location
* sharing service.
*/
SERVICE_RELAUNCH,
/**
* The session was created because a share was added to it. This should never be received by
* {@link SessionListener#onSessionCreated(Session, Share, SessionInitiationReason)} under any
* normal circumstances.
*/
SHARE_ADDED
}

View file

@ -15,8 +15,9 @@ public interface SessionListener {
*
* @param session The session that was created.
* @param share The share that the session was created for.
* @param reason The reason the session was created.
*/
void onSessionCreated(Session session, Share share);
void onSessionCreated(Session session, Share share, SessionInitiationReason reason);
/**
* Called if the session could not be initiated due to missing location permissions.

View file

@ -63,6 +63,12 @@ public abstract class SessionManager {
*/
private final StopSharingCallback stopCallback;
/**
* Intent for the location pusher, so that it can be stopped if already running when launching
* the app.
*/
private static Intent pusher = null;
/**
* Android application context.
*/
@ -121,6 +127,7 @@ public abstract class SessionManager {
// Called when sharing ends. Clear the active session, and all collections of active
// shares present in this class, then propagate this stop message upstream to all
// session listeners.
Log.d("Performing stop task cleanup for task %s and stopping timed callback on handler %s", this, SessionManager.this.handler); //NON-NLS
SessionManager.this.activeSession = null;
SessionManager.this.handler.removeCallbacksAndMessages(null);
SessionManager.this.resumable.clearResumableSession();
@ -182,7 +189,21 @@ public abstract class SessionManager {
* any are found in storage.
*/
public final void resumeShares(ResumePrompt prompt) {
this.resumable.tryResumeShare(new AutoResumptionPrompter(this, this.resumable, prompt));
// Check if the location push service is already running. This will happen if the main UI
// activity is killed/stopped, but the app itself and the pushing service keeps running in
// the background. If this happens, the push service should be silently restarted to ensure
// it behaves properly with new instances of GNSSActiveHandler and StopSharingTask that will
// be created and attached when creating a new SessionManager in MainActivity. There is
// probably a cleaner way to do this.
if (pusher != null) {
Log.d("Pusher is non-null (%s), stopping and nulling it and calling service relauncher", pusher); //NON-NLS
this.ctx.stopService(pusher);
pusher = null;
this.resumable.tryResumeShare(new ServiceRelauncher(this, this.resumable));
} else {
Log.d("Pusher is null, calling resumption prompter"); //NON-NLS
this.resumable.tryResumeShare(new AutoResumptionPrompter(this, this.resumable, prompt));
}
}
/**
@ -194,7 +215,7 @@ public abstract class SessionManager {
* @throws LocationServicesDisabledException if location services are disabled.
* @throws LocationPermissionsNotGrantedException if location permissions have not been granted.
*/
private SessionInitiationPacket.ResponseHandler preSessionInitiation(final SessionInitiationResponseHandler upstreamCallback) throws LocationServicesDisabledException, LocationPermissionsNotGrantedException {
private SessionInitiationPacket.ResponseHandler preSessionInitiation(final SessionInitiationResponseHandler upstreamCallback, final SessionInitiationReason reason) throws LocationServicesDisabledException, LocationPermissionsNotGrantedException {
// Check for location permission and prompt the user if missing. This returns because the
// checking function creates async dialogs here - the user is prompted to press the button
// again instead.
@ -215,7 +236,7 @@ public abstract class SessionManager {
Log.i("Session was initiated for share %s; setting session resumable", share); //NON-NLS
// Proceed with the location share.
shareLocation(share);
shareLocation(share, reason);
upstreamCallback.onSuccess();
}
@ -250,7 +271,7 @@ public abstract class SessionManager {
* @throws LocationPermissionsNotGrantedException if location permissions have not been granted.
*/
public final void shareLocation(SessionInitiationPacket.InitParameters initParams, SessionInitiationResponseHandler upstreamCallback, AdoptabilityPreference allowAdoption) throws LocationPermissionsNotGrantedException, LocationServicesDisabledException {
SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback);
SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback, SessionInitiationReason.USER_STARTED);
// Create a handshake request and handle the response. The handshake transmits the duration
// and interval to the server and waits for the server to return a session ID to confirm
@ -269,7 +290,7 @@ public abstract class SessionManager {
* @throws LocationPermissionsNotGrantedException if location permissions have not been granted.
*/
public final void shareLocation(SessionInitiationPacket.InitParameters initParams, SessionInitiationResponseHandler upstreamCallback, String nickname) throws LocationPermissionsNotGrantedException, LocationServicesDisabledException {
SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback);
SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback, SessionInitiationReason.USER_STARTED);
// Create a handshake request and handle the response. The handshake transmits the duration
// and interval to the server and waits for the server to return a session ID to confirm
@ -289,7 +310,7 @@ public abstract class SessionManager {
* @throws LocationPermissionsNotGrantedException if location permissions have not been granted.
*/
public final void shareLocation(SessionInitiationPacket.InitParameters initParams, SessionInitiationResponseHandler upstreamCallback, String nickname, String groupPin) throws LocationPermissionsNotGrantedException, LocationServicesDisabledException {
SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback);
SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback, SessionInitiationReason.USER_STARTED);
// Create a handshake request and handle the response. The handshake transmits the duration
// and interval to the server and waits for the server to return a session ID to confirm
@ -304,10 +325,10 @@ public abstract class SessionManager {
*
* @param share The share to run against the server.
*/
public final void shareLocation(Share share) {
public final void shareLocation(Share share, SessionInitiationReason reason) {
// If we are not already sharing our location, initiate a new session.
if (this.activeSession == null) {
initiateSessionForExistingShare(share);
initiateSessionForExistingShare(share, reason);
}
Log.i("Attaching to share, share=%s", share); //NON-NLS
@ -349,7 +370,7 @@ public abstract class SessionManager {
*
* @param share The share whose session should be pushed to.
*/
private void initiateSessionForExistingShare(Share share) {
private void initiateSessionForExistingShare(Share share, SessionInitiationReason reason) {
this.activeSession = share.getSession();
this.resumable.setSessionResumable(this.activeSession);
@ -366,6 +387,7 @@ public abstract class SessionManager {
pusher.setAction(LocationPushService.ACTION_ID);
pusher.putExtra(Constants.EXTRA_SHARE, ReceiverDataRegistry.register(share));
pusher.putExtra(Constants.EXTRA_STOP_TASK, ReceiverDataRegistry.register(this.stopTask));
pusher.putExtra(Constants.EXTRA_HANDLER, ReceiverDataRegistry.register(this.handler));
pusher.putExtra(Constants.EXTRA_GNSS_ACTIVE_TASK, ReceiverDataRegistry.register(statusUpdateHandler));
// Android O and higher require the service to be started as a foreground service for it
@ -382,10 +404,15 @@ public abstract class SessionManager {
// these so that they can be canceled when the location share ends.
this.stopTask.updateTask(pusher);
// Required for session relaunches
Log.d("Setting static pusher %s (was %s)", pusher, SessionManager.pusher); //NON-NLS
//noinspection AssignmentToStaticFieldFromInstanceMethod
SessionManager.pusher = pusher;
// stopTask is scheduled for expiration, but it could also be called if the user
// manually stops the share, or if the app is destroyed.
long expireIn = share.getSession().getRemainingMillis();
Log.i("Scheduling session expiration in %s milliseconds", expireIn); //NON-NLS
Log.i("Scheduling session task %s for expiration in %s milliseconds on handler %s", this.stopTask, expireIn, this.handler); //NON-NLS
this.handler.postDelayed(this.stopTask, expireIn);
// Push the start event to upstream listeners.
@ -393,7 +420,7 @@ public abstract class SessionManager {
listener.onStarted();
}
for (SessionListener listener : this.upstreamSessionListeners) {
listener.onSessionCreated(share.getSession(), share);
listener.onSessionCreated(share.getSession(), share, reason);
}
} else {
Log.w("Location permission has not been granted; sharing will not commence"); //NON-NLS
@ -435,6 +462,13 @@ public abstract class SessionManager {
this.session = session;
}
@Override
public void onCoarseRebound() {
for (GNSSStatusUpdateListener listener : SessionManager.this.upstreamUpdateHandlers) {
listener.onGNSSConnectionLost();
}
}
@Override
public void onCoarseLocationReceived() {
for (GNSSStatusUpdateListener listener : SessionManager.this.upstreamUpdateHandlers) {
@ -449,6 +483,20 @@ public abstract class SessionManager {
}
}
@Override
public void onServerConnectionLost() {
for (GNSSStatusUpdateListener listener : SessionManager.this.upstreamUpdateHandlers) {
listener.onServerConnectionLost();
}
}
@Override
public void onServerConnectionRestored() {
for (GNSSStatusUpdateListener listener : SessionManager.this.upstreamUpdateHandlers) {
listener.onServerConnectionRestored();
}
}
@Override
public void onShareListReceived(String linkFormat, String[] shareIDs) {
List<String> currentShares = Arrays.asList(shareIDs);
@ -460,7 +508,7 @@ public abstract class SessionManager {
// that can be initiated by a remote user (through adoption).
Share newShare = new Share(this.session, String.format(linkFormat, shareID), shareID, ShareMode.JOIN_GROUP);
Log.i("Received unknown share %s from server", newShare); //NON-NLS
shareLocation(newShare);
shareLocation(newShare, SessionInitiationReason.SHARE_ADDED);
}
}
for (Iterator<Map.Entry<String, Share>> it = SessionManager.this.knownShares.entrySet().iterator(); it.hasNext();) {

View file

@ -59,7 +59,7 @@ public abstract class StopSharingTask implements Runnable {
* @param pusher A location handler that should be unregistered when sharing is stopped.
*/
final void updateTask(Intent pusher) {
Log.i("Setting new update task"); //NON-NLS
Log.i("Setting new update task %s in task %s", pusher, this); //NON-NLS
this.pusher = pusher;
this.canExecute = true;
}
@ -93,6 +93,7 @@ public abstract class StopSharingTask implements Runnable {
*/
@Override
public final void run() {
Log.d("Stop sharing task %s was called", this); //NON-NLS
if (!this.canExecute) return;
Log.i("Executing share stop task"); //NON-NLS
this.canExecute = false;

View file

@ -60,6 +60,22 @@ public abstract class HaukNotification {
*/
protected abstract void build(NotificationCompat.Builder builder) throws Exception;
/**
* Displays the notification, or updates if it is already displayed.
*/
final void push() {
NotificationManager manager = (NotificationManager) this.ctx.getSystemService(Context.NOTIFICATION_SERVICE);
if (manager != null) {
try {
manager.notify(this.id, create());
} catch (Exception e) {
Log.e("Error while pushing notification", e); //NON-NLS
}
} else {
Log.e("Notification manager is null"); //NON-NLS
}
}
/**
* Creates a notification instance that can be displayed using NotificationManager.
*

View file

@ -56,6 +56,6 @@ final class Receiver<T> {
// the intent.
intent.putExtra(Constants.EXTRA_BROADCAST_RECEIVER_REGISTRY_INDEX, ReceiverDataRegistry.register(this.data));
return PendingIntent.getBroadcast(this.ctx, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
return PendingIntent.getBroadcast(this.ctx, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
}

View file

@ -32,6 +32,6 @@ final class ReopenIntent {
PendingIntent toPending() {
Intent intent = new Intent(this.ctx, this.activity);
intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
return PendingIntent.getActivity(this.ctx, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
return PendingIntent.getActivity(this.ctx, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
}

View file

@ -6,6 +6,7 @@ import androidx.core.app.NotificationCompat;
import info.varden.hauk.R;
import info.varden.hauk.manager.StopSharingTask;
import info.varden.hauk.service.GNSSActiveHandler;
import info.varden.hauk.struct.Share;
import info.varden.hauk.ui.MainActivity;
import info.varden.hauk.utils.Log;
@ -15,7 +16,7 @@ import info.varden.hauk.utils.Log;
*
* @author Marius Lindvall
*/
public final class SharingNotification extends HaukNotification {
public final class SharingNotification extends HaukNotification implements GNSSActiveHandler {
/**
* The share that this notification represents.
*/
@ -27,6 +28,16 @@ public final class SharingNotification extends HaukNotification {
*/
private final StopSharingTask stopSharingTask;
/**
* A string resource representing the title currently displayed in the notification.
*/
private int notifyTitle;
/**
* The old notification title, if switching to or from the "backend connection lost" title.
*/
private int lastTitle;
/**
* Creates a persistent notification.
*
@ -38,12 +49,14 @@ public final class SharingNotification extends HaukNotification {
super(ctx);
this.share = share;
this.stopSharingTask = stopSharingTask;
this.notifyTitle = R.string.label_status_wait;
this.lastTitle = R.string.label_status_wait;
}
@Override
public void build(NotificationCompat.Builder builder) throws Exception {
Log.v("Building sharing notification"); //NON-NLS
builder.setContentTitle(getContext().getString(R.string.notify_title));
builder.setContentTitle(getContext().getString(this.notifyTitle));
builder.setContentText(String.format(getContext().getString(R.string.notify_body), this.share.getSession().getServerURL()));
builder.setSmallIcon(R.drawable.ic_notify);
builder.setPriority(NotificationCompat.PRIORITY_DEFAULT);
@ -55,4 +68,41 @@ public final class SharingNotification extends HaukNotification {
builder.setOngoing(true);
}
@Override
public void onCoarseRebound() {
this.notifyTitle = R.string.label_status_lost_gnss;
this.lastTitle = this.notifyTitle;
push();
}
@Override
public void onCoarseLocationReceived() {
this.notifyTitle = R.string.label_status_coarse;
this.lastTitle = this.notifyTitle;
push();
}
@Override
public void onAccurateLocationReceived() {
this.notifyTitle = R.string.label_status_ok;
this.lastTitle = this.notifyTitle;
push();
}
@Override
public void onServerConnectionLost() {
this.notifyTitle = R.string.label_status_disconnected;
push();
}
@Override
public void onServerConnectionRestored() {
this.notifyTitle = this.lastTitle;
push();
}
@Override
public void onShareListReceived(String linkFormat, String[] shareIDs) {
}
}

View file

@ -6,6 +6,11 @@ package info.varden.hauk.service;
* @author Marius Lindvall
*/
public interface GNSSActiveHandler {
/**
* Called when the fine location provider times out and the coarse location provider is rebound.
*/
void onCoarseRebound();
/**
* Called when the initial low-accuracy GNSS fix has been obtained.
*/
@ -16,6 +21,16 @@ public interface GNSSActiveHandler {
*/
void onAccurateLocationReceived();
/**
* Called if the backend server is unreachable.
*/
void onServerConnectionLost();
/**
* Called if the backend server was unreachable, but is now reachable again.
*/
void onServerConnectionRestored();
/**
* Called when a list of shares the client is contributing to has been received from the server.
*

View file

@ -1,6 +1,7 @@
package info.varden.hauk.service;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import info.varden.hauk.utils.Log;
@ -13,7 +14,7 @@ import info.varden.hauk.utils.Log;
*/
abstract class LocationListenerBase implements LocationListener {
@Override
public final void onStatusChanged(String provider, int status, Bundle extras) {
public final void onStatusChanged(String provider, int status, Bundle bundle) {
Log.v("Location status changed for provider %s, status=%s", provider, status); //NON-NLS
}
@ -26,4 +27,13 @@ abstract class LocationListenerBase implements LocationListener {
public final void onProviderDisabled(String provider) {
Log.w("Location provider %s was disabled", provider); //NON-NLS
}
/**
* Request location updates from the given location manager.
*
* @param manager The location manager to request location updates from.
* @return true if successful, false otherwise.
* @throws SecurityException if location permission has not been granted.
*/
abstract boolean request(LocationManager manager) throws SecurityException;
}

View file

@ -6,20 +6,24 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Handler;
import android.os.IBinder;
import androidx.annotation.Nullable;
import info.varden.hauk.Constants;
import info.varden.hauk.http.LocationUpdatePacket;
import info.varden.hauk.http.ServerException;
import info.varden.hauk.http.parameter.LocationProvider;
import info.varden.hauk.manager.StopSharingTask;
import info.varden.hauk.notify.HaukNotification;
import info.varden.hauk.notify.SharingNotification;
import info.varden.hauk.struct.Share;
import info.varden.hauk.struct.Version;
import info.varden.hauk.system.preferences.PreferenceManager;
import info.varden.hauk.utils.Log;
import info.varden.hauk.utils.ReceiverDataRegistry;
import info.varden.hauk.utils.TimeUtils;
/**
* This class is a location listener that POSTs all location updates to Hauk as it receives them. It
@ -68,12 +72,25 @@ public final class LocationPushService extends Service {
/**
* The service's location listener for fine (GNSS, high-accuracy) location updates.
*/
private LocationListener listenFine;
private FineLocationListener listenFine;
/**
* The service's location listener for coarse (network, low-accuracy) location updates.
*/
private LocationListener listenCoarse;
private CoarseLocationListener listenCoarse;
/**
* The handler that has scheduled the stop task. This is needed so that the callback can be
* cancelled if the service is relaunched because of a {@link info.varden.hauk.ui.MainActivity}
* reset/recreation.
*/
private Handler handler;
/**
* Whether or not the last update packet was sent successfully, i.e. whether there is a
* connection to the backend server.
*/
private boolean connected = true;
@Override
public void onCreate() {
@ -83,12 +100,15 @@ public final class LocationPushService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("Location push service was started, flags=%s, startId=%s", flags, startId); //NON-NLS
Log.i("Location push service %s was started, flags=%s, startId=%s", this, flags, startId); //NON-NLS
// A task that should be run when sharing ends, either automatically or by user request.
StopSharingTask stopTask = (StopSharingTask) ReceiverDataRegistry.retrieve(intent.getIntExtra(Constants.EXTRA_STOP_TASK, -1));
this.share = (Share) ReceiverDataRegistry.retrieve(intent.getIntExtra(Constants.EXTRA_SHARE, -1));
this.gnssActiveTask = (GNSSActiveHandler) ReceiverDataRegistry.retrieve(intent.getIntExtra(Constants.EXTRA_GNSS_ACTIVE_TASK, -1));
GNSSActiveHandler parentHandler = (GNSSActiveHandler) ReceiverDataRegistry.retrieve(intent.getIntExtra(Constants.EXTRA_GNSS_ACTIVE_TASK, -1));
this.handler = (Handler) ReceiverDataRegistry.retrieve(intent.getIntExtra(Constants.EXTRA_HANDLER, -1));
Log.d("Pusher %s was given extras stopTask=%s, share=%s, parentHandler=%s, handler=%s", this, stopTask, this.share, parentHandler, this.handler); //NON-NLS
try {
// Even though we previously requested location permission, we still have to check for
@ -101,45 +121,18 @@ public final class LocationPushService extends Service {
// buttons that let the user interact with Hauk while in the background, but the
// real reason we need a notification is so that Android does not kill our app while
// it is in the background. Having an active notification stops this from happening.
HaukNotification notify = new SharingNotification(this, this.share, stopTask);
SharingNotification notify = new SharingNotification(this, this.share, stopTask);
startForeground(notify.getID(), notify.create());
this.listenCoarse = new LocationListenerBase() {
@Override
public void onLocationChanged(Location location) {
if (!LocationPushService.this.hasRunCoarseTask) {
// Notify the main activity that coarse GPS data is now being received,
// such that the UI can be updated.
LocationPushService.this.hasRunCoarseTask = true;
LocationPushService.this.gnssActiveTask.onCoarseLocationReceived();
}
Log.v("Location was received on coarse location provider"); //NON-NLS
LocationPushService.this.onLocationChanged(location);
}
};
// Send status changes both to the parent handler and the notification.
this.gnssActiveTask = new MultiTargetGNSSHandlerProxy(parentHandler, notify);
this.listenFine = new LocationListenerBase() {
@Override
public void onLocationChanged(Location location) {
if (LocationPushService.this.listenCoarse != null) {
// Unregister the coarse location listener, since we are now receiving
// accurate location data.
Log.i("Accurate location found; removing updates from coarse location provider"); //NON-NLS
LocationPushService.this.locMan.removeUpdates(LocationPushService.this.listenCoarse);
LocationPushService.this.listenCoarse = null;
}
if (!LocationPushService.this.hasRunAccurateTask) {
// Notify the main activity that accurate GPS data is now being
// received, such that the UI can be updated.
LocationPushService.this.hasRunAccurateTask = true;
LocationPushService.this.gnssActiveTask.onAccurateLocationReceived();
}
Log.v("Location was received on fine location provider"); //NON-NLS
LocationPushService.this.onLocationChanged(location);
}
};
// Create and bind location listeners.
this.listenCoarse = new CoarseLocationListener();
this.listenFine = new FineLocationListener();
if (!this.listenCoarse.request(this.locMan)) this.listenCoarse = null;
if (!this.listenFine.request(this.locMan)) this.listenFine = null;
attachToLocationServices();
} else {
Log.e("Location permission that was granted earlier has been rejected - sharing aborted"); //NON-NLS
}
@ -152,29 +145,21 @@ public final class LocationPushService extends Service {
@Override
public void onDestroy() {
if (this.listenCoarse != null) {
Log.i("Service destroyed; removing updates from coarse location provider"); //NON-NLS
Log.i("Service %s destroyed; removing updates from coarse location provider", this); //NON-NLS
this.locMan.removeUpdates(this.listenCoarse);
}
Log.i("Service destroyed; removing updates from fine location provider"); //NON-NLS
Log.i("Service %s destroyed; removing updates from fine location provider", this); //NON-NLS
this.listenFine.onStopped();
this.locMan.removeUpdates(this.listenFine);
stopForeground(true);
super.onDestroy();
}
/**
* Attaches the listeners to the location manager to request updates.
*
* @throws SecurityException If location permission is missing.
*/
private void attachToLocationServices() throws SecurityException {
Log.i("Requesting location updates from device location services"); //NON-NLS
try {
this.locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, this.share.getSession().getIntervalMillis(), 0.0F, this.listenCoarse);
} catch (IllegalArgumentException ex) {
Log.w("Coarse location provider does not exist!", ex); //NON-NLS
this.listenCoarse = null;
}
this.locMan.requestLocationUpdates(LocationManager.GPS_PROVIDER, this.share.getSession().getIntervalMillis(), 0.0F, this.listenFine);
Log.i("Removing callbacks from handler %s", this.handler); //NON-NLS
this.handler.removeCallbacksAndMessages(null);
this.gnssActiveTask = new MultiTargetGNSSHandlerProxy();
Log.i("Stopping foreground service"); //NON-NLS
stopForeground(true);
super.onDestroy();
}
/**
@ -183,21 +168,9 @@ public final class LocationPushService extends Service {
*
* @param location The location received from the device's location services.
*/
private void onLocationChanged(Location location) {
private void onLocationChanged(Location location, LocationProvider accuracy) {
Log.v("Sending location update packet"); //NON-NLS
new LocationUpdatePacket(this, this.share.getSession(), location) {
@Override
public void onShareListReceived(String linkFormat, String[] shares) {
Log.v("Received list of shares from server"); //NON-NLS
LocationPushService.this.gnssActiveTask.onShareListReceived(linkFormat, shares);
}
@Override
protected void onFailure(Exception ex) {
// Errors can be due to intermittent connectivity. Ignore them.
Log.w("Failed to push location update to server", ex); //NON-NLS
}
}.send();
new LocationUpdatePacketImpl(location, accuracy).send();
}
@Nullable
@ -205,4 +178,159 @@ public final class LocationPushService extends Service {
public IBinder onBind(Intent intent) {
return null;
}
/**
* Coarse location provider implementation (network-based location).
*/
private final class CoarseLocationListener extends LocationListenerBase {
@Override
public void onLocationChanged(Location location) {
if (!LocationPushService.this.hasRunCoarseTask) {
// Notify the main activity that coarse GPS data is now being received,
// such that the UI can be updated.
LocationPushService.this.hasRunCoarseTask = true;
LocationPushService.this.gnssActiveTask.onCoarseLocationReceived();
}
Log.v("Location was received on coarse location provider"); //NON-NLS
LocationPushService.this.onLocationChanged(location, LocationProvider.COARSE);
}
@Override
boolean request(LocationManager manager) throws SecurityException {
Log.i("Requesting location updates from device location services"); //NON-NLS
try {
manager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
LocationPushService.this.share.getSession().getIntervalMillis(),
LocationPushService.this.share.getSession().getMinimumDistance(),
this
);
return true;
} catch (IllegalArgumentException ex) {
Log.w("Coarse location provider does not exist!", ex); //NON-NLS
return false;
}
}
}
/**
* Fine location provider implementation (GNSS-based location).
*/
private final class FineLocationListener extends LocationListenerBase {
private final Handler noGnssTimer;
private final PreferenceManager prefs;
private Location locationOfLastUpdate;
private float minDistance;
private FineLocationListener() {
this.noGnssTimer = new Handler();
this.prefs = new PreferenceManager(LocationPushService.this);
this.locationOfLastUpdate = null;
this.minDistance = LocationPushService.this.share.getSession().getMinimumDistance();
}
@Override
public void onLocationChanged(Location location) {
if (LocationPushService.this.listenCoarse != null) {
// Unregister the coarse location listener, since we are now receiving
// accurate location data.
Log.i("Accurate location found; removing updates from coarse location provider"); //NON-NLS
LocationPushService.this.locMan.removeUpdates(LocationPushService.this.listenCoarse);
LocationPushService.this.listenCoarse = null;
}
if (!LocationPushService.this.hasRunAccurateTask) {
// Notify the main activity that accurate GPS data is now being
// received, such that the UI can be updated.
LocationPushService.this.hasRunAccurateTask = true;
LocationPushService.this.gnssActiveTask.onAccurateLocationReceived();
}
Log.v("Location was received on fine location provider"); //NON-NLS
// Set a timeout for the location updates to detect if the provider stops working. If
// that happens, fall back to the coarse location provider.
this.noGnssTimer.removeCallbacksAndMessages(null);
this.noGnssTimer.postDelayed(new CoarseLocationFallbackTask(), LocationPushService.this.share.getSession().getIntervalMillis() + this.prefs.get(Constants.PREF_NO_GNSS_FALLBACK) * TimeUtils.MILLIS_PER_SECOND);
// Only update the location if it is more than the minimum distance specified in
// settings. Done manually rather than delegating to
// LocationManager.requestLocationUpdates; see issue #124
float distance = this.locationOfLastUpdate == null ? -1 : this.locationOfLastUpdate.distanceTo(location);
if (this.locationOfLastUpdate == null || distance >= this.minDistance) {
Log.v("Received distance %s, more than minimum distance %s", distance, this.minDistance); //NON-NLS
this.locationOfLastUpdate = location;
LocationPushService.this.onLocationChanged(location, LocationProvider.FINE);
} else {
Log.v("Received distance %s, less than minimum distance %s", distance, this.minDistance); //NON-NLS
}
}
/**
* Should be called when the session is stopped and updates removed from this listener. This
* prevents the timeout from activating after the session has been stopped.
*/
private void onStopped() {
this.noGnssTimer.removeCallbacksAndMessages(false);
}
@Override
boolean request(LocationManager manager) throws SecurityException {
manager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
LocationPushService.this.share.getSession().getIntervalMillis(),
0.0F, // See https://github.com/bilde2910/Hauk/issues/124
this
);
return true;
}
private final class CoarseLocationFallbackTask implements Runnable {
@Override
public void run() {
// No location updates have been received for the timeout period. Rebind the coarse
// location listener while we wait for the fine listener to become functional again.
Log.w("Location fix lost. Rebinding coarse location provider."); //NON-NLS
LocationPushService.this.gnssActiveTask.onCoarseRebound();
LocationPushService.this.hasRunCoarseTask = false;
LocationPushService.this.hasRunAccurateTask = false;
LocationPushService.this.listenCoarse = new CoarseLocationListener();
if (!LocationPushService.this.listenCoarse.request(LocationPushService.this.locMan)) {
LocationPushService.this.listenCoarse = null;
}
}
}
}
private final class LocationUpdatePacketImpl extends LocationUpdatePacket {
private LocationUpdatePacketImpl(Location location, LocationProvider accuracy) {
super(LocationPushService.this, LocationPushService.this.share.getSession(), location, accuracy);
}
@Override
public void onShareListReceived(String linkFormat, String[] shares) {
Log.v("Received list of shares from server"); //NON-NLS
LocationPushService.this.gnssActiveTask.onShareListReceived(linkFormat, shares);
}
@Override
protected void onSuccess(String[] data, Version backendVersion) throws ServerException {
// Check if connection was lost previously, and notify upstream if that's the case.
if (!LocationPushService.this.connected) {
LocationPushService.this.connected = true;
Log.i("Connection to the backend was restored."); //NON-NLS
LocationPushService.this.gnssActiveTask.onServerConnectionRestored();
}
super.onSuccess(data, backendVersion);
}
@Override
protected void onFailure(Exception ex) {
Log.w("Failed to push location update to server", ex); //NON-NLS
// Notify upstream about connectivity loss.
if (LocationPushService.this.connected) {
LocationPushService.this.connected = false;
Log.i("Connection to the backend was lost."); //NON-NLS
LocationPushService.this.gnssActiveTask.onServerConnectionLost();
}
}
}
}

View file

@ -0,0 +1,44 @@
package info.varden.hauk.service;
/**
* Proxy class that forwards GNSS activity events to multiple upstream {@link GNSSActiveHandler}s.
*
* @author Marius Lindvall
*/
final class MultiTargetGNSSHandlerProxy implements GNSSActiveHandler {
private final GNSSActiveHandler[] upstream;
MultiTargetGNSSHandlerProxy(GNSSActiveHandler... upstream) {
this.upstream = upstream.clone();
}
@Override
public void onCoarseRebound() {
for (GNSSActiveHandler up : this.upstream) up.onCoarseRebound();
}
@Override
public void onCoarseLocationReceived() {
for (GNSSActiveHandler up : this.upstream) up.onCoarseLocationReceived();
}
@Override
public void onAccurateLocationReceived() {
for (GNSSActiveHandler up : this.upstream) up.onAccurateLocationReceived();
}
@Override
public void onServerConnectionLost() {
for (GNSSActiveHandler up : this.upstream) up.onServerConnectionLost();
}
@Override
public void onServerConnectionRestored() {
for (GNSSActiveHandler up : this.upstream) up.onServerConnectionRestored();
}
@Override
public void onShareListReceived(String linkFormat, String[] shareIDs) {
for (GNSSActiveHandler up : this.upstream) up.onShareListReceived(linkFormat, shareIDs);
}
}

View file

@ -28,6 +28,7 @@ public final class KeyDerivable implements Serializable {
/**
* End-to-end password to encrypt outgoing data with.
*/
@SuppressWarnings("FieldNotUsedInToString")
private final String password;
/**
@ -62,7 +63,7 @@ public final class KeyDerivable implements Serializable {
@Override
public String toString() {
return "KeyDerivable{password=" + this.password
return "KeyDerivable{password=<hidden>"
+ ",salt=0x" + StringUtils.bytesToHex(this.salt)
+ "}";
}

View file

@ -8,6 +8,7 @@ import java.util.Date;
import java.util.Locale;
import info.varden.hauk.Constants;
import info.varden.hauk.http.ConnectionParameters;
import info.varden.hauk.utils.TimeUtils;
/**
@ -16,13 +17,18 @@ import info.varden.hauk.utils.TimeUtils;
* @author Marius Lindvall
*/
public final class Session implements Serializable {
private static final long serialVersionUID = 8424014563201300999L;
private static final long serialVersionUID = 315568255735934584L;
/**
* The Hauk backend server base URL.
*/
private final String serverURL;
/**
* Connection parameters for the backend connection.
*/
private final ConnectionParameters connParams;
/**
* The version the backend is running.
*/
@ -45,24 +51,32 @@ public final class Session implements Serializable {
*/
private final int interval;
/**
* The minimum distance between each location update, in meters.
*/
private final float minDistance;
/**
* End-to-end encryption parameters.
*/
@Nullable
private final KeyDerivable e2eParams;
public Session(String serverURL, Version backendVersion, String sessionID, long expiry, int interval, @Nullable KeyDerivable e2eParams) {
public Session(String serverURL, ConnectionParameters connParams, Version backendVersion, String sessionID, long expiry, int interval, float minDistance, @Nullable KeyDerivable e2eParams) {
this.serverURL = serverURL;
this.backendVersion = backendVersion;
this.sessionID = sessionID;
this.expiry = expiry;
this.interval = interval;
this.minDistance = minDistance;
this.e2eParams = e2eParams;
this.connParams = connParams;
}
@Override
public String toString() {
return "Session{serverURL=" + this.serverURL
+ ",connParams=" + this.connParams
+ ",backendVersion=" + this.backendVersion
+ ",sessionID=" + this.sessionID
+ ",expiry=" + this.expiry
@ -75,6 +89,10 @@ public final class Session implements Serializable {
return this.serverURL;
}
public ConnectionParameters getConnectionParameters() {
return this.connParams;
}
public Version getBackendVersion() {
return this.backendVersion;
}
@ -100,7 +118,7 @@ public final class Session implements Serializable {
* Returns the expiration time of this session as a human-readable string.
*/
public String getExpiryString() {
SimpleDateFormat formatter = new SimpleDateFormat(Constants.DATE_FORMAT, Locale.getDefault());
SimpleDateFormat formatter = new SimpleDateFormat(Constants.DATE_FORMAT_UI, Locale.getDefault());
return formatter.format(getExpiryDate());
}
@ -140,6 +158,13 @@ public final class Session implements Serializable {
return getIntervalSeconds() * TimeUtils.MILLIS_PER_SECOND;
}
/**
* Returns the minimum distance between each location update, in meters.
*/
public float getMinimumDistance() {
return this.minDistance;
}
@Nullable
public KeyDerivable getDerivableE2EKey() {
return this.e2eParams;

View file

@ -0,0 +1,44 @@
package info.varden.hauk.system.launcher;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.view.View;
import androidx.preference.Preference;
/**
* Listener that opens a URI in the browser on click.
*
* @author Marius Lindvall
*/
public final class OpenLinkListener implements Preference.OnPreferenceClickListener, View.OnClickListener {
private final Context ctx;
private final Uri uri;
/**
* Creates the click listener.
*
* @param ctx Android application context.
* @param uriResource A string resource representing the link to open.
*/
public OpenLinkListener(Context ctx, int uriResource) {
this(ctx, Uri.parse(ctx.getString(uriResource)));
}
private OpenLinkListener(Context ctx, Uri uri) {
this.ctx = ctx;
this.uri = uri;
}
@Override
public boolean onPreferenceClick(Preference preference) {
this.ctx.startActivity(new Intent(Intent.ACTION_VIEW, this.uri));
return false;
}
@Override
public void onClick(View view) {
this.ctx.startActivity(new Intent(Intent.ACTION_VIEW, this.uri));
}
}

View file

@ -42,7 +42,7 @@ public enum Device {
XIAOMI(3,
R.string.manufacturer_xiaomi,
Build.HOST,
Pattern.compile("-miui-"),
Pattern.compile("(-miui-)|(xiaomi)"),
new ComponentLauncher(
"com.miui.powerkeeper",
".ui.HiddenAppsContainerManagementActivity"

View file

@ -48,7 +48,7 @@ public final class DeviceChecker {
dialogSvc.showDialog(
R.string.battery_savings_title,
String.format(this.ctx.getString(R.string.battery_savings_body), this.ctx.getString(device.getManufacturerStringResource())),
Buttons.SETTINGS_DISMISS,
Buttons.Two.SETTINGS_DISMISS,
new WarningDialog(this.ctx, prefs, device)
);
} else {

View file

@ -0,0 +1,64 @@
package info.varden.hauk.system.preferences;
import java.io.Serializable;
import java.lang.reflect.Field;
/**
* A base class for enum-like values that can be stored in preferences. A class can extend this
* class to allow it to be stored as an integer in preferences and be retrieved directly using
* {@link PreferenceManager#get(Preference)}.
*
* @param <T> The type that extends this class.
*/
public abstract class IndexedEnum<T extends IndexedEnum<T>> implements Serializable {
private static final long serialVersionUID = -1867612075461184507L;
/**
* An internal ID for this enum member that represents the value it is stored as in preferences.
*/
private final int index;
protected IndexedEnum(int index) {
this.index = index;
}
@SuppressWarnings("MethodOverloadsMethodOfSuperclass")
public final boolean equals(IndexedEnum<T> other) {
return this.getIndex() == other.getIndex();
}
public final int getIndex() {
return this.index;
}
/**
* Returns an enum member by its index.
*
* @param index The index of the enum member.
* @return An enum member.
*/
@SuppressWarnings("unchecked")
final T fromIndex(int index) throws IllegalAccessException, InstantiationException {
return (T) fromIndex(getClass(), index);
}
@SuppressWarnings("unchecked")
public static <T extends IndexedEnum<T>> T fromIndex(Class<T> type, int index) throws IllegalAccessException, InstantiationException {
Field[] fields = type.getFields();
for (Field field : fields) {
if (field.getType().isAssignableFrom(type)) {
IndexedEnum<T> instance = (IndexedEnum<T>) field.get(null);
if (instance != null && instance.getIndex() == index) {
return (T) instance;
}
}
}
throw new InstantiationException("Failed to find a member with this index");
}
@SuppressWarnings("DesignForExtension")
@Override
public String toString() {
return "index=" + this.index;
}
}

View file

@ -0,0 +1,15 @@
package info.varden.hauk.system.preferences;
/**
* An exception that is thrown when attempting to read a setting that is not compatible with the
* given type on the settings screen.
*
* @author Marius Lindvall
*/
class InvalidPreferenceTypeException extends RuntimeException {
private static final long serialVersionUID = -1966346602996946755L;
InvalidPreferenceTypeException(Object value, Class<?> target) {
super(String.format("Cannot direct-cast %s to %s", value.toString(), target.getName())); //NON-NLS
}
}

View file

@ -1,4 +1,4 @@
package info.varden.hauk.utils;
package info.varden.hauk.system.preferences;
import android.content.SharedPreferences;
@ -6,6 +6,8 @@ import info.varden.hauk.system.security.EncryptedData;
import info.varden.hauk.system.security.EncryptionException;
import info.varden.hauk.system.security.KeyStoreAlias;
import info.varden.hauk.system.security.KeyStoreHelper;
import info.varden.hauk.utils.Log;
import info.varden.hauk.utils.StringSerializer;
/**
* Represents a preference key to default value mapping pair for use with storing preferences for
@ -16,6 +18,34 @@ import info.varden.hauk.system.security.KeyStoreHelper;
*/
public abstract class Preference<T> {
private final java.lang.String key;
private final T def;
private final Class<?> type;
private Preference(java.lang.String key, T def, Class<?> type) {
this.key = key;
this.def = def;
this.type = type;
}
/**
* Returns the key of this preference in the {@link SharedPreferences} instance.
*/
public final java.lang.String getKey() {
return this.key;
}
public final T getDefault() {
return this.def;
}
/**
* Returns the type of data this preference stores.
*/
public final Class<?> getPreferenceType() {
return this.type;
}
/**
* Gets the value of the preference from the given preference object.
*
@ -36,14 +66,24 @@ public abstract class Preference<T> {
*
* @param prefs The shared preferences to check for preference existence in.
*/
abstract boolean has(SharedPreferences prefs);
public final boolean has(SharedPreferences prefs) {
return prefs.contains(this.key);
}
/**
* Clears the preference from the given preference object.
*
* @param prefs The shared preferences to clear the value from.
*/
abstract void clear(SharedPreferences.Editor prefs);
public final void clear(SharedPreferences.Editor prefs) {
prefs.remove(this.key);
}
/**
* Returns whether or not this preference is expected to contain sensitive information that
* should not be logged.
*/
abstract boolean isSensitive();
/**
* Represents a String-value preference.
@ -53,6 +93,7 @@ public abstract class Preference<T> {
private final java.lang.String def;
public String(java.lang.String key, java.lang.String def) {
super(key, def, java.lang.String.class);
this.key = key;
this.def = def;
}
@ -68,13 +109,8 @@ public abstract class Preference<T> {
}
@Override
boolean has(SharedPreferences prefs) {
return prefs.contains(this.key);
}
@Override
void clear(SharedPreferences.Editor prefs) {
prefs.remove(this.key);
boolean isSensitive() {
return false;
}
@SuppressWarnings("DuplicateStringLiteralInspection")
@ -92,6 +128,7 @@ public abstract class Preference<T> {
private final java.lang.String def;
public EncryptedString(java.lang.String key, java.lang.String def) {
super(key, def, java.lang.String.class);
this.key = key;
this.def = def;
}
@ -119,13 +156,8 @@ public abstract class Preference<T> {
}
@Override
boolean has(SharedPreferences prefs) {
return prefs.contains(this.key);
}
@Override
void clear(SharedPreferences.Editor prefs) {
prefs.remove(this.key);
boolean isSensitive() {
return true;
}
@SuppressWarnings("DuplicateStringLiteralInspection")
@ -143,6 +175,7 @@ public abstract class Preference<T> {
private final int def;
public Integer(java.lang.String key, int def) {
super(key, def, java.lang.Integer.class);
this.key = key;
this.def = def;
}
@ -158,13 +191,8 @@ public abstract class Preference<T> {
}
@Override
boolean has(SharedPreferences prefs) {
return prefs.contains(this.key);
}
@Override
void clear(SharedPreferences.Editor prefs) {
prefs.remove(this.key);
boolean isSensitive() {
return false;
}
@SuppressWarnings("DuplicateStringLiteralInspection")
@ -174,6 +202,77 @@ public abstract class Preference<T> {
}
}
public static final class Enum<U extends IndexedEnum<U>> extends Preference<U> {
private final java.lang.String key;
private final U def;
public Enum(java.lang.String key, U def) {
super(key, def, IndexedEnum.class);
this.key = key;
this.def = def;
}
@Override
U get(SharedPreferences prefs) {
try {
return this.def.fromIndex(prefs.getInt(this.key, this.def.getIndex()));
} catch (Exception e) {
return this.def;
}
}
@Override
void set(SharedPreferences.Editor prefs, IndexedEnum value) {
prefs.putInt(this.key, value.getIndex());
}
@Override
boolean isSensitive() {
return false;
}
@SuppressWarnings("DuplicateStringLiteralInspection")
@Override
public java.lang.String toString() {
return "Preference<Enum>{key=" + this.key + ",default=" + this.def + "}";
}
}
/**
* Represents a Float-value preference.
*/
public static final class Float extends Preference<java.lang.Float> {
private final java.lang.String key;
private final float def;
public Float(java.lang.String key, float def) {
super(key, def, java.lang.Float.class);
this.key = key;
this.def = def;
}
@Override
java.lang.Float get(SharedPreferences prefs) {
return prefs.getFloat(this.key, this.def);
}
@Override
void set(SharedPreferences.Editor prefs, java.lang.Float value) {
prefs.putFloat(this.key, value);
}
@Override
boolean isSensitive() {
return false;
}
@SuppressWarnings("DuplicateStringLiteralInspection")
@Override
public java.lang.String toString() {
return "Preference<Float>{key=" + this.key + ",default=" + this.def + "}";
}
}
/**
* Represents a Boolean-value preference.
*/
@ -183,6 +282,7 @@ public abstract class Preference<T> {
@SuppressWarnings("BooleanParameter")
public Boolean(java.lang.String key, boolean def) {
super(key, def, java.lang.Boolean.class);
this.key = key;
this.def = def;
}
@ -198,13 +298,8 @@ public abstract class Preference<T> {
}
@Override
boolean has(SharedPreferences prefs) {
return prefs.contains(this.key);
}
@Override
void clear(SharedPreferences.Editor prefs) {
prefs.remove(this.key);
boolean isSensitive() {
return false;
}
@SuppressWarnings("DuplicateStringLiteralInspection")

View file

@ -0,0 +1,15 @@
package info.varden.hauk.system.preferences;
/**
* An exception that is thrown if assignment ot a value to a preference fails due to a downstream
* exception.
*
* @author Marius Lindvall
*/
class PreferenceAssignmentException extends RuntimeException {
private static final long serialVersionUID = 8233494694851033874L;
PreferenceAssignmentException(Exception parent) {
super(parent);
}
}

View file

@ -0,0 +1,166 @@
package info.varden.hauk.system.preferences;
import android.content.Context;
import androidx.preference.PreferenceDataStore;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import info.varden.hauk.Constants;
import info.varden.hauk.utils.Log;
/**
* Preference interceptor data store that redirects preference storage requests to
* {@link PreferenceManager} and {@link Preference} for proper validation and storage. This allows
* preferences to be encrypted, for example.
*
* @author Marius Lindvall
*/
public final class PreferenceHandler extends PreferenceDataStore {
/**
* Mapping between all preference keys and {@link Preference}s.
*/
private static final Map<String, Preference> map;
static {
// Initialize the preference map.
map = new HashMap<>();
// Find all Preferences declared in the Constants class and add them to the map.
Field[] fields = Constants.class.getFields();
for (Field field : fields) {
if (field.getType().isAssignableFrom(Preference.class)) {
try {
Log.v("Found field %s of type Preference in Constants, adding to map", field.getName()); //NON-NLS
Preference p = (Preference) field.get(null);
map.put(p.getKey(), p);
} catch (IllegalAccessException e) {
Log.wtf("Failed to read constant from Constants", e); //NON-NLS
}
}
}
}
/**
* Hauk preference manager.
*/
private final PreferenceManager manager;
public PreferenceHandler(Context ctx) {
this.manager = new PreferenceManager(ctx);
}
@Override
public boolean getBoolean(String key, boolean defValue) {
Log.v("Getting boolean key %s", key); //NON-NLS
if (!map.containsKey(key)) throw new PreferenceNotFoundException(key);
Object value = this.manager.get(map.get(key));
if (value instanceof Boolean) {
return (boolean) value;
} else {
throw new InvalidPreferenceTypeException(value, Boolean.class);
}
}
@Override
public float getFloat(String key, float defValue) {
Log.v("Getting float key %s", key); //NON-NLS
if (!map.containsKey(key)) throw new PreferenceNotFoundException(key);
Object value = this.manager.get(map.get(key));
if (value instanceof Float) {
return (float) value;
} else {
throw new InvalidPreferenceTypeException(value, Float.class);
}
}
@Override
public int getInt(String key, int defValue) {
Log.v("Getting int key %s", key); //NON-NLS
if (!map.containsKey(key)) throw new PreferenceNotFoundException(key);
Object value = this.manager.get(map.get(key));
if (value instanceof Integer) {
return (int) value;
} else {
throw new InvalidPreferenceTypeException(value, Integer.class);
}
}
@Override
public long getLong(String key, long defValue) {
Log.v("Getting long key %s", key); //NON-NLS
if (!map.containsKey(key)) throw new PreferenceNotFoundException(key);
Object value = this.manager.get(map.get(key));
if (value instanceof Long) {
return (long) value;
} else {
throw new InvalidPreferenceTypeException(value, Long.class);
}
}
@Override
public String getString(String key, String defValue) {
Log.v("Getting string key %s", key); //NON-NLS
if (!map.containsKey(key)) throw new PreferenceNotFoundException(key);
Object value = this.manager.get(map.get(key));
if (value instanceof String) {
return (String) value;
} else if (value instanceof Integer || value instanceof Float || value instanceof Long) {
// EditTextPreference calls getString() instead of getInt(), getFloat() and getLong()
// because it is a text input field, despite the type of data it is set to store. This
// must be handled properly.
return String.valueOf(value);
} else if (value instanceof IndexedEnum) {
return String.valueOf(((IndexedEnum) value).getIndex());
} else {
throw new InvalidPreferenceTypeException(value, String.class);
}
}
@Override
public void putBoolean(String key, boolean value) {
this.manager.set(map.get(key), value);
}
@Override
public void putFloat(String key, float value) {
this.manager.set(map.get(key), value);
}
@Override
public void putInt(String key, int value) {
this.manager.set(map.get(key), value);
}
@Override
public void putLong(String key, long value) {
this.manager.set(map.get(key), value);
}
@Override
public void putString(String key, String value) {
// EditTextPreferences calls putString() instead of putInt(), putFloat() and putLong()
// because it is a text input field, despite the type of data it is set to store. This
// must be handled properly.
Class<?> type = map.get(key).getPreferenceType();
if (type == Integer.class) {
putInt(key, Integer.valueOf(value));
} else if (type == Float.class) {
putFloat(key, Float.valueOf(value));
} else if (type == Long.class) {
putLong(key, Long.valueOf(value));
} else if (type == IndexedEnum.class) {
@SuppressWarnings("unchecked")
Preference<IndexedEnum> pref = (Preference<IndexedEnum>) map.get(key);
try {
this.manager.set(pref, pref.getDefault().fromIndex(Integer.valueOf(value)));
} catch (Exception e) {
throw new PreferenceAssignmentException(e);
}
} else {
this.manager.set(map.get(key), value);
}
}
}

View file

@ -1,9 +1,10 @@
package info.varden.hauk.utils;
package info.varden.hauk.system.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import info.varden.hauk.Constants;
import info.varden.hauk.utils.Log;
/**
* Utility class that manages connection preferences in Hauk.
@ -39,7 +40,7 @@ public final class PreferenceManager {
* @see Constants
*/
public <T> void set(Preference<T> pair, T value) {
Log.v("Setting preference %s, value=%s", pair, value); //NON-NLS
Log.v("Setting preference %s, value=%s", pair, pair.isSensitive() ? "<hidden>" : value); //NON-NLS
SharedPreferences.Editor editor = this.prefs.edit();
pair.set(editor, value);
editor.apply();

View file

@ -0,0 +1,15 @@
package info.varden.hauk.system.preferences;
/**
* Exception that is thrown when {@link info.varden.hauk.system.preferences.ui.SettingsActivity}
* tries to read a setting that does not exist in Hauk.
*
* @author Marius Lindvall
*/
class PreferenceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 6201186189243885309L;
PreferenceNotFoundException(String key) {
super(String.format("Preference %s was requested but does not exist in Hauk", key)); //NON-NLS
}
}

View file

@ -0,0 +1,22 @@
package info.varden.hauk.system.preferences.indexresolver;
import androidx.appcompat.app.AppCompatDelegate;
/**
* An enum preference that maps night mode styles for the app.
*
* @author Marius Lindvall
*/
@SuppressWarnings("unused")
public final class NightModeStyle extends Resolver<NightModeStyle, Integer> {
private static final long serialVersionUID = 1926796368584326815L;
public static final NightModeStyle FOLLOW_SYSTEM = new NightModeStyle(0, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
public static final NightModeStyle AUTO_BATTERY = new NightModeStyle(1, AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
public static final NightModeStyle ALWAYS_DARK = new NightModeStyle(2, AppCompatDelegate.MODE_NIGHT_YES);
public static final NightModeStyle NEVER_DARK = new NightModeStyle(3, AppCompatDelegate.MODE_NIGHT_NO);
private NightModeStyle(int index, Integer mapping) {
super(index, mapping);
}
}

View file

@ -0,0 +1,23 @@
package info.varden.hauk.system.preferences.indexresolver;
import java.net.Proxy;
/**
* An enum representing the various types of proxies available on the system, and their ID when
* stored in preferences.
*
* @author Marius Lindvall
*/
@SuppressWarnings("unused")
public final class ProxyTypeResolver extends Resolver<ProxyTypeResolver, Proxy.Type> {
private static final long serialVersionUID = -2687503543989317320L;
public static final ProxyTypeResolver SYSTEM_DEFAULT = new ProxyTypeResolver(0, null);
public static final ProxyTypeResolver DIRECT = new ProxyTypeResolver(1, Proxy.Type.DIRECT);
public static final ProxyTypeResolver HTTP = new ProxyTypeResolver(2, Proxy.Type.HTTP);
public static final ProxyTypeResolver SOCKS = new ProxyTypeResolver(3, Proxy.Type.SOCKS);
private ProxyTypeResolver(int index, Proxy.Type mapping) {
super(index, mapping);
}
}

View file

@ -0,0 +1,32 @@
package info.varden.hauk.system.preferences.indexresolver;
import java.io.Serializable;
import info.varden.hauk.system.preferences.IndexedEnum;
/**
* A class that provides mappings between an index and a specific type of object for easy
* translation when reading preferences.
*
* @param <T1> The type of class implementing this class.
* @param <T2> The class that each entry in the parent class should map to.
*/
public abstract class Resolver<T1 extends IndexedEnum<T1>, T2 extends Serializable> extends IndexedEnum<T1> {
private static final long serialVersionUID = 1829235445367254385L;
private final T2 mapping;
protected Resolver(int index, T2 mapping) {
super(index);
this.mapping = mapping;
}
public final T2 resolve() {
return this.mapping;
}
@Override
public final String toString() {
return getClass().getSimpleName() + "{mapping=" + this.mapping + "," + super.toString() + "}";
}
}

View file

@ -0,0 +1,124 @@
package info.varden.hauk.system.preferences.ui;
import android.content.Context;
import android.os.Bundle;
import android.text.InputType;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.EditTextPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import info.varden.hauk.BuildConfig;
import info.varden.hauk.Constants;
import info.varden.hauk.R;
import info.varden.hauk.system.launcher.OpenLinkListener;
import info.varden.hauk.system.preferences.PreferenceHandler;
import info.varden.hauk.system.preferences.ui.listener.CascadeBindListener;
import info.varden.hauk.system.preferences.ui.listener.CascadeChangeListener;
import info.varden.hauk.system.preferences.ui.listener.FloatBoundChangeListener;
import info.varden.hauk.system.preferences.ui.listener.HintBindListener;
import info.varden.hauk.system.preferences.ui.listener.InputTypeBindListener;
import info.varden.hauk.system.preferences.ui.listener.IntegerBoundChangeListener;
import info.varden.hauk.system.preferences.ui.listener.NightModeChangeListener;
import info.varden.hauk.system.preferences.ui.listener.ProxyPreferenceChangeListener;
import info.varden.hauk.utils.Log;
/**
* Settings activity that allows the user to change app preferences.
*
* @author Marius Lindvall
*/
public final class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.settings_activity);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings, new SettingsFragment())
.commit();
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
public static final class SettingsFragment extends PreferenceFragmentCompat {
private Context ctx = null;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
PreferenceManager manager = getPreferenceManager();
info.varden.hauk.system.preferences.PreferenceManager prefs = new info.varden.hauk.system.preferences.PreferenceManager(this.ctx);
// Intercept all reads and writes so that values are properly validated and encrypted if
// required by Preference.
manager.setPreferenceDataStore(new PreferenceHandler(this.ctx));
// Load the preferences layout.
setPreferencesFromResource(R.xml.root_preferences, rootKey);
// Set InputType and other attributes for text edit boxes.
setTextEditParams(manager, Constants.PREF_SERVER_ENCRYPTED, new InputTypeBindListener(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI), new HintBindListener(R.string.pref_cryptServer_hint));
setTextEditParams(manager, Constants.PREF_USERNAME_ENCRYPTED, new InputTypeBindListener(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PERSON_NAME), new HintBindListener(R.string.pref_cryptUsername_hint));
setTextEditParams(manager, Constants.PREF_PASSWORD_ENCRYPTED, new InputTypeBindListener(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD));
setTextEditParams(manager, Constants.PREF_E2E_PASSWORD, new InputTypeBindListener(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD));
setTextEditParams(manager, Constants.PREF_INTERVAL, new InputTypeBindListener(InputType.TYPE_CLASS_NUMBER));
setTextEditParams(manager, Constants.PREF_UPDATE_DISTANCE, new InputTypeBindListener(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL));
setTextEditParams(manager, Constants.PREF_CUSTOM_ID, new InputTypeBindListener(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE), new HintBindListener(R.string.pref_requestLink_hint));
setTextEditParams(manager, Constants.PREF_PROXY_HOST, new InputTypeBindListener(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI));
setTextEditParams(manager, Constants.PREF_PROXY_PORT, new InputTypeBindListener(InputType.TYPE_CLASS_NUMBER));
setTextEditParams(manager, Constants.PREF_CONNECTION_TIMEOUT, new InputTypeBindListener(InputType.TYPE_CLASS_NUMBER));
// Set value bounds checks.
setChangeListeners(manager, Constants.PREF_INTERVAL, new IntegerBoundChangeListener(1, Integer.MAX_VALUE));
setChangeListeners(manager, Constants.PREF_UPDATE_DISTANCE, new FloatBoundChangeListener(0.0F, Float.MAX_VALUE));
setChangeListeners(manager, Constants.PREF_PROXY_PORT, new IntegerBoundChangeListener(Constants.PORT_MIN, Constants.PORT_MAX));
setChangeListeners(manager, Constants.PREF_CONNECTION_TIMEOUT, new IntegerBoundChangeListener(1, Integer.MAX_VALUE));
// Set proxy settings disabled if proxy is set to default or none.
setChangeListeners(manager, Constants.PREF_PROXY_TYPE, new ProxyPreferenceChangeListener(new Preference[]{
manager.findPreference(Constants.PREF_PROXY_HOST.getKey()),
manager.findPreference(Constants.PREF_PROXY_PORT.getKey())
}));
Preference proxyTypePref = manager.findPreference(Constants.PREF_PROXY_TYPE.getKey());
if (proxyTypePref != null) proxyTypePref.callChangeListener(String.valueOf(prefs.get(Constants.PREF_PROXY_TYPE).getIndex()));
// Update night mode when its preference is changed.
setChangeListeners(manager, Constants.PREF_NIGHT_MODE, new NightModeChangeListener());
manager.findPreference("dummy_version").setSummary(BuildConfig.VERSION_NAME);
manager.findPreference("dummy_sourceCode").setOnPreferenceClickListener(new OpenLinkListener(this.ctx, R.string.label_source_link));
manager.findPreference("dummy_reportIssue").setOnPreferenceClickListener(new OpenLinkListener(this.ctx, R.string.link_issue_tracker));
}
private static void setTextEditParams(PreferenceManager manager, info.varden.hauk.system.preferences.Preference<?> preference, EditTextPreference.OnBindEditTextListener... listeners) {
EditTextPreference pref = manager.findPreference(preference.getKey());
if (pref != null) {
pref.setOnBindEditTextListener(new CascadeBindListener(listeners));
} else {
Log.wtf("Could not find setting for preference %s setting OnBindEditTextListener", preference); //NON-NLS
}
}
private static void setChangeListeners(PreferenceManager manager, info.varden.hauk.system.preferences.Preference<?> preference, Preference.OnPreferenceChangeListener... listeners) {
Preference pref = manager.findPreference(preference.getKey());
if (pref != null) {
pref.setOnPreferenceChangeListener(new CascadeChangeListener(listeners));
} else {
Log.wtf("Could not find setting for preference %s when setting OnPreferenceChangeListener", preference); //NON-NLS
}
}
@Override
public void onAttach(Context ctx) {
super.onAttach(ctx);
this.ctx = ctx;
}
}
}

View file

@ -0,0 +1,27 @@
package info.varden.hauk.system.preferences.ui.listener;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.preference.EditTextPreference;
/**
* Edit text bind listener that cascades the bind event to several
* {@link androidx.preference.EditTextPreference.OnBindEditTextListener}s.
*
* @author Marius Lindvall
*/
public final class CascadeBindListener implements EditTextPreference.OnBindEditTextListener {
private final EditTextPreference.OnBindEditTextListener[] listeners;
public CascadeBindListener(EditTextPreference.OnBindEditTextListener[] listeners) {
this.listeners = listeners.clone();
}
@Override
public void onBindEditText(@NonNull EditText editText) {
for (EditTextPreference.OnBindEditTextListener listener : this.listeners) {
listener.onBindEditText(editText);
}
}
}

View file

@ -0,0 +1,25 @@
package info.varden.hauk.system.preferences.ui.listener;
import androidx.preference.Preference;
/**
* Preference change listener that cascades the change event to several
* {@link androidx.preference.Preference.OnPreferenceChangeListener}s.
*
* @author Marius Lindvall
*/
public final class CascadeChangeListener implements Preference.OnPreferenceChangeListener {
private final Preference.OnPreferenceChangeListener[] listeners;
public CascadeChangeListener(Preference.OnPreferenceChangeListener[] listeners) {
this.listeners = listeners.clone();
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
for (Preference.OnPreferenceChangeListener listener : this.listeners) {
if (!listener.onPreferenceChange(preference, newValue)) return false;
}
return true;
}
}

View file

@ -0,0 +1,32 @@
package info.varden.hauk.system.preferences.ui.listener;
import androidx.preference.Preference;
import info.varden.hauk.utils.Log;
/**
* Bounds checking preference change listener that ensures the given value is between two floating
* point values (inclusive).
*
* @author Marius Lindvall
*/
public final class FloatBoundChangeListener implements Preference.OnPreferenceChangeListener {
private final float min;
private final float max;
public FloatBoundChangeListener(float min, float max) {
this.min = min;
this.max = max;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
try {
float value = Float.parseFloat((String) newValue);
return value >= this.min && value <= this.max;
} catch (NumberFormatException ex) {
Log.e("Number %s is not a valid float", ex, newValue); //NON-NLS
}
return false;
}
}

View file

@ -0,0 +1,24 @@
package info.varden.hauk.system.preferences.ui.listener;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.preference.EditTextPreference;
/**
* Edit text bind listener that sets the hint of an {@link EditTextPreference}.
*
* @author Marius Lindvall
*/
public final class HintBindListener implements EditTextPreference.OnBindEditTextListener {
private final int hintResource;
public HintBindListener(int hintResource) {
this.hintResource = hintResource;
}
@Override
public void onBindEditText(@NonNull EditText editText) {
editText.setHint(this.hintResource);
}
}

View file

@ -0,0 +1,24 @@
package info.varden.hauk.system.preferences.ui.listener;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.preference.EditTextPreference;
/**
* Edit text bind listener that sets the input type of an {@link EditTextPreference}.
*
* @author Marius Lindvall
*/
public final class InputTypeBindListener implements EditTextPreference.OnBindEditTextListener {
private final int inputType;
public InputTypeBindListener(int inputType) {
this.inputType = inputType;
}
@Override
public void onBindEditText(@NonNull EditText editText) {
editText.setInputType(this.inputType);
}
}

View file

@ -0,0 +1,32 @@
package info.varden.hauk.system.preferences.ui.listener;
import androidx.preference.Preference;
import info.varden.hauk.utils.Log;
/**
* Bounds checking preference change listener that ensures the given value is between two integer
* values (inclusive).
*
* @author Marius Lindvall
*/
public final class IntegerBoundChangeListener implements Preference.OnPreferenceChangeListener {
private final int min;
private final int max;
public IntegerBoundChangeListener(int min, int max) {
this.min = min;
this.max = max;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
try {
int value = Integer.parseInt((String) newValue);
return value >= this.min && value <= this.max;
} catch (NumberFormatException ex) {
Log.e("Number %s is not a valid integer", ex, newValue); //NON-NLS
}
return false;
}
}

View file

@ -0,0 +1,31 @@
package info.varden.hauk.system.preferences.ui.listener;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.Preference;
import info.varden.hauk.system.preferences.IndexedEnum;
import info.varden.hauk.system.preferences.indexresolver.NightModeStyle;
import info.varden.hauk.utils.Log;
/**
* Value change listener for the night mode preference that sets the new night mode style on
* selection.
*
* @author Marius Lindvall
*/
public final class NightModeChangeListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
try {
// Resolve the night mode (an instance of NightModeStyle is required for this).
int mode = IndexedEnum.fromIndex(NightModeStyle.class, Integer.valueOf((String) newValue)).resolve();
Log.i("Setting night mode %s", mode); //NON-NLS
AppCompatDelegate.setDefaultNightMode(mode);
return true;
} catch (Exception e) {
Log.e("Could not determine night mode style for value %s", e, newValue); //NON-NLS
return false;
}
}
}

View file

@ -0,0 +1,29 @@
package info.varden.hauk.system.preferences.ui.listener;
import androidx.preference.Preference;
import info.varden.hauk.system.preferences.indexresolver.ProxyTypeResolver;
/**
* Value change listener for the proxy type selection preference that disables the other proxy
* settings if a selection is made to the type that makes the other proxy settings unnecessary.
*
* @author Marius Lindvall
*/
public final class ProxyPreferenceChangeListener implements Preference.OnPreferenceChangeListener {
private final Preference[] prefsToDisable;
public ProxyPreferenceChangeListener(Preference[] prefsToDisable) {
this.prefsToDisable = prefsToDisable.clone();
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
int choice = Integer.valueOf((String) newValue);
boolean enable = choice != ProxyTypeResolver.SYSTEM_DEFAULT.getIndex() && choice != ProxyTypeResolver.DIRECT.getIndex();
for (Preference pref : this.prefsToDisable) {
pref.setEnabled(enable);
}
return true;
}
}

View file

@ -22,6 +22,9 @@ final class GNSSStatusLabelUpdater implements GNSSStatusUpdateListener {
*/
private final TextView statusLabel;
private int lastStatus = R.string.label_status_none;
private int lastColor = R.color.statusOff;
GNSSStatusLabelUpdater(Context ctx, TextView statusLabel) {
this.ctx = ctx;
this.statusLabel = statusLabel;
@ -30,15 +33,30 @@ final class GNSSStatusLabelUpdater implements GNSSStatusUpdateListener {
@Override
public void onShutdown() {
Log.d("Resetting GNSS status label"); //NON-NLS
this.statusLabel.setText(this.ctx.getString(R.string.label_status_none));
this.statusLabel.setText(R.string.label_status_none);
this.statusLabel.setTextColor(this.ctx.getColor(R.color.statusOff));
this.lastStatus = R.string.label_status_none;
this.lastColor = R.color.statusOff;
}
@Override
public void onStarted() {
Log.d("Set GNSS status label to initial state"); //NON-NLS
this.statusLabel.setText(this.ctx.getString(R.string.label_status_wait));
this.statusLabel.setText(R.string.label_status_wait);
this.statusLabel.setTextColor(this.ctx.getColor(R.color.statusWait));
this.lastStatus = R.string.label_status_wait;
this.lastColor = R.color.statusWait;
}
@Override
public void onGNSSConnectionLost() {
// Indicate to the user that the GNSS connection was lost, and that we are now searching for
// a location again.
Log.i("GNSS location provider has stopped working; bound to coarse location provider"); //NON-NLS
this.statusLabel.setText(R.string.label_status_lost_gnss);
this.statusLabel.setTextColor(this.ctx.getColor(R.color.statusWait));
this.lastStatus = R.string.label_status_lost_gnss;
this.lastColor = R.color.statusWait;
}
@Override
@ -46,7 +64,10 @@ final class GNSSStatusLabelUpdater implements GNSSStatusUpdateListener {
// Indicate to the user that GPS data is being received when the location pusher starts
// receiving GPS data.
Log.i("Initial coarse location was received, awaiting high accuracy fix"); //NON-NLS
this.statusLabel.setText(this.ctx.getString(R.string.label_status_coarse));
this.statusLabel.setText(R.string.label_status_coarse);
this.statusLabel.setTextColor(this.ctx.getColor(R.color.statusWait));
this.lastStatus = R.string.label_status_coarse;
this.lastColor = R.color.statusWait;
}
@Override
@ -54,7 +75,23 @@ final class GNSSStatusLabelUpdater implements GNSSStatusUpdateListener {
// Indicate to the user that GPS data is being received when the location pusher starts
// receiving GPS data.
Log.i("Initial high accuracy location was received, using GNSS location data for all future location updates"); //NON-NLS
this.statusLabel.setText(this.ctx.getString(R.string.label_status_ok));
this.statusLabel.setText(R.string.label_status_ok);
this.statusLabel.setTextColor(this.ctx.getColor(R.color.statusOn));
this.lastStatus = R.string.label_status_ok;
this.lastColor = R.color.statusOn;
}
@Override
public void onServerConnectionLost() {
// Indicate to the user that the backend connection was lost.
this.statusLabel.setText(R.string.label_status_disconnected);
this.statusLabel.setTextColor(this.ctx.getColor(R.color.statusDisconnected));
}
@Override
public void onServerConnectionRestored() {
// Restore the previous status when connection to the backend is restored.
this.statusLabel.setText(this.lastStatus);
this.statusLabel.setTextColor(this.ctx.getColor(this.lastColor));
}
}

View file

@ -6,17 +6,20 @@ import android.content.Context;
import android.content.Intent;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Checkable;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import info.varden.hauk.Constants;
@ -25,27 +28,27 @@ import info.varden.hauk.caching.ResumePrompt;
import info.varden.hauk.dialog.Buttons;
import info.varden.hauk.dialog.CustomDialogBuilder;
import info.varden.hauk.dialog.DialogService;
import info.varden.hauk.dialog.StopSharingConfirmationPrompt;
import info.varden.hauk.http.SessionInitiationPacket;
import info.varden.hauk.manager.PromptCallback;
import info.varden.hauk.manager.SessionInitiationReason;
import info.varden.hauk.manager.SessionInitiationResponseHandler;
import info.varden.hauk.manager.SessionListener;
import info.varden.hauk.manager.SessionManager;
import info.varden.hauk.manager.ShareListener;
import info.varden.hauk.struct.AdoptabilityPreference;
import info.varden.hauk.struct.Session;
import info.varden.hauk.struct.Share;
import info.varden.hauk.struct.ShareMode;
import info.varden.hauk.struct.Version;
import info.varden.hauk.system.LocationPermissionsNotGrantedException;
import info.varden.hauk.system.LocationServicesDisabledException;
import info.varden.hauk.system.launcher.OpenLinkListener;
import info.varden.hauk.system.powersaving.DeviceChecker;
import info.varden.hauk.system.preferences.PreferenceManager;
import info.varden.hauk.system.preferences.ui.SettingsActivity;
import info.varden.hauk.ui.listener.AddLinkClickListener;
import info.varden.hauk.ui.listener.InitiateAdoptionClickListener;
import info.varden.hauk.ui.listener.RememberPasswordPreferenceChangedListener;
import info.varden.hauk.ui.listener.SelectionModeChangedListener;
import info.varden.hauk.utils.DeprecationMigrator;
import info.varden.hauk.utils.Log;
import info.varden.hauk.utils.PreferenceManager;
import info.varden.hauk.utils.TimeUtils;
/**
@ -102,17 +105,13 @@ public final class MainActivity extends AppCompatActivity {
Log.i("Creating main activity"); //NON-NLS
setContentView(R.layout.activity_main);
setSupportActionBar((Toolbar) findViewById(R.id.mainToolbar));
setClassVariables();
((TextView) findViewById(R.id.labelAdoptWhatsThis)).setPaintFlags(Paint.UNDERLINE_TEXT_FLAG);
Log.d("Attaching event handlers"); //NON-NLS
// Add an on checked handler to the password remember checkbox to save their encryption
// password immediately.
((CompoundButton) findViewById(R.id.chkRemember)).setOnCheckedChangeListener(
new RememberPasswordPreferenceChangedListener(this, (EditText) findViewById(R.id.txtE2EPassword))
);
// Add an event handler to the sharing mode selector.
//noinspection OverlyStrongTypeCast
((Spinner) findViewById(R.id.selMode)).setOnItemSelectedListener(new SelectionModeChangedListener(
@ -122,6 +121,7 @@ public final class MainActivity extends AppCompatActivity {
));
loadPreferences();
this.manager.resumeShares(new ResumePrompt() {
@Override
public void promptForResumption(Context ctx, Session session, Share[] shares, PromptCallback response) {
@ -129,7 +129,7 @@ public final class MainActivity extends AppCompatActivity {
new DialogService(ctx).showDialog(
R.string.resume_title,
String.format(ctx.getString(R.string.resume_body), shares.length, session.getExpiryString()),
Buttons.YES_NO,
Buttons.Two.YES_NO,
new ResumeDialogBuilder(response)
);
}
@ -139,21 +139,50 @@ public final class MainActivity extends AppCompatActivity {
new DeviceChecker(this).performCheck();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.title_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
startActivity(new Intent(this, SettingsActivity.class));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onDestroy() {
this.uiStopTask.setActivityDestroyed();
super.onDestroy();
}
@Override
protected void onResume() {
super.onResume();
// Set app logo visibility.
PreferenceManager prefs = new PreferenceManager(this);
findViewById(R.id.imgLogo).setVisibility(prefs.get(Constants.PREF_HIDE_LOGO) ? View.GONE : View.VISIBLE);
}
/**
* On-tap handler for the "start sharing" and "stop sharing" button.
*/
public void startSharing(@SuppressWarnings("unused") View view) {
PreferenceManager prefs = new PreferenceManager(this);
// If there is an executable stop task, that means that sharing is already active. Shut down
// the share by running the stop task instead of starting a new share.
if (this.manager.isSessionActive()) {
Log.i("Sharing is being stopped from main activity"); //NON-NLS
this.manager.stopSharing();
stopSharing(prefs);
return;
}
@ -161,72 +190,74 @@ public final class MainActivity extends AppCompatActivity {
findViewById(R.id.btnShare).setEnabled(false);
disableUI();
String server = ((TextView) findViewById(R.id.txtServer)).getText().toString().trim();
String username = ((TextView) findViewById(R.id.txtUsername)).getText().toString().trim();
String password = ((TextView) findViewById(R.id.txtPassword)).getText().toString();
int duration = Integer.parseInt(((TextView) findViewById(R.id.txtDuration)).getText().toString());
int interval = Integer.parseInt(((TextView) findViewById(R.id.txtInterval)).getText().toString());
String customID = ((TextView) findViewById(R.id.txtCustomID)).getText().toString().trim();
String e2ePass = ((TextView) findViewById(R.id.txtE2EPassword)).getText().toString();
String server = prefs.get(Constants.PREF_SERVER_ENCRYPTED).trim();
String username = prefs.get(Constants.PREF_USERNAME_ENCRYPTED).trim();
String password = prefs.get(Constants.PREF_PASSWORD_ENCRYPTED);
int duration;
int interval = prefs.get(Constants.PREF_INTERVAL);
float minDistance = prefs.get(Constants.PREF_UPDATE_DISTANCE);
String customID = prefs.get(Constants.PREF_CUSTOM_ID).trim();
boolean useE2E = prefs.get(Constants.PREF_ENABLE_E2E);
String e2ePass = !useE2E ? "" : prefs.get(Constants.PREF_E2E_PASSWORD);
String nickname = ((TextView) findViewById(R.id.txtNickname)).getText().toString().trim();
@SuppressWarnings("OverlyStrongTypeCast") ShareMode mode = ShareMode.fromMode(((Spinner) findViewById(R.id.selMode)).getSelectedItemPosition());
String groupPin = ((TextView) findViewById(R.id.txtGroupCode)).getText().toString();
boolean allowAdoption = ((Checkable) findViewById(R.id.chkAllowAdopt)).isChecked();
@SuppressWarnings("OverlyStrongTypeCast") int durUnit = ((Spinner) findViewById(R.id.selUnit)).getSelectedItemPosition();
assert mode != null;
server = server.endsWith("/") ? server : server + "/";
// Save connection preferences for next launch, so the user doesn't have to enter URL etc.
// every time.
Log.i("Updating connection preferences"); //NON-NLS
PreferenceManager prefs = new PreferenceManager(this);
prefs.set(Constants.PREF_SERVER_ENCRYPTED, server);
prefs.set(Constants.PREF_USERNAME_ENCRYPTED, username);
prefs.set(Constants.PREF_PASSWORD_ENCRYPTED, password);
prefs.set(Constants.PREF_DURATION, duration);
prefs.set(Constants.PREF_INTERVAL, interval);
prefs.set(Constants.PREF_CUSTOM_ID, customID);
prefs.set(Constants.PREF_DURATION_UNIT, durUnit);
prefs.set(Constants.PREF_NICKNAME, nickname);
prefs.set(Constants.PREF_ALLOW_ADOPTION, allowAdoption);
// If password saving is enabled, save the password as well.
if (((Checkable) findViewById(R.id.chkRemember)).isChecked()) {
Log.i("Saving E2E password"); //NON-NLS
prefs.set(Constants.PREF_REMEMBER_PASSWORD, true);
prefs.set(Constants.PREF_E2E_PASSWORD, e2ePass);
try {
// Try to parse the duration.
duration = Integer.parseInt(((TextView) findViewById(R.id.txtDuration)).getText().toString());
prefs.set(Constants.PREF_DURATION, duration);
// The backend takes duration in seconds, hence it must be converted.
duration = TimeUtils.timeUnitsToSeconds(duration, durUnit);
} catch (NumberFormatException | ArithmeticException ex) {
Log.e("Illegal duration value", ex); //NON-NLS
this.dialogSvc.showDialog(R.string.err_client, R.string.err_invalid_duration, this.uiResetTask);
return;
}
assert mode != null;
server = server.endsWith("/") ? server : server + "/";
if ((mode == ShareMode.CREATE_GROUP || mode == ShareMode.JOIN_GROUP) && nickname.isEmpty()) {
Log.e("No nickname set!"); //NON-NLS
this.dialogSvc.showDialog(R.string.err_client, R.string.err_no_nickname, this.uiResetTask);
return;
}
// The backend takes duration in seconds, so convert the minutes supplied by the user.
duration = TimeUtils.timeUnitsToSeconds(duration, durUnit);
if (server.isEmpty()) {
// If the user hasn't set up a server yet, open the settings menu and prompt them to
// configure the backend.
this.uiResetTask.run();
Toast.makeText(this, R.string.err_server_not_configured, Toast.LENGTH_LONG).show();
startActivity(new Intent(this, SettingsActivity.class));
return;
}
SessionInitiationPacket.InitParameters initParams = new SessionInitiationPacket.InitParameters(server, username, password, duration, interval, customID, e2ePass);
SessionInitiationResponseHandler responseHandler = new SessionInitiationResponseHandlerImpl();
SessionInitiationPacket.InitParameters initParams = new SessionInitiationPacket.InitParameters(server, username, password, duration, interval, minDistance, customID, e2ePass);
new ProxyHostnameResolverImpl(this, this.manager, this.uiResetTask, prefs, new SessionInitiationResponseHandlerImpl(), initParams, mode, allowAdoption, nickname, groupPin).resolve();
}
try {
switch (mode) {
case CREATE_ALONE:
this.manager.shareLocation(initParams, responseHandler, allowAdoption ? AdoptabilityPreference.ALLOW_ADOPTION : AdoptabilityPreference.DISALLOW_ADOPTION);
break;
case CREATE_GROUP:
this.manager.shareLocation(initParams, responseHandler, nickname);
break;
case JOIN_GROUP:
this.manager.shareLocation(initParams, responseHandler, nickname, groupPin);
break;
default:
Log.wtf("Unknown sharing mode. This is not supposed to happen, ever"); //NON-NLS
break;
}
} catch (LocationServicesDisabledException e) {
Log.e("Share initiation was stopped because location services are disabled", e); //NON-NLS
this.dialogSvc.showDialog(R.string.err_client, R.string.err_location_disabled, this.uiResetTask);
} catch (LocationPermissionsNotGrantedException e) {
Log.w("Share initiation was stopped because the user has not granted location permissions yet", e); //NON-NLS
/**
* Stops sharing. If the setting to prompt for confirmation is enabled, a dialog box is shown to
* confirm that the share should be stopped.
*
* @param prefs A preference manager.
*/
private void stopSharing(PreferenceManager prefs) {
if (prefs.get(Constants.PREF_CONFIRM_STOP)) {
this.dialogSvc.showDialog(R.string.dialog_confirm_stop_title, R.string.dialog_confirm_stop_body, Buttons.Three.YES_NO_REMEMBER, new StopSharingConfirmationPrompt(prefs, this.manager));
} else {
this.manager.stopSharing();
}
}
@ -250,14 +281,10 @@ public final class MainActivity extends AppCompatActivity {
}
/**
* On-tap handler for the "show advanced settings" button.
* On-tap handler for the header logo and link that opens the Hauk project page on GitHub.
*/
public void showAdvancedSettings(View view) {
view.setVisibility(View.GONE);
findViewById(R.id.rowUpdateInterval).setVisibility(View.VISIBLE);
findViewById(R.id.rowCustomID).setVisibility(View.VISIBLE);
findViewById(R.id.rowE2EPassword).setVisibility(View.VISIBLE);
findViewById(R.id.rowRemember).setVisibility(View.VISIBLE);
public void openProjectSite(View view) {
new OpenLinkListener(this, R.string.label_source_link).onClick(view);
}
/**
@ -267,13 +294,7 @@ public final class MainActivity extends AppCompatActivity {
private void setClassVariables() {
Log.d("Setting class variables"); //NON-NLS
this.lockWhileRunning = new View[] {
findViewById(R.id.txtServer),
findViewById(R.id.txtUsername),
findViewById(R.id.txtPassword),
findViewById(R.id.txtDuration),
findViewById(R.id.txtInterval),
findViewById(R.id.txtCustomID),
findViewById(R.id.txtE2EPassword),
findViewById(R.id.selUnit),
findViewById(R.id.selMode),
@ -301,7 +322,7 @@ public final class MainActivity extends AppCompatActivity {
this.manager.attachShareListener(new ShareListenerImpl());
this.manager.attachSessionListener(new SessionListenerImpl());
this.linkList = new ShareLinkLayoutManager(this, this.manager, (ViewGroup) findViewById(R.id.tableLinks));
this.linkList = new ShareLinkLayoutManager(this, this.manager, (ViewGroup) findViewById(R.id.tableLinks), (TextView) findViewById(R.id.headerLinks));
}
/**
@ -310,20 +331,18 @@ public final class MainActivity extends AppCompatActivity {
private void loadPreferences() {
Log.i("Loading preferences..."); //NON-NLS
PreferenceManager prefs = new PreferenceManager(this);
((TextView) findViewById(R.id.txtServer)).setText(prefs.get(Constants.PREF_SERVER_ENCRYPTED));
((TextView) findViewById(R.id.txtUsername)).setText(prefs.get(Constants.PREF_USERNAME_ENCRYPTED));
((TextView) findViewById(R.id.txtDuration)).setText(String.valueOf(prefs.get(Constants.PREF_DURATION)));
((TextView) findViewById(R.id.txtInterval)).setText(String.valueOf(prefs.get(Constants.PREF_INTERVAL)));
((TextView) findViewById(R.id.txtCustomID)).setText(prefs.get(Constants.PREF_CUSTOM_ID));
((TextView) findViewById(R.id.txtE2EPassword)).setText(prefs.get(Constants.PREF_E2E_PASSWORD));
((TextView) findViewById(R.id.txtPassword)).setText(prefs.get(Constants.PREF_PASSWORD_ENCRYPTED));
((TextView) findViewById(R.id.txtNickname)).setText(prefs.get(Constants.PREF_NICKNAME));
// Because I can choose between an unchecked cast warning and an overly strong cast warning,
// I'm going to with the latter.
//noinspection OverlyStrongTypeCast
((Spinner) findViewById(R.id.selUnit)).setSelection(prefs.get(Constants.PREF_DURATION_UNIT));
((Checkable) findViewById(R.id.chkRemember)).setChecked(prefs.get(Constants.PREF_REMEMBER_PASSWORD));
((Checkable) findViewById(R.id.chkAllowAdopt)).setChecked(prefs.get(Constants.PREF_ALLOW_ADOPTION));
// Set night mode preference.
AppCompatDelegate.setDefaultNightMode(prefs.get(Constants.PREF_NIGHT_MODE).resolve());
// Set app logo visibility.
findViewById(R.id.imgLogo).setVisibility(prefs.get(Constants.PREF_HIDE_LOGO) ? View.GONE : View.VISIBLE);
}
/**
@ -454,7 +473,7 @@ public final class MainActivity extends AppCompatActivity {
*/
private final class SessionListenerImpl implements SessionListener {
@Override
public void onSessionCreated(Session session, final Share share) {
public void onSessionCreated(Session session, final Share share, SessionInitiationReason reason) {
// We now have a link to share, so we enable the additional link creation button if the backend supports it. Add an event handler to handle the user clicking on it.
if (session.getBackendVersion().isAtLeast(Constants.VERSION_COMPAT_VIEW_ID)) {
boolean allowNewLinkAdoption = ((Checkable) findViewById(R.id.chkAllowAdopt)).isChecked();
@ -463,7 +482,7 @@ public final class MainActivity extends AppCompatActivity {
btnLink.setOnClickListener(new AddLinkClickListener(MainActivity.this, session, allowNewLinkAdoption) {
@Override
public void onShareCreated(Share share) {
MainActivity.this.manager.shareLocation(share);
MainActivity.this.manager.shareLocation(share, SessionInitiationReason.SHARE_ADDED);
}
});
btnLink.setEnabled(true);
@ -477,31 +496,35 @@ public final class MainActivity extends AppCompatActivity {
MainActivity.this.shareCountdown.start(session.getRemainingSeconds());
// Re-enable the start (stop) button and inform the user.
disableUI();
findViewById(R.id.btnShare).setEnabled(true);
MainActivity.this.dialogSvc.showDialog(R.string.ok_title, R.string.ok_message, Buttons.OK_SHARE, new CustomDialogBuilder() {
@Override
public void onPositive() {
// OK button
}
// Service relaunches should be handled silently.
if (reason == SessionInitiationReason.USER_STARTED) {
MainActivity.this.dialogSvc.showDialog(R.string.ok_title, R.string.ok_message, Buttons.Two.OK_SHARE, new CustomDialogBuilder() {
@Override
public void onPositive() {
// OK button
}
@Override
public void onNegative() {
// Share button
Log.i("User requested to share %s", share); //NON-NLS
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType(Constants.INTENT_TYPE_COPY_LINK);
shareIntent.putExtra(Intent.EXTRA_SUBJECT, MainActivity.this.getString(R.string.share_subject));
shareIntent.putExtra(Intent.EXTRA_TEXT, share.getViewURL());
MainActivity.this.startActivity(Intent.createChooser(shareIntent, MainActivity.this.getString(R.string.share_via)));
}
@Override
public void onNegative() {
// Share button
Log.i("User requested to share %s", share); //NON-NLS
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType(Constants.INTENT_TYPE_COPY_LINK);
shareIntent.putExtra(Intent.EXTRA_SUBJECT, MainActivity.this.getString(R.string.share_subject));
shareIntent.putExtra(Intent.EXTRA_TEXT, share.getViewURL());
MainActivity.this.startActivity(Intent.createChooser(shareIntent, MainActivity.this.getString(R.string.share_via)));
}
@Nullable
@Override
public View createView(Context ctx) {
return null;
}
});
@Nullable
@Override
public View createView(Context ctx) {
return null;
}
});
}
}
@Override

View file

@ -0,0 +1,190 @@
package info.varden.hauk.ui;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.net.Proxy;
import info.varden.hauk.Constants;
import info.varden.hauk.R;
import info.varden.hauk.dialog.Buttons;
import info.varden.hauk.dialog.CustomDialogBuilder;
import info.varden.hauk.dialog.DialogService;
import info.varden.hauk.http.ConnectionParameters;
import info.varden.hauk.http.SessionInitiationPacket;
import info.varden.hauk.http.proxy.NameResolverTask;
import info.varden.hauk.http.security.CertificateValidationPolicy;
import info.varden.hauk.manager.SessionInitiationResponseHandler;
import info.varden.hauk.manager.SessionManager;
import info.varden.hauk.struct.AdoptabilityPreference;
import info.varden.hauk.struct.ShareMode;
import info.varden.hauk.system.LocationPermissionsNotGrantedException;
import info.varden.hauk.system.LocationServicesDisabledException;
import info.varden.hauk.system.preferences.PreferenceManager;
import info.varden.hauk.utils.Log;
import info.varden.hauk.utils.TimeUtils;
/**
* Implementation of {@link NameResolverTask} for {@link MainActivity}. This implementation is
* responsible for starting shares after the proxy configuration has been resolved.
*/
public final class ProxyHostnameResolverImpl extends NameResolverTask {
private final PreferenceManager prefs;
private final WeakReference<Activity> ctx;
private final SessionManager manager;
private final Runnable uiResetTask;
private final SessionInitiationResponseHandler responseHandler;
private final ShareMode mode;
private final SessionInitiationPacket.InitParameters initParams;
private final boolean allowAdoption;
private final String nickname;
private final String groupPin;
private final Object progressLock = new Object();
private ProgressDialog progress = null;
ProxyHostnameResolverImpl(Activity ctx, SessionManager manager, Runnable uiResetTask, PreferenceManager prefs, SessionInitiationResponseHandler responseHandler, SessionInitiationPacket.InitParameters initParams, ShareMode mode, boolean allowAdoption, String nickname, String groupPin) {
super(prefs);
this.prefs = prefs;
this.ctx = new WeakReference<>(ctx);
this.manager = manager;
this.uiResetTask = uiResetTask;
this.responseHandler = responseHandler;
this.initParams = initParams;
this.mode = mode;
this.allowAdoption = allowAdoption;
this.nickname = nickname;
this.groupPin = groupPin;
}
@Override
protected void onResolutionStarted(String hostname) {
// Show a progress dialog only if resolution has to take place. Otherwise, no progress
// dialog is shown.
synchronized (this.progressLock) {
this.progress = new ProgressDialog(this.ctx.get());
this.progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
this.progress.setTitle(R.string.progress_connect_title);
this.progress.setMessage(this.ctx.get().getString(R.string.progress_resolving_proxy));
this.progress.setIndeterminate(true);
this.progress.setCancelable(false);
this.progress.show();
}
}
@Override
protected void onHostUnresolved(final String hostname) {
// The hostname couldn't be resolved. Show an error message.
final Activity ctx = this.ctx.get();
if (ctx != null) {
ctx.runOnUiThread(new Runnable() {
@Override
public void run() {
// Hide progress dialog if visible.
synchronized (ProxyHostnameResolverImpl.this.progressLock) {
if (ProxyHostnameResolverImpl.this.progress != null) {
ProxyHostnameResolverImpl.this.progress.dismiss();
}
}
new DialogService(ctx).showDialog(R.string.err_connect, ctx.getString(R.string.err_proxy_host_resolution, hostname), ProxyHostnameResolverImpl.this.uiResetTask);
}
});
}
}
@Override
protected void onSuccess(@Nullable Proxy proxy) {
// Hide progress dialog if visible.
synchronized (this.progressLock) {
if (this.progress != null) {
this.progress.dismiss();
}
}
// Set the connection parameters.
int timeout = this.prefs.get(Constants.PREF_CONNECTION_TIMEOUT) * (int) TimeUtils.MILLIS_PER_SECOND;
CertificateValidationPolicy tlsPolicy = this.prefs.get(Constants.PREF_CERTIFICATE_VALIDATION);
ConnectionParameters params;
if (proxy == null) {
params = new ConnectionParameters(null, null, timeout, tlsPolicy);
} else {
params = new ConnectionParameters(proxy.type(), proxy.address(), timeout, tlsPolicy);
}
this.initParams.setConnectionParameters(params);
// Start the location share.
try {
switch (this.mode) {
case CREATE_ALONE:
this.manager.shareLocation(this.initParams, this.responseHandler, this.allowAdoption ? AdoptabilityPreference.ALLOW_ADOPTION : AdoptabilityPreference.DISALLOW_ADOPTION);
break;
case CREATE_GROUP:
this.manager.shareLocation(this.initParams, this.responseHandler, this.nickname);
break;
case JOIN_GROUP:
this.manager.shareLocation(this.initParams, this.responseHandler, this.nickname, this.groupPin);
break;
default:
Log.wtf("Unknown sharing mode. This is not supposed to happen, ever"); //NON-NLS
break;
}
} catch (LocationServicesDisabledException e) {
Log.e("Share initiation was stopped because location services are disabled", e); //NON-NLS
final Context ctx = this.ctx.get();
if (ctx != null) {
new DialogService(ctx).showDialog(R.string.err_client, R.string.err_location_disabled, Buttons.Two.SETTINGS_OK, new CustomDialogBuilder() {
@Override
public void onPositive() {
// OK button
ProxyHostnameResolverImpl.this.uiResetTask.run();
}
@Override
public void onNegative() {
// Open Settings button
ProxyHostnameResolverImpl.this.uiResetTask.run();
ctx.startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
}
@Nullable
@Override
public View createView(Context ctx) {
return null;
}
});
}
} catch (LocationPermissionsNotGrantedException e) {
Log.w("Share initiation was stopped because the user has not granted location permissions yet", e); //NON-NLS
}
}
@Override
public void onFailure(final Exception ex) {
// Proxy configuration failed for some reason. Show the error message to the user in a
// dialog.
final Activity ctx = this.ctx.get();
if (ctx != null) {
ctx.runOnUiThread(new Runnable() {
@Override
public void run() {
// Hide progress dialog if visible.
synchronized (ProxyHostnameResolverImpl.this.progressLock) {
if (ProxyHostnameResolverImpl.this.progress != null) {
ProxyHostnameResolverImpl.this.progress.dismiss();
}
}
new DialogService(ctx).showDialog(R.string.err_connect, ctx.getString(R.string.err_proxy_failure, ex.getLocalizedMessage()), ProxyHostnameResolverImpl.this.uiResetTask);
}
});
}
}
}

View file

@ -1,10 +1,12 @@
package info.varden.hauk.ui;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TableRow;
import android.widget.TextView;
import java.util.HashMap;
@ -24,7 +26,7 @@ import info.varden.hauk.utils.Log;
*
* @author Marius Lindvall
*/
final class ShareLinkLayoutManager {
public final class ShareLinkLayoutManager {
/**
* The activity on which the links should be placed.
*/
@ -40,16 +42,22 @@ final class ShareLinkLayoutManager {
*/
private final ViewGroup linkLayout;
/**
* The header above the link list, used to change the text when there are no active shares.
*/
private final TextView headerView;
/**
* A list of links displayed on the UI that the client is contributing to, paired with the View
* representing the link of that share and its controls in the link list.
*/
private final Map<Share, View> shareViewMap;
ShareLinkLayoutManager(Activity act, SessionManager manager, ViewGroup linkLayout) {
ShareLinkLayoutManager(Activity act, SessionManager manager, ViewGroup linkLayout, TextView headerView) {
this.act = act;
this.manager = manager;
this.linkLayout = linkLayout;
this.headerView = headerView;
this.shareViewMap = new HashMap<>();
removeAll();
}
@ -64,14 +72,30 @@ final class ShareLinkLayoutManager {
this.act.runOnUiThread(new AddTask(share));
}
/**
* Returns the number of shares that are visible on the UI.
*/
public int getShareViewCount() {
return this.shareViewMap.size();
}
/**
* Removes a link from the list of links that represents the given share.
*
* @param share The share whose link should be removed from the link list.
*/
@SuppressWarnings("NonBooleanMethodNameMayNotStartWithQuestion")
void remove(Share share) {
this.linkLayout.removeView(this.shareViewMap.remove(share));
void remove(final Share share) {
this.act.runOnUiThread(new Runnable() {
@Override
public void run() {
ShareLinkLayoutManager.this.linkLayout.removeView(ShareLinkLayoutManager.this.shareViewMap.remove(share));
if (ShareLinkLayoutManager.this.shareViewMap.isEmpty()) {
ShareLinkLayoutManager.this.headerView.setText(R.string.label_heading_no_links);
}
}
});
}
/**
@ -79,8 +103,14 @@ final class ShareLinkLayoutManager {
*/
@SuppressWarnings("NonBooleanMethodNameMayNotStartWithQuestion")
void removeAll() {
this.linkLayout.removeAllViews();
this.shareViewMap.clear();
this.act.runOnUiThread(new Runnable() {
@Override
public void run() {
ShareLinkLayoutManager.this.linkLayout.removeAllViews();
ShareLinkLayoutManager.this.shareViewMap.clear();
ShareLinkLayoutManager.this.headerView.setText(R.string.label_heading_no_links);
}
});
}
/**
@ -99,13 +129,19 @@ final class ShareLinkLayoutManager {
// Get the table row layout and inflate it into a view.
LayoutInflater inflater = ShareLinkLayoutManager.this.act.getLayoutInflater();
View linkView = inflater.inflate(R.layout.content_link, null);
View linkView = inflater.inflate(R.layout.content_link, ShareLinkLayoutManager.this.linkLayout, false);
// Add an event handler for the stop button. This will stop the given share only.
Button btnStop = linkView.findViewById(R.id.linkBtnStop);
if (this.share.getSession().getBackendVersion().isAtLeast(Constants.VERSION_COMPAT_VIEW_ID)) {
Log.i("Server is compatible with individual share termination"); //NON-NLS
btnStop.setOnClickListener(new StopLinkClickListener(ShareLinkLayoutManager.this.manager, this.share));
btnStop.setOnClickListener(new StopLinkClickListener(
ShareLinkLayoutManager.this.act,
ShareLinkLayoutManager.this.manager,
this.share,
ShareLinkLayoutManager.this
));
btnStop.setLayoutParams(new TableRow.LayoutParams(calculateRealWidth(btnStop), ViewGroup.LayoutParams.WRAP_CONTENT));
} else {
Log.i("Server is not compatible with individual share termination"); //NON-NLS
btnStop.setVisibility(View.GONE);
@ -114,6 +150,7 @@ final class ShareLinkLayoutManager {
// Add an event handler for the share button.
Button btnShare = linkView.findViewById(R.id.linkBtnShare);
btnShare.setOnClickListener(new ShareLinkClickListener(ShareLinkLayoutManager.this.act, this.share));
btnShare.setLayoutParams(new TableRow.LayoutParams(calculateRealWidth(btnShare), ViewGroup.LayoutParams.WRAP_CONTENT));
// Update the text on the UI.
TextView txtLink = linkView.findViewById(R.id.linkTxtLink);
@ -126,6 +163,33 @@ final class ShareLinkLayoutManager {
Log.i("Putting share in class-level share list"); //NON-NLS
ShareLinkLayoutManager.this.shareViewMap.put(this.share, linkView);
ShareLinkLayoutManager.this.linkLayout.addView(linkView);
ShareLinkLayoutManager.this.headerView.setText(R.string.label_heading_links);
}
}
/**
* Calculates the real displayed with of a {@link TextView}. The measurement of text view
* (button) width is unreliable when inflating views, as it does not appear to take into account
* drawables or padding properly. This method calculates the drawn width manually instead,
* giving a reliable width that prevents character wrapping.
*
* @param view The text view to calculate the width for.
* @return A width in pixels.
*/
private static int calculateRealWidth(TextView view) {
// Calculate the padding of the view.
int realWidth = view.getCompoundDrawablePadding() + view.getTotalPaddingStart() + view.getTotalPaddingEnd();
// Add the width of the contained text.
String text = view.getTransformationMethod().getTransformation(view.getText(), view).toString();
realWidth += view.getPaint().measureText(text);
// Add the widths of any drawables on the button.
for (Drawable drawable : view.getCompoundDrawablesRelative()) {
if (drawable != null) realWidth += drawable.getIntrinsicWidth();
}
// Return the result.
return realWidth;
}
}

View file

@ -62,7 +62,7 @@ public abstract class AddLinkClickListener implements View.OnClickListener {
@Override
public final void onClick(View view) {
this.dialogSvc.showDialog(R.string.create_link_title, R.string.create_link_body, Buttons.CREATE_CANCEL, new CustomDialogBuilder() {
this.dialogSvc.showDialog(R.string.create_link_title, R.string.create_link_body, Buttons.Two.CREATE_CANCEL, new CustomDialogBuilder() {
private CheckBox chkAdopt;

View file

@ -45,7 +45,7 @@ public final class InitiateAdoptionClickListener implements View.OnClickListener
this.dialogSvc.showDialog(
R.string.adopt_title,
String.format(this.ctx.getString(R.string.adopt_body), this.share.getSession().getServerURL()),
Buttons.OK_CANCEL,
Buttons.Two.OK_CANCEL,
new AdoptDialogBuilder(this.ctx, this.share) {
@Override

View file

@ -1,43 +0,0 @@
package info.varden.hauk.ui.listener;
import android.content.Context;
import android.widget.CompoundButton;
import android.widget.EditText;
import info.varden.hauk.Constants;
import info.varden.hauk.utils.Log;
import info.varden.hauk.utils.PreferenceManager;
/**
* On-checked-change listener for the checkbox that lets users change their preference of whether or
* not they want the app to save their password.
*
* @see info.varden.hauk.ui.MainActivity
* @author Marius Lindvall
*/
public final class RememberPasswordPreferenceChangedListener implements CompoundButton.OnCheckedChangeListener {
/**
* Android application context.
*/
private final Context ctx;
/**
* The text input box that contains the password.
*/
private final EditText passwordBox;
public RememberPasswordPreferenceChangedListener(Context ctx, EditText passwordBox) {
this.ctx = ctx;
this.passwordBox = passwordBox;
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Log.i("Password remember preference changed, remember=%s", isChecked); //NON-NLS
// Update the stored password immediately. Clear the password from preferences if the box
// was unchecked.
PreferenceManager prefs = new PreferenceManager(this.ctx);
prefs.set(Constants.PREF_REMEMBER_PASSWORD, isChecked);
prefs.set(Constants.PREF_E2E_PASSWORD, isChecked ? this.passwordBox.getText().toString() : "");
}
}

View file

@ -1,9 +1,17 @@
package info.varden.hauk.ui.listener;
import android.content.Context;
import android.view.View;
import info.varden.hauk.Constants;
import info.varden.hauk.R;
import info.varden.hauk.dialog.Buttons;
import info.varden.hauk.dialog.DialogService;
import info.varden.hauk.dialog.StopSharingConfirmationPrompt;
import info.varden.hauk.manager.SessionManager;
import info.varden.hauk.struct.Share;
import info.varden.hauk.system.preferences.PreferenceManager;
import info.varden.hauk.ui.ShareLinkLayoutManager;
import info.varden.hauk.utils.Log;
/**
@ -14,24 +22,57 @@ import info.varden.hauk.utils.Log;
* @author Marius Lindvall
*/
public final class StopLinkClickListener implements View.OnClickListener {
/**
* Service for showing dialogs.
*/
private final DialogService dialogSvc;
/**
* Preference manager, for checking if a confirmation dialog has to be displayed.
*/
private final PreferenceManager prefs;
/**
* Android application context.
*/
private final SessionManager manager;
/**
* Link layout on the UI.
*/
private final ShareLinkLayoutManager layout;
/**
* The share to share the link for.
*/
private final Share share;
public StopLinkClickListener(SessionManager manager, Share share) {
public StopLinkClickListener(Context ctx, SessionManager manager, Share share, ShareLinkLayoutManager layout) {
this.manager = manager;
this.share = share;
this.layout = layout;
this.prefs = new PreferenceManager(ctx);
this.dialogSvc = new DialogService(ctx);
}
@Override
public void onClick(View view) {
Log.i("User requested to stop sharing %s", this.share); //NON-NLS
this.manager.stopSharing(this.share);
// If there is only one share still active, stop the entire session rather than just this
// one share.
if (this.layout.getShareViewCount() == 1) {
Log.i("Stopping session because there is only one share left"); //NON-NLS
if (this.prefs.get(Constants.PREF_CONFIRM_STOP)) {
this.dialogSvc.showDialog(R.string.dialog_confirm_stop_title, R.string.dialog_confirm_stop_body, Buttons.Three.YES_NO_REMEMBER, new StopSharingConfirmationPrompt(this.prefs, this.manager));
} else {
this.manager.stopSharing();
}
} else {
if (this.prefs.get(Constants.PREF_CONFIRM_STOP)) {
this.dialogSvc.showDialog(R.string.dialog_confirm_stop_title, R.string.dialog_confirm_stop_share, Buttons.Three.YES_NO_REMEMBER, new StopSharingConfirmationPrompt(this.prefs, this.manager, this.share));
} else {
this.manager.stopSharing(this.share);
}
}
}
}

View file

@ -3,6 +3,7 @@ package info.varden.hauk.utils;
import android.content.Context;
import info.varden.hauk.Constants;
import info.varden.hauk.system.preferences.PreferenceManager;
/**
* Helper utility to migrate old, deprecated settings saved in shared preferences to modern storage.
@ -42,5 +43,10 @@ public final class DeprecationMigrator {
prefs.set(Constants.PREF_PASSWORD_ENCRYPTED, pass);
prefs.clear(Constants.PREF_PASSWORD);
}
if (!prefs.has(Constants.PREF_ENABLE_E2E)) {
boolean enableE2E = !prefs.get(Constants.PREF_E2E_PASSWORD).isEmpty();
Log.i("Setting E2E enabled preference to %s based on stored preferences", String.valueOf(enableE2E)); //NON-NLS
prefs.set(Constants.PREF_ENABLE_E2E, enableE2E);
}
}
}

View file

@ -1,6 +1,11 @@
package info.varden.hauk.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import info.varden.hauk.BuildConfig;
import info.varden.hauk.Constants;
/**
* Log wrapper to simplify logging in Hauk.
@ -13,14 +18,16 @@ public enum Log {
private static final int STACK_DEPTH = 4;
/**
* Returns the caller of the log function.
* Returns the timestamp and caller of the log function.
*/
private static String getCaller() {
private static String getLogPrefix() {
String timestamp = new SimpleDateFormat(Constants.DATE_FORMAT_LOG, Locale.US).format(new Date());
String caller = Thread.currentThread().getStackTrace()[STACK_DEPTH].toString();
if (caller.startsWith(BuildConfig.APPLICATION_ID)) {
caller = caller.substring(BuildConfig.APPLICATION_ID.length());
}
return caller;
return timestamp + ": " + caller + ": ";
}
/**
@ -37,98 +44,98 @@ public enum Log {
}
public static void e(String msg) {
android.util.Log.e(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg);
android.util.Log.e(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
}
public static void e(String msg, Object... args) {
android.util.Log.e(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)));
android.util.Log.e(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)));
}
public static void e(String msg, Throwable tr) {
android.util.Log.e(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg, tr);
android.util.Log.e(BuildConfig.APPLICATION_ID, getLogPrefix() + msg, tr);
}
public static void e(String msg, Throwable tr, Object... args) {
android.util.Log.e(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)), tr);
android.util.Log.e(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)), tr);
}
public static void w(String msg) {
android.util.Log.w(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg);
android.util.Log.w(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
}
public static void w(String msg, Object... args) {
android.util.Log.w(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)));
android.util.Log.w(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)));
}
public static void w(String msg, Throwable tr) {
android.util.Log.w(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg, tr);
android.util.Log.w(BuildConfig.APPLICATION_ID, getLogPrefix() + msg, tr);
}
public static void w(String msg, Throwable tr, Object... args) {
android.util.Log.w(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)), tr);
android.util.Log.w(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)), tr);
}
public static void i(String msg) {
android.util.Log.i(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg);
android.util.Log.i(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
}
public static void i(String msg, Object... args) {
android.util.Log.i(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)));
android.util.Log.i(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)));
}
public static void i(String msg, Throwable tr) {
android.util.Log.i(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg, tr);
android.util.Log.i(BuildConfig.APPLICATION_ID, getLogPrefix() + msg, tr);
}
public static void i(String msg, Throwable tr, Object... args) {
android.util.Log.i(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)), tr);
android.util.Log.i(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)), tr);
}
public static void v(String msg) {
android.util.Log.v(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg);
android.util.Log.v(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
}
public static void v(String msg, Object... args) {
android.util.Log.v(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)));
android.util.Log.v(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)));
}
public static void v(String msg, Throwable tr) {
android.util.Log.v(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg, tr);
android.util.Log.v(BuildConfig.APPLICATION_ID, getLogPrefix() + msg, tr);
}
public static void v(String msg, Throwable tr, Object... args) {
android.util.Log.v(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)), tr);
android.util.Log.v(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)), tr);
}
public static void d(String msg) {
android.util.Log.d(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg);
android.util.Log.d(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
}
public static void d(String msg, Object... args) {
android.util.Log.d(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)));
android.util.Log.d(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)));
}
public static void d(String msg, Throwable tr) {
android.util.Log.v(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg, tr);
android.util.Log.v(BuildConfig.APPLICATION_ID, getLogPrefix() + msg, tr);
}
public static void d(String msg, Throwable tr, Object... args) {
android.util.Log.d(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)), tr);
android.util.Log.d(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)), tr);
}
public static void wtf(String msg) {
android.util.Log.wtf(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg);
android.util.Log.wtf(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
}
public static void wtf(String msg, Object... args) {
android.util.Log.wtf(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)));
android.util.Log.wtf(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)));
}
public static void wtf(String msg, Throwable tr) {
android.util.Log.wtf(BuildConfig.APPLICATION_ID, getCaller() + ": " + msg, tr);
android.util.Log.wtf(BuildConfig.APPLICATION_ID, getLogPrefix() + msg, tr);
}
public static void wtf(String msg, Throwable tr, Object... args) {
android.util.Log.wtf(BuildConfig.APPLICATION_ID, getCaller() + ": " + String.format(msg, argsToStrings(args)), tr);
android.util.Log.wtf(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)), tr);
}
}

View file

@ -44,15 +44,21 @@ public enum TimeUtils {
return sb.toString();
}
public static int timeUnitsToSeconds(int scalar, int unit) {
public static int timeUnitsToSeconds(int scalar, int unit) throws ArithmeticException {
switch (unit) {
case Constants.DURATION_UNIT_MINUTES:
if (Integer.MAX_VALUE / SECONDS_PER_MINUTE < scalar)
throw new ArithmeticException(String.format("Integer will overflow when converting %d minutes to seconds", scalar));
return scalar * SECONDS_PER_MINUTE;
case Constants.DURATION_UNIT_HOURS:
if (Integer.MAX_VALUE / SECONDS_PER_HOUR < scalar)
throw new ArithmeticException(String.format("Integer will overflow when converting %d hours to seconds", scalar));
return scalar * SECONDS_PER_HOUR;
case Constants.DURATION_UNIT_DAYS:
if (Integer.MAX_VALUE / SECONDS_PER_DAY < scalar)
throw new ArithmeticException(String.format("Integer will overflow when converting %d days to seconds", scalar));
return scalar * SECONDS_PER_DAY;
default:

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M9,2c-1.05,0 -2.05,0.16 -3,0.46 4.06,1.27 7,5.06 7,9.54 0,4.48 -2.94,8.27 -7,9.54 0.95,0.3 1.95,0.46 3,0.46 5.52,0 10,-4.48 10,-10S14.52,2 9,2z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
</vector>

View file

@ -1,12 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:alpha="0.6"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:tint="#333333"
android:width="24dp">
<path
android:fillColor="#FF000000"
android:fillColor="?attr/colorControlNormal"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
</vector>

View file

@ -3,10 +3,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:fillColor="?attr/colorControlNormal"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View file

@ -3,10 +3,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:fillColor="?attr/colorControlNormal"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M13.5,5.5c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM9.8,8.9L7,23h2.1l1.8,-8 2.1,2v6h2v-7.5l-2.1,-2 0.6,-3C14.8,12 16.8,13 19,13v-2c-1.9,0 -3.5,-1 -4.3,-2.4l-1,-1.6c-0.4,-0.6 -1,-1 -1.7,-1 -0.3,0 -0.5,0.1 -0.8,0.1L6,8.3V13h2V9.6l1.8,-0.7"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06c-1.13,0.12 -2.19,0.46 -3.16,0.97l1.5,1.5C10.16,5.19 11.06,5 12,5c3.87,0 7,3.13 7,7 0,0.94 -0.19,1.84 -0.52,2.65l1.5,1.5c0.5,-0.96 0.84,-2.02 0.97,-3.15L23,13v-2h-2.06zM3,4.27l2.04,2.04C3.97,7.62 3.25,9.23 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c1.77,-0.2 3.38,-0.91 4.69,-1.98L19.73,21 21,19.73 4.27,3 3,4.27zM16.27,17.54C15.09,18.45 13.61,19 12,19c-3.87,0 -7,-3.13 -7,-7 0,-1.61 0.55,-3.09 1.46,-4.27l9.81,9.81z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z"/>
</vector>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillAlpha=".3"
android:fillColor="?attr/colorControlNormal"
android:pathData="M22,8V2L2,22h16V8z"/>
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M20,10v8h2v-8h-2zM12,22L12,12L2,22h10zM20,22h2v-2h-2v2z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M7.77,6.76L6.23,5.48 0.82,12l5.41,6.52 1.54,-1.28L3.42,12l4.35,-5.24zM7,13h2v-2L7,11v2zM17,11h-2v2h2v-2zM11,13h2v-2h-2v2zM17.77,5.48l-1.54,1.28L20.58,12l-4.35,5.24 1.54,1.28L23.18,12l-5.41,-6.52z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M16,17.01V10h-2v7.01h-3L15,21l4,-3.99h-3zM9,3L5,6.99h3V14h2V6.99h3L9,3z"/>
</vector>

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