mirror of
https://github.com/bilde2910/Hauk.git
synced 2026-01-24 02:46:54 +00:00
Compare commits
456 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b3d8dcbec | ||
|
|
0bbea4fe64 | ||
|
|
63f901b17a | ||
|
|
7dea87aef9 | ||
|
|
50f93c1246 | ||
|
|
2a7e9ac0b0 | ||
|
|
26d8a2e13a | ||
|
|
62cac722d3 | ||
|
|
5376f938d4 | ||
|
|
52d48fa319 | ||
|
|
1b6f6217f3 | ||
|
|
f5fb57c56e | ||
|
|
085ca4ee97 | ||
|
|
56e216375c | ||
|
|
ec223ba4e6 | ||
|
|
a0287e2c13 | ||
|
|
02ccb4fcd2 | ||
|
|
55d2f8b8fd | ||
|
|
26bd32504d | ||
|
|
08a5050b7c | ||
|
|
9d637c5b58 | ||
|
|
b95f517ada | ||
|
|
ce6b064f48 | ||
|
|
eb04608ca5 | ||
|
|
5f671667cf | ||
|
|
1e5217b1d7 | ||
|
|
bc2e9fdcce | ||
|
|
8ada63b85e | ||
|
|
899e525844 | ||
|
|
1a241900bb | ||
|
|
f6220ac07c | ||
|
|
a56ddc274e | ||
|
|
aba2be5445 | ||
|
|
cbc1278587 | ||
|
|
3dd594df55 | ||
|
|
0b6defc382 | ||
|
|
3d2c4321b7 | ||
|
|
6d7dcc3bfe | ||
|
|
9dcf6a536f | ||
|
|
f3b94ce567 | ||
|
|
148d64b0bc | ||
|
|
b443bb9689 | ||
|
|
bbca5862f3 | ||
|
|
5556018609 | ||
|
|
3ab6f72879 | ||
|
|
ccf8503562 | ||
|
|
396302a197 | ||
|
|
076dc9ac84 | ||
|
|
a7e241ade1 | ||
|
|
3235f789f7 | ||
|
|
a9379ff24b | ||
|
|
6bea156d39 | ||
|
|
f44a7eb36c | ||
|
|
b1761913a2 | ||
|
|
51241fa872 | ||
|
|
4ee03b3479 | ||
|
|
f7ae59e7c0 | ||
|
|
7f21ffb761 | ||
|
|
7b7bbb0767 | ||
|
|
532ad1c633 | ||
|
|
9ec5a2c1b7 | ||
|
|
576cb540fc | ||
|
|
d986074c00 | ||
|
|
66c7b2e00b | ||
|
|
f3a99dfcfe | ||
|
|
09ab078fa7 | ||
|
|
b5e71bdc04 | ||
|
|
f69ff64a72 | ||
|
|
2211fbfb47 | ||
|
|
cfa0032d98 | ||
|
|
a2b0936026 | ||
|
|
d37313c901 | ||
|
|
8e401e837f | ||
|
|
acab476e19 | ||
|
|
87a8ec42f3 | ||
|
|
58bc9bee12 | ||
|
|
0fb03f45b3 | ||
|
|
1ca89c560f | ||
|
|
2e912e5d5f | ||
|
|
d2856bfdb0 | ||
|
|
f387577807 | ||
|
|
d60e53b7a4 | ||
|
|
f5cb0ded1e | ||
|
|
8c438fc2e6 | ||
|
|
8e4e33bc9a | ||
|
|
19c6da754f | ||
|
|
d7371d41eb | ||
|
|
7ad8686098 | ||
|
|
5c59910e54 | ||
|
|
5ac1a87ded | ||
|
|
5ea2ff473e | ||
|
|
6dff205239 | ||
|
|
d69feaca90 | ||
|
|
d1b3a158e1 | ||
|
|
7547029e4b | ||
|
|
a2d5b6536f | ||
|
|
cc4c43ba49 | ||
|
|
b169caf778 | ||
|
|
060a81429a | ||
|
|
539bc6940e | ||
|
|
d6c338611d | ||
|
|
a9860fd433 | ||
|
|
ce47e2d84f | ||
|
|
9d27fc819f | ||
|
|
ad6eb7ce76 | ||
|
|
b86c0e7b7d | ||
|
|
cf7cb4af54 | ||
|
|
1dd863d15e | ||
|
|
a92de3409e | ||
|
|
9fe3e3e373 | ||
|
|
e33ebaf06d | ||
|
|
3dd6386a16 | ||
|
|
44ce280e4c | ||
|
|
374c155516 | ||
|
|
9c51f39506 | ||
|
|
bfa9ed6c81 | ||
|
|
5894aaec11 | ||
|
|
94426fb5b9 | ||
|
|
9c886b5002 | ||
|
|
312bec7faf | ||
|
|
d52a80c51c | ||
|
|
69bbd1d78a | ||
|
|
d160b9438a | ||
|
|
afde9c15aa | ||
|
|
a221979d64 | ||
|
|
f728447d5d | ||
|
|
8c4a893a25 | ||
|
|
088e4b5570 | ||
|
|
51c3c496a2 | ||
|
|
88c46e5f06 | ||
|
|
fa241fdc59 | ||
|
|
eefc7029b9 | ||
|
|
fe2ad64bb1 | ||
|
|
fb9dc7c7c9 | ||
|
|
1dc5d73911 | ||
|
|
ba90e59965 | ||
|
|
30190715b6 | ||
|
|
0dd94dcf82 | ||
|
|
c0f732ae32 | ||
|
|
2c79f7209f | ||
|
|
5b61666de0 | ||
|
|
80313532e1 | ||
|
|
adf829cb1a | ||
|
|
3dcd31caaa | ||
|
|
548b9c962f | ||
|
|
d58e92946b | ||
|
|
ef90630350 | ||
|
|
388fd91151 | ||
|
|
3383c713e0 | ||
|
|
9db4a66813 | ||
|
|
4258eb2d41 | ||
|
|
190347af48 | ||
|
|
9935a4b1c1 | ||
|
|
413e6bca77 | ||
|
|
256c621446 | ||
|
|
b28692ebbc | ||
|
|
eb5b47e3f2 | ||
|
|
4cd021b63b | ||
|
|
bc1b7d0a4e | ||
|
|
90b0ec1d97 | ||
|
|
c0fd845799 | ||
|
|
73f94af397 | ||
|
|
129a85641a | ||
|
|
be6cee8f1f | ||
|
|
f7a430f039 | ||
|
|
36f22f4085 | ||
|
|
e0918f25bd | ||
|
|
edf74b5cea | ||
|
|
ebd71f0300 | ||
|
|
bf21d3f025 | ||
|
|
77bb8943e5 | ||
|
|
65a4f2708f | ||
|
|
9d7ae8d9b5 | ||
|
|
60bef413be | ||
|
|
178dbd05c3 | ||
|
|
f08aa7cc46 | ||
|
|
bec5ac5557 | ||
|
|
ead0b55e16 | ||
|
|
e51d628fe8 | ||
|
|
4f1ac92b9a | ||
|
|
ea398fda16 | ||
|
|
3236a98a7e | ||
|
|
f54718364d | ||
|
|
2e933a319e | ||
|
|
86a1badccb | ||
|
|
17e9df1998 | ||
|
|
010b46f0bc | ||
|
|
89f85d2dfb | ||
|
|
e01e58d830 | ||
|
|
13ac15a57b | ||
|
|
2ec464c9c3 | ||
|
|
068aa2a9c6 | ||
|
|
fb47240df6 | ||
|
|
36b9245b7f | ||
|
|
9b811869cf | ||
|
|
f3eada1548 | ||
|
|
94491f7118 | ||
|
|
4113fe73d3 | ||
|
|
ca5afdb9a8 | ||
|
|
a891ae445f | ||
|
|
5ccd3f8694 | ||
|
|
5c21d4bb7d | ||
|
|
579e0eb5ac | ||
|
|
2a5ae0f757 | ||
|
|
1445131b58 | ||
|
|
4ce104642e | ||
|
|
99d7dce435 | ||
|
|
afc72551d8 | ||
|
|
99b5dccf16 | ||
|
|
c1bd1e8395 | ||
|
|
d0814ce437 | ||
|
|
c30740367d | ||
|
|
012ec636b8 | ||
|
|
682e11ea18 | ||
|
|
95546a8238 | ||
|
|
7d4059f3ed | ||
|
|
336b1509fd | ||
|
|
313b393ae5 | ||
|
|
0ca32c46f8 | ||
|
|
fdb35db2ce | ||
|
|
cde81fdacd | ||
|
|
4f2c99bb84 | ||
|
|
22392a4099 | ||
|
|
ddc23a2736 | ||
|
|
56929dc930 | ||
|
|
b90585fd8f | ||
|
|
c5b4b05a46 | ||
|
|
b21f73de6b | ||
|
|
f8ece683a7 | ||
|
|
9fd20cb7a7 | ||
|
|
c0c6917abc | ||
|
|
88bfcd1eb9 | ||
|
|
a658f57644 | ||
|
|
7ec9c12acc | ||
|
|
4088ea5947 | ||
|
|
16b92c0c03 | ||
|
|
6d4327f0c9 | ||
|
|
b380456849 | ||
|
|
ef6a1747dc | ||
|
|
201165d227 | ||
|
|
1d0d8767de | ||
|
|
4e660f1fe4 | ||
|
|
31b5daaa12 | ||
|
|
c8db154a36 | ||
|
|
a1018fa7a7 | ||
|
|
122304a1be | ||
|
|
9c6e5e6362 | ||
|
|
b94dacdb6a | ||
|
|
1e1d44be9f | ||
|
|
bf4eac8d2a | ||
|
|
58e5ac9414 | ||
|
|
62d28f42fe | ||
|
|
88e5419792 | ||
|
|
405e552c43 | ||
|
|
6fa10a14f3 | ||
|
|
7703ec10de | ||
|
|
9ecb7c08a9 | ||
|
|
319200ea54 | ||
|
|
21446811ab | ||
|
|
9f430a445e | ||
|
|
ed08ece16a | ||
|
|
c6c919f272 | ||
|
|
fa88bb2d7e | ||
|
|
eddd168b16 | ||
|
|
0affc83c0f | ||
|
|
fb7bc0f4c7 | ||
|
|
384dc826ca | ||
|
|
394cb90491 | ||
|
|
3449cba1a2 | ||
|
|
49aabf478c | ||
|
|
04c0f63dc8 | ||
|
|
dbfb58a502 | ||
|
|
cacc65b481 | ||
|
|
ae19fbf5d5 | ||
|
|
c2fb3d4a80 | ||
|
|
a6ccdbb857 | ||
|
|
f350a7defe | ||
|
|
8179836e8d | ||
|
|
fc6eed091b | ||
|
|
82ee59c366 | ||
|
|
37d7e4727e | ||
|
|
925b7b83c0 | ||
|
|
a1564ec0f4 | ||
|
|
c43119a396 | ||
|
|
0d4081f6a9 | ||
|
|
eaaec730d8 | ||
|
|
4f0e62c557 | ||
|
|
59c8ba5748 | ||
|
|
3ae9ae4dee | ||
|
|
9340f78c6a | ||
|
|
86c4718055 | ||
|
|
4c88a26e14 | ||
|
|
102a15b67b | ||
|
|
48e298a061 | ||
|
|
3305f4956e | ||
|
|
01a23ae780 | ||
|
|
247e08c765 | ||
|
|
73a25f486f | ||
|
|
faaf0e2989 | ||
|
|
a639fcd6be | ||
|
|
9b6dcdef7e | ||
|
|
fd4483e9bb | ||
|
|
9d988a6337 | ||
|
|
cf8d9789d6 | ||
|
|
6dd1404ef5 | ||
|
|
9357f82ef9 | ||
|
|
975ec8c2f9 | ||
|
|
a56457af90 | ||
|
|
dbeb004e45 | ||
|
|
92b567c643 | ||
|
|
4eff3eb186 | ||
|
|
a28976be1d | ||
|
|
3c3ae808f1 | ||
|
|
c949fe128c | ||
|
|
bdfc8058e9 | ||
|
|
18044fdf7c | ||
|
|
02853edc5f | ||
|
|
25217bcab8 | ||
|
|
14cef246fd | ||
|
|
f305015579 | ||
|
|
ba78bf6910 | ||
|
|
f7b1eb929f | ||
|
|
279d787365 | ||
|
|
6e6d4c1fcc | ||
|
|
04df6a22ce | ||
|
|
f950581a87 | ||
|
|
132dde219e | ||
|
|
062acbd80a | ||
|
|
9d9a06e16b | ||
|
|
85238b321d | ||
|
|
41012ee067 | ||
|
|
1705f1d92d | ||
|
|
ab76a63ee4 | ||
|
|
054d8c7214 | ||
|
|
15f53c7a38 | ||
|
|
df4d88be44 | ||
|
|
340f7a75ab | ||
|
|
60be6fa5bc | ||
|
|
a7b3a94279 | ||
|
|
b8f0eec4fe | ||
|
|
1f16601daa | ||
|
|
0eaec374ee | ||
|
|
0eb34801e8 | ||
|
|
132bca1d80 | ||
|
|
d66e7ad9d2 | ||
|
|
f7fabe2986 | ||
|
|
e43720cbd1 | ||
|
|
046d227f1e | ||
|
|
08fb603e82 | ||
|
|
6463e45cfa | ||
|
|
6cd9bb4394 | ||
|
|
9adb2d8209 | ||
|
|
c18d6a379f | ||
|
|
5cf5615398 | ||
|
|
7f4111e129 | ||
|
|
9c8cc0241a | ||
|
|
9ded0d794f | ||
|
|
a5384f5771 | ||
|
|
e25fa50363 | ||
|
|
3a9ea06134 | ||
|
|
5c44d4471c | ||
|
|
358b38e01a | ||
|
|
5a82b3eceb | ||
|
|
6a4f5b14c1 | ||
|
|
4326d08ceb | ||
|
|
88f17d5e6b | ||
|
|
db3f334fae | ||
|
|
ab1ab677c8 | ||
|
|
3c8dd9d0b7 | ||
|
|
ff77067fc9 | ||
|
|
63b296d142 | ||
|
|
0d2e03e6e3 | ||
|
|
a9d9721db2 | ||
|
|
cf1f84a8b8 | ||
|
|
45fd7461fe | ||
|
|
0cd9c3aefa | ||
|
|
febb72c6aa | ||
|
|
933daf0f9f | ||
|
|
c6f4aebcc5 | ||
|
|
bcda18dd82 | ||
|
|
8660d84f96 | ||
|
|
9e277f6f8b | ||
|
|
a326fed818 | ||
|
|
c1c7e15d08 | ||
|
|
94fd78a126 | ||
|
|
de016c2409 | ||
|
|
ba00fdaf54 | ||
|
|
e87e36f124 | ||
|
|
d23c5c2776 | ||
|
|
ed3ef23d6d | ||
|
|
4df1aeac90 | ||
|
|
cad6c13f70 | ||
|
|
369e1a4530 | ||
|
|
ca7cf93380 | ||
|
|
df944ad7e5 | ||
|
|
3b9e5c74a4 | ||
|
|
44dd1f8257 | ||
|
|
12589d3d1b | ||
|
|
1e4872d159 | ||
|
|
ebd09b2042 | ||
|
|
62777ef814 | ||
|
|
511a092bd3 | ||
|
|
0a551cbe04 | ||
|
|
a9ebeb1c9d | ||
|
|
456a9b135b | ||
|
|
4c5dc41664 | ||
|
|
18e3155b5a | ||
|
|
cf071aa0e5 | ||
|
|
c9dab2b087 | ||
|
|
b4a0ee7ce9 | ||
|
|
6bf7ef1943 | ||
|
|
277a24b692 | ||
|
|
c14170777e | ||
|
|
7ab8f5da28 | ||
|
|
7d31597b57 | ||
|
|
e50d3159e5 | ||
|
|
b4b33e7254 | ||
|
|
8cf1753927 | ||
|
|
571913bb91 | ||
|
|
ad52c8f4d0 | ||
|
|
fc1bade9ed | ||
|
|
6200de5c7e | ||
|
|
03bb0258f6 | ||
|
|
d67fd52b82 | ||
|
|
e1a11a12f7 | ||
|
|
4587be7949 | ||
|
|
028fe44e43 | ||
|
|
2279978fb2 | ||
|
|
4416259ed1 | ||
|
|
f1a95cf7a6 | ||
|
|
ef428499c9 | ||
|
|
b3077d2c72 | ||
|
|
8c82b1c9fd | ||
|
|
188153bb9a | ||
|
|
aeb618e6f8 | ||
|
|
2b5f053a94 | ||
|
|
d7fbfb13af | ||
|
|
09e1f96e70 | ||
|
|
3e562d73cb | ||
|
|
6d8a8d0725 | ||
|
|
d914fc1cea | ||
|
|
fba3ed3037 | ||
|
|
874dac1ff2 | ||
|
|
33ed4a4a59 | ||
|
|
8b4981fcf4 | ||
|
|
428570d1b1 | ||
|
|
272a85fe0d | ||
|
|
d431915f8c | ||
|
|
7954454372 | ||
|
|
a9f06317f5 | ||
|
|
f55ed2d982 | ||
|
|
a80ee5daf9 | ||
|
|
79ff4b3088 | ||
|
|
9c41006cfa | ||
|
|
a4cfea8d94 | ||
|
|
921be81c80 |
223 changed files with 10138 additions and 888 deletions
53
.github/workflows/docker-image.yml
vendored
Normal file
53
.github/workflows/docker-image.yml
vendored
Normal 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 }}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
113
README.md
113
README.md
|
|
@ -2,9 +2,21 @@
|
|||
|
||||
# Hauk
|
||||
|
||||
[](https://github.com/bilde2910/Hauk/blob/master/LICENSE)
|
||||
[](https://github.com/bilde2910/Hauk/issues)
|
||||
[](https://traduki.varden.info/engage/hauk/)
|
||||
[](https://github.com/bilde2910/Hauk/stargazers)
|
||||
[](https://f-droid.org/packages/info.varden.hauk/)
|
||||
[](https://github.com/bilde2910/Hauk/releases)
|
||||

|
||||
[](https://hub.docker.com/r/bilde2910/hauk)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/packages/info.varden.hauk)
|
||||
[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"
|
||||
alt="Get it on Google Play"
|
||||
height="80">](https://play.google.com/store/apps/details?id=info.varden.hauk)
|
||||
|
||||
Hauk is a fully open source, self-hosted location sharing service. Install the
|
||||
backend code on a PHP-compatible web server, install the companion app on your
|
||||
|
|
@ -12,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
|
||||
|
|
@ -23,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
|
||||
|
|
@ -40,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
|
||||
|
|
@ -59,7 +90,7 @@ services:
|
|||
- ./config/hauk:/etc/hauk
|
||||
```
|
||||
|
||||
Copy the [config.php](https://github.com/bilde2910/Hauk/blob/master/backend-php/include/config.php) file to the ./config/hauk directory and customize it. Leave the memcached connection details as-is; memcached is included in the Docker image.
|
||||
Copy the [config.php](https://github.com/bilde2910/Hauk/blob/master/backend-php/include/config-sample.php) file to the ./config/hauk directory and customize it. Leave the memcached connection details as-is; memcached is included in the Docker image.
|
||||
|
||||
The Docker container exposes port 80. For security reasons, you should use a reverse proxy in front of Hauk that can handle TLS termination, and only expose Hauk via HTTPS. If you expose Hauk directly on port 80, or via a reverse proxy on port 80, anyone between the clients and server can intercept and read your location data.
|
||||
|
||||
|
|
@ -97,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:
|
||||
|
|
@ -105,3 +152,49 @@ Server: https://apps.varden.info/demo/hauk/
|
|||
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.
|
||||
|
||||
[](https://traduki.varden.info/engage/hauk/)
|
||||
|
||||
- **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
|
||||
|
||||
[](https://traduki.varden.info/engage/hauk/)
|
||||
|
||||
## Donate
|
||||
|
||||
Hauk is an ad-free, open source project, and I am not doing this for financial gain. Thus, my time spent making this is unpaid. I do however accept donations from anyone who appreciates my work enough that they feel inclined to compensate me, no matter the amount. Donations mean a lot to me, as they help cover costs associated with server upkeep, domains and hosting, and general cost of living, and they serve as an incentive for me to keep working on open-source projects.
|
||||
|
||||
If you wish to donate to me, you may check out my [donations page](https://varden.info/donate.php) on my website.
|
||||
|
|
|
|||
5
android/.idea/dictionaries/kruzah.xml
generated
5
android/.idea/dictionaries/kruzah.xml
generated
|
|
@ -5,9 +5,14 @@
|
|||
<w>backends</w>
|
||||
<w>gnss</w>
|
||||
<w>hauk</w>
|
||||
<w>huawei</w>
|
||||
<w>miui</w>
|
||||
<w>oneplus</w>
|
||||
<w>powerkeeper</w>
|
||||
<w>resumable</w>
|
||||
<w>sharedpref</w>
|
||||
<w>varden</w>
|
||||
<w>xiaomi</w>
|
||||
<w>xxxx</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
|
|
|
|||
14
android/.idea/gradle.xml
generated
14
android/.idea/gradle.xml
generated
|
|
@ -1,14 +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="resolveModulePerSourceSet" value="false" />
|
||||
<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>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
|
|
|||
|
|
@ -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
25
android/.idea/jarRepositories.xml
generated
Normal 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
10
android/.idea/misc.xml
generated
|
|
@ -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">
|
||||
|
|
|
|||
12
android/.idea/runConfigurations.xml
generated
12
android/.idea/runConfigurations.xml
generated
|
|
@ -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>
|
||||
1
android/app/.gitignore
vendored
1
android/app/.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
/build
|
||||
/release
|
||||
|
|
|
|||
|
|
@ -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 5
|
||||
versionName "1.2"
|
||||
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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.*;
|
||||
|
||||
|
|
@ -1,45 +1,84 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
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" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<receiver android:name=".notify.CopyLinkReceiver" android:exported="false">
|
||||
|
||||
<activity
|
||||
android:name=".global.ui.AuthorizationActivity"
|
||||
android:screenOrientation="portrait">
|
||||
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".global.Receiver"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="info.varden.hauk.START_ALONE_THEN_SHARE_VIA" />
|
||||
<action android:name="info.varden.hauk.START_ALONE_THEN_MAKE_TOAST" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<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">
|
||||
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -20,16 +23,38 @@ public enum Constants {
|
|||
// Shared preferences dictionaries.
|
||||
public static final String SHARED_PREFS_CONNECTION = "connectionPrefs";
|
||||
public static final String SHARED_PREFS_RESUMABLE = "sessionResumption";
|
||||
public static final String SHARED_PREFS_AUTHORIZATIONS = "broadcastAuthorizations";
|
||||
public static final String SHARED_PREFS_DEVICE_SPECS = "deviceSpecs";
|
||||
|
||||
// Keys for use in stored server preferences.
|
||||
public static final Preference<String> PREF_SERVER = new Preference.String("server", "");
|
||||
public static final Preference<String> PREF_PASSWORD = new Preference.String("password", "");
|
||||
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", "");
|
||||
@Deprecated // Use PREF_USERNAME_ENCRYPTED instead
|
||||
public static final Preference<String> PREF_USERNAME = new Preference.String("username", "");
|
||||
@Deprecated // Use PREF_PASSWORD_ENCRYPTED instead
|
||||
public static final Preference<String> PREF_PASSWORD = new Preference.String("password", "");
|
||||
|
||||
// Keys for use in session resumption preferences.
|
||||
public static final String RESUME_AVAILABLE = "canResume";
|
||||
|
|
@ -37,17 +62,37 @@ public enum Constants {
|
|||
public static final String RESUME_SESSION_PARAMS = "sessionParams";
|
||||
public static final String RESUME_SHARE_PARAMS = "shareParams";
|
||||
|
||||
// Keys for use in device spec preferences.
|
||||
public static final String DEVICE_PREF_WARNED_BATTERY_SAVINGS = "hasPromptedBatterySavings";
|
||||
|
||||
// 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";
|
||||
public static final String EXTRA_SESSION_SERVER_URL = "server";
|
||||
public static final String EXTRA_SESSION_USERNAME = "username";
|
||||
public static final String EXTRA_SESSION_PASSWORD = "password";
|
||||
public static final String EXTRA_SESSION_DURATION = "duration";
|
||||
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.
|
||||
public static final String INTENT_TYPE_COPY_LINK = "text/plain";
|
||||
|
||||
// Backend URLs.
|
||||
public static final String URL_PATH_ADOPT_SHARE = "api/adopt.php";
|
||||
|
|
@ -60,18 +105,23 @@ public enum Constants {
|
|||
public static final String PACKET_PARAM_ACCURACY = "acc";
|
||||
public static final String PACKET_PARAM_ADOPTABLE = "ado";
|
||||
public static final String PACKET_PARAM_DURATION = "dur";
|
||||
public static final String PACKET_PARAM_E2E_FLAG = "e2e";
|
||||
public static final String PACKET_PARAM_GROUP_PIN = "pin";
|
||||
public static final String PACKET_PARAM_ID_TO_ADOPT = "aid";
|
||||
public static final String PACKET_PARAM_INIT_VECTOR = "iv";
|
||||
public static final String PACKET_PARAM_INTERVAL = "int";
|
||||
public static final String PACKET_PARAM_LATITUDE = "lat";
|
||||
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";
|
||||
public static final String PACKET_PARAM_SHARE_MODE = "mod";
|
||||
public static final String PACKET_PARAM_SPEED = "spd";
|
||||
public static final String PACKET_PARAM_TIMESTAMP = "time";
|
||||
public static final String PACKET_PARAM_USERNAME = "usr";
|
||||
|
||||
// Packet OK response header. All valid packets start with this line.
|
||||
public static final String PACKET_RESPONSE_OK = "OK";
|
||||
|
|
@ -84,4 +134,14 @@ public enum Constants {
|
|||
|
||||
// Minimum backend version that sends the link ID as well as the view link itself.
|
||||
public static final Version VERSION_COMPAT_VIEW_ID = new Version("1.2");
|
||||
|
||||
// Minimum backend/frontend version that support end-to-end encryption.
|
||||
public static final Version VERSION_COMPAT_E2E_ENCRYPTION = new Version("1.5");
|
||||
|
||||
// End-to-end encryption specifications.
|
||||
public static final int E2E_AES_KEY_SIZE = 256;
|
||||
public static final int E2E_PBKDF2_ITERATIONS = 65536;
|
||||
public static final String E2E_KD_FUNCTION = "PBKDF2WithHmacSHA1";
|
||||
public static final String E2E_TRANSFORMATION = "AES/CBC/PKCS5Padding";
|
||||
public static final String E2E_KEY_SPEC = "AES";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,32 +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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -195,7 +253,7 @@ public final class DialogService {
|
|||
this.run = run;
|
||||
}
|
||||
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
public void onClick(DialogInterface dialogInterface, int which) {
|
||||
Log.v("Closing dialog, which=%s (unknown, run=%s)", which, this.run); //NON-NLS
|
||||
if (this.run != null) this.run.run();
|
||||
}
|
||||
|
|
@ -212,12 +270,30 @@ public final class DialogService {
|
|||
this.builder = builder;
|
||||
}
|
||||
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
public void onClick(DialogInterface dialogInterface, int which) {
|
||||
Log.d("Closing dialog, which=%s (positive)", which); //NON-NLS
|
||||
this.builder.onPositive();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
@ -230,7 +306,7 @@ public final class DialogService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
public void onClick(DialogInterface dialogInterface, int which) {
|
||||
Log.d("Closing dialog, which=%s (negative)", which); //NON-NLS
|
||||
this.builder.onNegative();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package info.varden.hauk.global;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import info.varden.hauk.R;
|
||||
import info.varden.hauk.manager.SessionManager;
|
||||
import info.varden.hauk.manager.StopSharingCallback;
|
||||
|
||||
/**
|
||||
* Session manager implementation for sessions created via the broadcast receiver.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class BroadcastSessionManager extends SessionManager {
|
||||
/**
|
||||
* Android application context.
|
||||
*/
|
||||
private final Context ctx;
|
||||
|
||||
BroadcastSessionManager(Context ctx) {
|
||||
super(ctx, new StopSharingCallbackImpl(ctx));
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void requestLocationPermission() {
|
||||
Toast.makeText(this.ctx, R.string.err_missing_perms, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link StopSharingCallback} for the broadcast receiver session manager.
|
||||
* Displays toast notifications when sharing stops.
|
||||
*/
|
||||
private static final class StopSharingCallbackImpl implements StopSharingCallback {
|
||||
private final Context ctx;
|
||||
|
||||
private StopSharingCallbackImpl(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
Toast.makeText(this.ctx, R.string.ended_message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShareNull() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception ex) {
|
||||
Toast.makeText(this.ctx, ex.getMessage(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
208
android/app/src/main/java/info/varden/hauk/global/Receiver.java
Normal file
208
android/app/src/main/java/info/varden/hauk/global/Receiver.java
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
package info.varden.hauk.global;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
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;
|
||||
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.TimeUtils;
|
||||
|
||||
/**
|
||||
* Public broadcast receiver for Hauk. Allows creation of new sessions from broadcasts. Broadcasts
|
||||
* sent to this receiver may declare the following extras:
|
||||
*
|
||||
* <p>For info.varden.hauk.START_ALONE_THEN_SHARE_VIA:</p>
|
||||
* <ul>
|
||||
* <li><b>source: </b><i>(required)</i>
|
||||
* An identifier for the broadcast source, e.g. package name of source app.</li>
|
||||
* <li><b>server: </b><i>(optional)</i>
|
||||
* The Hauk backend to connect to. Defaults to saved preference.</li>
|
||||
* <li><b>password: </b><i>(optional)</i>
|
||||
* The backend password. Defaults to saved preference.</li>
|
||||
* <li><b>duration: </b><i>(optional)</i>
|
||||
* Number of seconds to share for. Defaults to saved preference.</li>
|
||||
* <li><b>interval: </b><i>(optional)</i>
|
||||
* Number of seconds between each update. Defaults to saved preference.</li>
|
||||
* <li><b>adoptable: </b><i>(optional)</i>
|
||||
* True or false for allowing share adoption. Defaults to saved preference.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 1.3
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class Receiver extends BroadcastReceiver {
|
||||
@SuppressWarnings("HardCodedStringLiteral")
|
||||
private static final String ACTION_START_SHARING_ALONE_WITH_MENU = "info.varden.hauk.START_ALONE_THEN_SHARE_VIA";
|
||||
@SuppressWarnings("HardCodedStringLiteral")
|
||||
private static final String ACTION_START_SHARING_ALONE_WITH_TOAST = "info.varden.hauk.START_ALONE_THEN_MAKE_TOAST";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
// Ensure we have a valid broadcast.
|
||||
if (intent.getAction() == null) return;
|
||||
if (!intent.hasExtra(Constants.EXTRA_BROADCAST_AUTHORIZATION_IDENTIFIER)) return;
|
||||
|
||||
// Check that the broadcast is authorized.
|
||||
if (!checkAuthorization(context, intent)) return;
|
||||
|
||||
// Subsequent calls may result in data being read from preferences. We should ensure that
|
||||
// all deprecated preferences have been migrated before we continue.
|
||||
new DeprecationMigrator(context).migrate();
|
||||
|
||||
// Handle the broadcast appropriately.
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_START_SHARING_ALONE_WITH_MENU:
|
||||
startAloneThenShareVia(context, intent);
|
||||
break;
|
||||
case ACTION_START_SHARING_ALONE_WITH_TOAST:
|
||||
startAloneThenMakeToast(context, intent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not the given broadcast source is authorized to start sharing without user
|
||||
* interaction, and prompts the user if this status is unknown.
|
||||
*
|
||||
* @param ctx Android application context.
|
||||
* @param intent The broadcast intent.
|
||||
* @return true if handling should continue; false otherwise.
|
||||
*/
|
||||
private static boolean checkAuthorization(Context ctx, Intent intent) {
|
||||
// Check that the source is authorized.
|
||||
String identifier = intent.getStringExtra(Constants.EXTRA_BROADCAST_AUTHORIZATION_IDENTIFIER);
|
||||
SharedPreferences authPrefs = ctx.getSharedPreferences(Constants.SHARED_PREFS_AUTHORIZATIONS, Context.MODE_PRIVATE);
|
||||
if (!authPrefs.contains(identifier)) {
|
||||
// If not, show the authorization dialog and abort the session initiation.
|
||||
Intent authIntent = new Intent(ctx, AuthorizationActivity.class);
|
||||
authIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
authIntent.putExtra(Constants.EXTRA_BROADCAST_AUTHORIZATION_IDENTIFIER, identifier);
|
||||
ctx.startActivity(authIntent);
|
||||
return false;
|
||||
} else if (!authPrefs.getBoolean(identifier, false)) {
|
||||
// Denied sources are silently ignored.
|
||||
return false;
|
||||
} else {
|
||||
// Proceed with broadcast handling.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a single user sharing session and prompts the user to share the link.
|
||||
*
|
||||
* @param ctx Android application context.
|
||||
* @param intent The broadcast intent.
|
||||
*/
|
||||
private static void startAloneThenShareVia(Context ctx, Intent intent) {
|
||||
// Create session initiation parameters.
|
||||
PreferenceManager prefs = new PreferenceManager(ctx);
|
||||
SessionInitiationPacket.InitParameters initParams = buildSessionParams(intent, prefs);
|
||||
boolean adoptable = intent.hasExtra(Constants.EXTRA_SESSION_ALLOW_ADOPT) ? intent.getBooleanExtra(Constants.EXTRA_SESSION_ALLOW_ADOPT, true) : prefs.get(Constants.PREF_ALLOW_ADOPTION);
|
||||
|
||||
SessionManager manager = new BroadcastSessionManager(ctx);
|
||||
manager.attachShareListener(new DisplayShareDialogListener(ctx));
|
||||
manager.attachStatusListener(new GNSSStatusUpdateListenerImpl(ctx));
|
||||
|
||||
try {
|
||||
manager.shareLocation(initParams, new SessionInitiationResponseHandlerImpl(ctx), adoptable ? AdoptabilityPreference.ALLOW_ADOPTION : AdoptabilityPreference.DISALLOW_ADOPTION);
|
||||
} catch (LocationPermissionsNotGrantedException e) {
|
||||
Toast.makeText(ctx, R.string.err_missing_perms, Toast.LENGTH_LONG).show();
|
||||
} catch (LocationServicesDisabledException e) {
|
||||
Toast.makeText(ctx, R.string.err_location_disabled, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a single user sharing session and displays the sharing link in a toast notification.
|
||||
*
|
||||
* @param ctx Android application context.
|
||||
* @param intent The broadcast intent.
|
||||
*/
|
||||
private static void startAloneThenMakeToast(Context ctx, Intent intent) {
|
||||
// Require a custom link ID for this broadcast.
|
||||
if (!intent.hasExtra(Constants.EXTRA_SESSION_CUSTOM_ID)) return;
|
||||
|
||||
// Create session initiation parameters.
|
||||
PreferenceManager prefs = new PreferenceManager(ctx);
|
||||
SessionInitiationPacket.InitParameters initParams = buildSessionParams(intent, prefs);
|
||||
boolean adoptable = intent.hasExtra(Constants.EXTRA_SESSION_ALLOW_ADOPT) ? intent.getBooleanExtra(Constants.EXTRA_SESSION_ALLOW_ADOPT, true) : prefs.get(Constants.PREF_ALLOW_ADOPTION);
|
||||
|
||||
SessionManager manager = new BroadcastSessionManager(ctx);
|
||||
manager.attachShareListener(new ShareListenerImpl(ctx));
|
||||
manager.attachStatusListener(new GNSSStatusUpdateListenerImpl(ctx));
|
||||
|
||||
try {
|
||||
manager.shareLocation(initParams, new SessionInitiationResponseHandlerImpl(ctx), adoptable ? AdoptabilityPreference.ALLOW_ADOPTION : AdoptabilityPreference.DISALLOW_ADOPTION);
|
||||
} catch (LocationPermissionsNotGrantedException e) {
|
||||
Toast.makeText(ctx, R.string.err_missing_perms, Toast.LENGTH_LONG).show();
|
||||
} catch (LocationServicesDisabledException e) {
|
||||
Toast.makeText(ctx, R.string.err_location_disabled, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates session initiation parameters from broadcast intent data.
|
||||
*
|
||||
* @param intent The intent to extract data from.
|
||||
* @param fallback A preference manager to fetch default values from.
|
||||
* @return Session initiation parameters.
|
||||
*/
|
||||
private static SessionInitiationPacket.InitParameters buildSessionParams(Intent intent, PreferenceManager fallback) {
|
||||
String server = intent.hasExtra(Constants.EXTRA_SESSION_SERVER_URL) ? intent.getStringExtra(Constants.EXTRA_SESSION_SERVER_URL) : fallback.get(Constants.PREF_SERVER_ENCRYPTED);
|
||||
String username = intent.hasExtra(Constants.EXTRA_SESSION_USERNAME) ? intent.getStringExtra(Constants.EXTRA_SESSION_USERNAME) : fallback.get(Constants.PREF_USERNAME_ENCRYPTED);
|
||||
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 = "";
|
||||
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 + "/";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package info.varden.hauk.global.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import info.varden.hauk.Constants;
|
||||
import info.varden.hauk.R;
|
||||
|
||||
/**
|
||||
* Activity that is displayed to prompt the user to authorize a broadcast receiver source.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class AuthorizationActivity extends AppCompatActivity {
|
||||
/**
|
||||
* A string passed along with the broadcast intent to identify the source of the broadcast.
|
||||
*/
|
||||
private String identifier;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_authorize_broadcast);
|
||||
this.identifier = getIntent().getStringExtra(Constants.EXTRA_BROADCAST_AUTHORIZATION_IDENTIFIER);
|
||||
((TextView) findViewById(R.id.authorizeIdentifier)).setText(this.identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called if the user presses the Yes button.
|
||||
*/
|
||||
public void accept(@SuppressWarnings("unused") View view) {
|
||||
savePreference(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called if the user presses the No button.
|
||||
*/
|
||||
public void deny(@SuppressWarnings("unused") View view) {
|
||||
savePreference(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the user's preference on whether or not the given source should be approved and closes
|
||||
* the dialog.
|
||||
*
|
||||
* @param preference Whether or not the source was authorized.
|
||||
*/
|
||||
private void savePreference(boolean preference) {
|
||||
SharedPreferences prefs = getSharedPreferences(Constants.SHARED_PREFS_AUTHORIZATIONS, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean(this.identifier, preference);
|
||||
editor.apply();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package info.varden.hauk.global.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.widget.Toast;
|
||||
|
||||
import info.varden.hauk.Constants;
|
||||
import info.varden.hauk.R;
|
||||
import info.varden.hauk.manager.ShareListener;
|
||||
import info.varden.hauk.struct.Share;
|
||||
|
||||
/**
|
||||
* Share listener that opens a sharing dialog for the first sharing link received from the server.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class DisplayShareDialogListener implements ShareListener {
|
||||
/**
|
||||
* Android application context.
|
||||
*/
|
||||
private final Context ctx;
|
||||
|
||||
/**
|
||||
* Whether or not the sharing dialog should be displayed.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
public DisplayShareDialogListener(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShareJoined(Share share) {
|
||||
// Ensure that the sharing dialog is only displayed for the first link. The user could
|
||||
// otherwise end up having another sharing dialog displayed if the share is adopted and a
|
||||
// new sharing link is added that way.
|
||||
if (this.enabled) {
|
||||
this.enabled = false;
|
||||
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType(Constants.INTENT_TYPE_COPY_LINK);
|
||||
shareIntent.putExtra(Intent.EXTRA_SUBJECT, this.ctx.getString(R.string.share_subject));
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, share.getViewURL());
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(shareIntent, this.ctx.getString(R.string.share_via));
|
||||
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
this.ctx.startActivity(chooserIntent);
|
||||
|
||||
Toast.makeText(this.ctx, R.string.ok_message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShareParted(Share share) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package info.varden.hauk.global.ui.toast;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import info.varden.hauk.R;
|
||||
import info.varden.hauk.manager.GNSSStatusUpdateListener;
|
||||
|
||||
/**
|
||||
* GNSS status update listener that displays status updates in toast notifications. Used primarily
|
||||
* for starting shares via the broadcast receiver.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class GNSSStatusUpdateListenerImpl implements GNSSStatusUpdateListener {
|
||||
/**
|
||||
* Android application context.
|
||||
*/
|
||||
private final Context ctx;
|
||||
|
||||
public GNSSStatusUpdateListenerImpl(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShutdown() {
|
||||
Toast.makeText(this.ctx, R.string.label_status_none, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStarted() {
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package info.varden.hauk.global.ui.toast;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import info.varden.hauk.Constants;
|
||||
import info.varden.hauk.R;
|
||||
import info.varden.hauk.manager.SessionInitiationResponseHandler;
|
||||
import info.varden.hauk.struct.ShareMode;
|
||||
import info.varden.hauk.struct.Version;
|
||||
|
||||
/**
|
||||
* Session initiation response handler that shows the initiation status in toasts. Used primarily
|
||||
* for shares created via the broadcast receiver.
|
||||
*/
|
||||
public final class SessionInitiationResponseHandlerImpl implements SessionInitiationResponseHandler {
|
||||
/**
|
||||
* Android application context.
|
||||
*/
|
||||
private final Context ctx;
|
||||
|
||||
public SessionInitiationResponseHandlerImpl(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitiating() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShareModeForciblyDowngraded(ShareMode downgradeTo, Version backendVersion) {
|
||||
Toast.makeText(this.ctx, String.format(this.ctx.getString(R.string.err_ver_group), Constants.VERSION_COMPAT_GROUP_SHARE, backendVersion), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onE2EForciblyDisabled(Version backendVersion) {
|
||||
Toast.makeText(this.ctx, String.format(this.ctx.getString(R.string.err_ver_e2e), Constants.VERSION_COMPAT_E2E_ENCRYPTION, backendVersion), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception ex) {
|
||||
Toast.makeText(this.ctx, ex.getMessage(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package info.varden.hauk.global.ui.toast;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import info.varden.hauk.R;
|
||||
import info.varden.hauk.manager.ShareListener;
|
||||
import info.varden.hauk.struct.Share;
|
||||
|
||||
/**
|
||||
* Share listener that opens a toast with the first sharing link received from the server.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class ShareListenerImpl implements ShareListener {
|
||||
/**
|
||||
* Android application context.
|
||||
*/
|
||||
private final Context ctx;
|
||||
|
||||
/**
|
||||
* Whether or not the sharing dialog should be displayed.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
public ShareListenerImpl(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShareJoined(Share share) {
|
||||
// Ensure that the toast is only displayed for the first link. The user could otherwise end
|
||||
// up having multiple toasts displayed if the share is adopted and a new sharing link is
|
||||
// added that way.
|
||||
if (this.enabled) {
|
||||
this.enabled = false;
|
||||
Toast.makeText(this.ctx, String.format(this.ctx.getString(R.string.notify_body), share.getViewURL()), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShareParted(Share share) {
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
|
@ -10,17 +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.
|
||||
|
|
@ -28,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.
|
||||
|
|
@ -61,17 +66,38 @@ 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");
|
||||
client.setRequestProperty("User-Agent", "Hauk/" + BuildConfig.VERSION_NAME + " " + System.getProperty("http.agent"));
|
||||
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());
|
||||
|
|
@ -79,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
|
||||
|
|
@ -87,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);
|
||||
}
|
||||
}
|
||||
|
|
@ -120,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() {
|
||||
|
|
@ -142,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();
|
||||
|
|
@ -149,12 +187,28 @@ public class ConnectionThread extends AsyncTask<ConnectionThread.Request, String
|
|||
for (Map.Entry<String, String> entry : this.data.entrySet()) {
|
||||
if (first) first = false;
|
||||
else sb.append("&");
|
||||
sb.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.toString()));
|
||||
sb.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()));
|
||||
sb.append("=");
|
||||
sb.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.toString()));
|
||||
sb.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()));
|
||||
}
|
||||
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
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -194,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
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,11 +2,19 @@ package info.varden.hauk.http;
|
|||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.util.Base64;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
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;
|
||||
import info.varden.hauk.utils.TimeUtils;
|
||||
|
||||
/**
|
||||
|
|
@ -36,20 +44,45 @@ 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.
|
||||
*/
|
||||
public LocationUpdatePacket(Context ctx, Session session, Location location) {
|
||||
super(ctx, session.getServerURL(), Constants.URL_PATH_POST_LOCATION);
|
||||
setParameter(Constants.PACKET_PARAM_LATITUDE, String.valueOf(location.getLatitude()));
|
||||
setParameter(Constants.PACKET_PARAM_LONGITUDE, String.valueOf(location.getLongitude()));
|
||||
setParameter(Constants.PACKET_PARAM_TIMESTAMP, String.valueOf(System.currentTimeMillis() / (double) TimeUtils.MILLIS_PER_SECOND));
|
||||
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());
|
||||
|
||||
// Not all devices provide these parameters.
|
||||
if (location.hasSpeed()) setParameter(Constants.PACKET_PARAM_SPEED, String.valueOf(location.getSpeed()));
|
||||
if (location.hasAccuracy()) setParameter(Constants.PACKET_PARAM_ACCURACY, String.valueOf(location.getAccuracy()));
|
||||
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:
|
||||
if (location.hasSpeed()) setParameter(Constants.PACKET_PARAM_SPEED, String.valueOf(location.getSpeed()));
|
||||
if (location.hasAccuracy()) setParameter(Constants.PACKET_PARAM_ACCURACY, String.valueOf(location.getAccuracy()));
|
||||
} else {
|
||||
// We're using end-to-end encryption - generate an IV and encrypt all parameters.
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(Constants.E2E_TRANSFORMATION);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, session.getDerivableE2EKey().deriveSpec(), new SecureRandom());
|
||||
byte[] iv = cipher.getIV();
|
||||
setParameter(Constants.PACKET_PARAM_INIT_VECTOR, Base64.encodeToString(iv, Base64.DEFAULT));
|
||||
|
||||
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:
|
||||
if (location.hasSpeed()) setParameter(Constants.PACKET_PARAM_SPEED, Base64.encodeToString(cipher.doFinal(String.valueOf(location.getSpeed()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT));
|
||||
if (location.hasAccuracy()) setParameter(Constants.PACKET_PARAM_ACCURACY, Base64.encodeToString(cipher.doFinal(String.valueOf(location.getAccuracy()).getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT));
|
||||
} catch (Exception e) {
|
||||
Log.e("Error was thrown when encrypting location data", e); //NON-NLS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
package info.varden.hauk.http;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import info.varden.hauk.Constants;
|
||||
import info.varden.hauk.R;
|
||||
import info.varden.hauk.struct.AdoptabilityPreference;
|
||||
import info.varden.hauk.struct.KeyDerivable;
|
||||
import info.varden.hauk.struct.Session;
|
||||
import info.varden.hauk.struct.Share;
|
||||
import info.varden.hauk.struct.ShareMode;
|
||||
|
|
@ -26,13 +32,35 @@ public class SessionInitiationPacket extends Packet {
|
|||
*/
|
||||
private ShareMode mode;
|
||||
|
||||
/**
|
||||
* A salt used if the session is end-to-end encrypted.
|
||||
*/
|
||||
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) {
|
||||
setParameter(Constants.PACKET_PARAM_USERNAME, params.getUsername());
|
||||
}
|
||||
if (params.getCustomID() != null) {
|
||||
setParameter(Constants.PACKET_PARAM_SHARE_ID, params.getCustomID());
|
||||
}
|
||||
// Generate a random salt key derivation if using end-to-end encryption.
|
||||
if (params.getE2EPassword() != null) {
|
||||
SecureRandom rand = new SecureRandom();
|
||||
this.salt = new byte[Constants.E2E_AES_KEY_SIZE / 8];
|
||||
rand.nextBytes(this.salt);
|
||||
// The backend needs to know about the salt so the frontend can derive the key using it.
|
||||
setParameter(Constants.PACKET_PARAM_SALT, Base64.encodeToString(this.salt, Base64.DEFAULT));
|
||||
} else {
|
||||
this.salt = null;
|
||||
}
|
||||
setParameter(Constants.PACKET_PARAM_PASSWORD, params.getPassword());
|
||||
setParameter(Constants.PACKET_PARAM_DURATION, String.valueOf(params.getDuration()));
|
||||
setParameter(Constants.PACKET_PARAM_INTERVAL, String.valueOf(params.getInterval()));
|
||||
setParameter(Constants.PACKET_PARAM_E2E_FLAG, params.getE2EPassword() != null ? "1" : "0");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -91,6 +119,16 @@ public class SessionInitiationPacket extends Packet {
|
|||
}
|
||||
}
|
||||
|
||||
// Check if the server is out of date for end-to-end encryption, if applicable.
|
||||
KeyDerivable e2eParams = null;
|
||||
if (this.params.getE2EPassword() != null) {
|
||||
if (backendVersion.isAtLeast(Constants.VERSION_COMPAT_E2E_ENCRYPTION)) {
|
||||
e2eParams = new KeyDerivable(this.params.getE2EPassword(), this.salt);
|
||||
} else {
|
||||
this.handler.onE2EUnavailable(backendVersion);
|
||||
}
|
||||
}
|
||||
|
||||
// Somehow the data array can be empty? Check for this.
|
||||
if (data.length < 1) {
|
||||
throw new ServerException(getContext(), R.string.err_empty);
|
||||
|
|
@ -117,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());
|
||||
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);
|
||||
|
|
@ -161,6 +208,14 @@ public class SessionInitiationPacket extends Packet {
|
|||
* @param backendVersion The Hauk backend version.
|
||||
*/
|
||||
void onShareModeIncompatible(ShareMode downgradeTo, Version backendVersion);
|
||||
|
||||
/**
|
||||
* Called if end-to-end encryption was forcibly disabled because the backend/frontend
|
||||
* doesn't support this feature.
|
||||
*
|
||||
* @param backendVersion The Hauk backend version.
|
||||
*/
|
||||
void onE2EUnavailable(Version backendVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -169,29 +224,54 @@ public class SessionInitiationPacket extends Packet {
|
|||
*/
|
||||
public static final class InitParameters {
|
||||
private final String server;
|
||||
private final String username;
|
||||
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.
|
||||
*
|
||||
* @param server The full URL to the server to create a session on.
|
||||
* @param username The backend username, or empty string if not applicable.
|
||||
* @param password The backend password.
|
||||
* @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 password, int duration, int interval) {
|
||||
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;
|
||||
}
|
||||
|
||||
String getServerURL() {
|
||||
return this.server;
|
||||
}
|
||||
|
||||
public void setConnectionParameters(ConnectionParameters connParams) {
|
||||
this.connParams = connParams;
|
||||
}
|
||||
|
||||
ConnectionParameters getConnectionParameters() {
|
||||
return this.connParams;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
|
@ -203,5 +283,19 @@ public class SessionInitiationPacket extends Packet {
|
|||
int getInterval() {
|
||||
return this.interval;
|
||||
}
|
||||
|
||||
float getMinimumDistance() {
|
||||
return this.minDistance;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getCustomID() {
|
||||
return this.customID;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getE2EPassword() {
|
||||
return this.e2ePass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 + ">";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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() + "}";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -30,4 +30,13 @@ public interface SessionInitiationResponseHandler extends FailureHandler {
|
|||
* @param backendVersion The version of the Hauk backend server.
|
||||
*/
|
||||
void onShareModeForciblyDowngraded(ShareMode downgradeTo, Version backendVersion);
|
||||
|
||||
/**
|
||||
* Called if the session was successfully initiated, but the backend does not support end-to-end
|
||||
* encryption. {@code onSuccess()} will still be called after this callback in the event that
|
||||
* this happens.
|
||||
*
|
||||
* @param backendVersion The version of the Hauk backend server.
|
||||
*/
|
||||
void onE2EForciblyDisabled(Version backendVersion);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package info.varden.hauk.manager;
|
||||
|
||||
import info.varden.hauk.struct.Session;
|
||||
import info.varden.hauk.struct.Share;
|
||||
|
||||
/**
|
||||
* Callback interface that {@link SessionManager} handlers can attach to receive status updates
|
||||
|
|
@ -13,8 +14,10 @@ public interface SessionListener {
|
|||
* Called whenever a new session is created.
|
||||
*
|
||||
* @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);
|
||||
void onSessionCreated(Session session, Share share, SessionInitiationReason reason);
|
||||
|
||||
/**
|
||||
* Called if the session could not be initiated due to missing location permissions.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -226,6 +247,12 @@ public abstract class SessionManager {
|
|||
upstreamCallback.onShareModeForciblyDowngraded(downgradeTo, backendVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onE2EUnavailable(Version backendVersion) {
|
||||
Log.e("End-to-end encryption was requested but dropped because the server is out of date (backend=%s)", backendVersion); //NON-NLS
|
||||
upstreamCallback.onE2EForciblyDisabled(backendVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception ex) {
|
||||
Log.e("Share could not be initiated", ex); //NON-NLS
|
||||
|
|
@ -244,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
|
||||
|
|
@ -263,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
|
||||
|
|
@ -283,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
|
||||
|
|
@ -298,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
|
||||
|
|
@ -343,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);
|
||||
|
||||
|
|
@ -360,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
|
||||
|
|
@ -376,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.
|
||||
|
|
@ -387,7 +420,7 @@ public abstract class SessionManager {
|
|||
listener.onStarted();
|
||||
}
|
||||
for (SessionListener listener : this.upstreamSessionListeners) {
|
||||
listener.onSessionCreated(share.getSession());
|
||||
listener.onSessionCreated(share.getSession(), share, reason);
|
||||
}
|
||||
} else {
|
||||
Log.w("Location permission has not been granted; sharing will not commence"); //NON-NLS
|
||||
|
|
@ -429,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) {
|
||||
|
|
@ -443,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);
|
||||
|
|
@ -454,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();) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ public abstract class HaukBroadcastReceiver<T> extends BroadcastReceiver {
|
|||
// to the subclass.
|
||||
int index = intent.getIntExtra(Constants.EXTRA_BROADCAST_RECEIVER_REGISTRY_INDEX, -1);
|
||||
//noinspection unchecked
|
||||
T data = (T) ReceiverDataRegistry.retrieve(index);
|
||||
T data = (T) ReceiverDataRegistry.retrieve(index, true);
|
||||
Log.v("Received broadcast for class %s; fetched stored data of type %s; calling handler", getClass().getName(), data.getClass().getName()); //NON-NLS
|
||||
handle(context, data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,21 +49,60 @@ 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);
|
||||
|
||||
// Add "Copy link" and "Stop sharing" buttons to the notification.
|
||||
builder.addAction(R.drawable.ic_notify, getContext().getString(R.string.action_copy), new Receiver<>(getContext(), CopyLinkReceiver.class, this.share.getViewURL()).toPending());
|
||||
builder.addAction(R.drawable.ic_notify, getContext().getString(R.string.action_stop), new Receiver<>(getContext(), StopSharingReceiver.class, this.stopSharingTask).toPending());
|
||||
builder.addAction(R.drawable.ic_button_copy, getContext().getString(R.string.action_copy), new Receiver<>(getContext(), CopyLinkReceiver.class, this.share.getViewURL()).toPending());
|
||||
builder.addAction(R.drawable.ic_button_stop, getContext().getString(R.string.action_stop), new Receiver<>(getContext(), StopSharingReceiver.class, this.stopSharingTask).toPending());
|
||||
builder.setContentIntent(new ReopenIntent(getContext(), MainActivity.class).toPending());
|
||||
|
||||
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) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,47 +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;
|
||||
|
||||
Log.i("Requesting location updates from device location services"); //NON-NLS
|
||||
this.locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, this.share.getSession().getIntervalMillis(), 0.0F, this.listenCoarse);
|
||||
this.locMan.requestLocationUpdates(LocationManager.GPS_PROVIDER, this.share.getSession().getIntervalMillis(), 0.0F, this.listenFine);
|
||||
} else {
|
||||
Log.e("Location permission that was granted earlier has been rejected - sharing aborted"); //NON-NLS
|
||||
}
|
||||
|
|
@ -154,12 +145,20 @@ 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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -169,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
|
||||
|
|
@ -191,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package info.varden.hauk.struct;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import info.varden.hauk.Constants;
|
||||
import info.varden.hauk.utils.StringUtils;
|
||||
|
||||
/**
|
||||
* Serializable key spec that stores a password and salt for deriving a secret AES key spec.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class KeyDerivable implements Serializable {
|
||||
private static final long serialVersionUID = -4298542521894801298L;
|
||||
|
||||
/**
|
||||
* Salt used in PBKDF2 for key derivation.
|
||||
*/
|
||||
private final byte[] salt;
|
||||
|
||||
/**
|
||||
* End-to-end password to encrypt outgoing data with.
|
||||
*/
|
||||
@SuppressWarnings("FieldNotUsedInToString")
|
||||
private final String password;
|
||||
|
||||
/**
|
||||
* Secret key spec cache to improve performance for key derivation.
|
||||
*/
|
||||
@SuppressWarnings("FieldNotUsedInToString")
|
||||
private transient SecretKeySpec keySpec = null;
|
||||
|
||||
public KeyDerivable(String password, byte[] salt) {
|
||||
this.password = password;
|
||||
this.salt = salt.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a key spec from this derivable key.
|
||||
*
|
||||
* @return A secret key spec for use with encryption functions.
|
||||
* @throws InvalidKeySpecException if the key spec doesn't exist.
|
||||
* @throws NoSuchAlgorithmException if the algorithm doesn't exist.
|
||||
*/
|
||||
public SecretKeySpec deriveSpec() throws InvalidKeySpecException, NoSuchAlgorithmException {
|
||||
if (this.keySpec == null) {
|
||||
// E2E encryption is used, but the key spec hasn't been cached yet. Generate and cache
|
||||
// it, then return the spec.
|
||||
KeySpec ks = new PBEKeySpec(this.password.toCharArray(), this.salt, Constants.E2E_PBKDF2_ITERATIONS, Constants.E2E_AES_KEY_SIZE);
|
||||
SecretKeyFactory kf = SecretKeyFactory.getInstance(Constants.E2E_KD_FUNCTION);
|
||||
byte[] key = kf.generateSecret(ks).getEncoded();
|
||||
this.keySpec = new SecretKeySpec(key, Constants.E2E_KEY_SPEC);
|
||||
}
|
||||
return this.keySpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "KeyDerivable{password=<hidden>"
|
||||
+ ",salt=0x" + StringUtils.bytesToHex(this.salt)
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
package info.varden.hauk.struct;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.SimpleDateFormat;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
|
@ -14,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.
|
||||
*/
|
||||
|
|
@ -43,21 +51,37 @@ public final class Session implements Serializable {
|
|||
*/
|
||||
private final int interval;
|
||||
|
||||
public Session(String serverURL, Version backendVersion, String sessionID, long expiry, 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, 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
|
||||
+ ",interval=" + this.interval
|
||||
+ ",e2eParams=" + this.e2eParams
|
||||
+ "}";
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +89,10 @@ public final class Session implements Serializable {
|
|||
return this.serverURL;
|
||||
}
|
||||
|
||||
public ConnectionParameters getConnectionParameters() {
|
||||
return this.connParams;
|
||||
}
|
||||
|
||||
public Version getBackendVersion() {
|
||||
return this.backendVersion;
|
||||
}
|
||||
|
|
@ -90,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());
|
||||
}
|
||||
|
||||
|
|
@ -129,4 +157,16 @@ public final class Session implements Serializable {
|
|||
public long getIntervalMillis() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package info.varden.hauk.system.launcher;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Activity starter that launches an intent by an action name.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class ActionLauncher implements Launcher {
|
||||
/**
|
||||
* The name of the action to launch.
|
||||
*/
|
||||
private final String action;
|
||||
|
||||
public ActionLauncher(String action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(Context ctx) {
|
||||
Intent intent = new Intent(this.action);
|
||||
ctx.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ActionLauncher{"
|
||||
+ "action=" + this.action
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package info.varden.hauk.system.launcher;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Activity starter that launches an intent based on a component name.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class ComponentLauncher implements Launcher {
|
||||
/**
|
||||
* The package of the activity class to launch.
|
||||
*/
|
||||
private final String packageName;
|
||||
|
||||
/**
|
||||
* The activity class to launch.
|
||||
*/
|
||||
private final String className;
|
||||
|
||||
public ComponentLauncher(String packageName, String className) {
|
||||
this.packageName = packageName;
|
||||
this.className = className.startsWith(".") ? packageName + className : className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(Context ctx) {
|
||||
Intent intent = new Intent();
|
||||
intent.setComponent(new ComponentName(this.packageName, this.className));
|
||||
ctx.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ComponentLauncher{"
|
||||
+ "packageName=" + this.packageName
|
||||
+ ",className=" + this.className
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package info.varden.hauk.system.launcher;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Base launcher spec used to start an activity.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
@SuppressWarnings("ClassNamePrefixedWithPackageName")
|
||||
public interface Launcher {
|
||||
/**
|
||||
* Starts the activity.
|
||||
*
|
||||
* @param ctx Android application context.
|
||||
*/
|
||||
void launch(Context ctx);
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package info.varden.hauk.system.powersaving;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import info.varden.hauk.R;
|
||||
import info.varden.hauk.system.launcher.ActionLauncher;
|
||||
import info.varden.hauk.system.launcher.ComponentLauncher;
|
||||
import info.varden.hauk.system.launcher.Launcher;
|
||||
|
||||
/**
|
||||
* A list of power saving devices. These devices enforce aggressive power saving settings that
|
||||
* interfere with the operation of foreground services, causing Hauk to stop working. This list is
|
||||
* used to notify the user that this can happen, and prompts them to open settings to ensure Hauk is
|
||||
* whitelisted.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public enum Device {
|
||||
@SuppressWarnings({"HardCodedStringLiteral", "SpellCheckingInspection"})
|
||||
HUAWEI(1,
|
||||
R.string.manufacturer_huawei,
|
||||
Build.DISPLAY,
|
||||
Pattern.compile("^FRD-[A-Z0-9]+$"),
|
||||
new ComponentLauncher(
|
||||
"com.huawei.systemmanager",
|
||||
".optimize.process.ProtectActivity"
|
||||
)
|
||||
),
|
||||
@SuppressWarnings("HardCodedStringLiteral")
|
||||
ONEPLUS(2,
|
||||
R.string.manufacturer_oneplus,
|
||||
Build.DISPLAY,
|
||||
Pattern.compile("^ONEPLUS "),
|
||||
new ActionLauncher(
|
||||
"com.android.settings.action.BACKGROUND_OPTIMIZE"
|
||||
)
|
||||
),
|
||||
@SuppressWarnings("HardCodedStringLiteral")
|
||||
XIAOMI(3,
|
||||
R.string.manufacturer_xiaomi,
|
||||
Build.HOST,
|
||||
Pattern.compile("(-miui-)|(xiaomi)"),
|
||||
new ComponentLauncher(
|
||||
"com.miui.powerkeeper",
|
||||
".ui.HiddenAppsContainerManagementActivity"
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* A unique internal identifier for this device.
|
||||
*/
|
||||
private final int id;
|
||||
|
||||
/**
|
||||
* A string resource representing the name of the device manufacturer.
|
||||
*/
|
||||
private final int manufacturer;
|
||||
|
||||
/**
|
||||
* The build property against which matching should be performed.
|
||||
*/
|
||||
private final String buildProp;
|
||||
|
||||
/**
|
||||
* A regular expression matched against the build number of the device, used to identify the
|
||||
* device ROM.
|
||||
*/
|
||||
private final Pattern buildRegex;
|
||||
|
||||
/**
|
||||
* A launcher that opens system battery saving settings.
|
||||
*/
|
||||
private final Launcher launcher;
|
||||
|
||||
Device(int id, int manufacturer, String buildProp, Pattern buildRegex, Launcher launcher) {
|
||||
this.id = id;
|
||||
this.manufacturer = manufacturer;
|
||||
this.buildProp = buildProp;
|
||||
this.buildRegex = buildRegex;
|
||||
this.launcher = launcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not the given device matches the device the app is currently running on.
|
||||
*/
|
||||
public boolean matches() {
|
||||
return this.buildRegex.matcher(this.buildProp).find();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens system battery saving settings.
|
||||
*
|
||||
* @param ctx Android application context.
|
||||
*/
|
||||
public void launch(Context ctx) {
|
||||
this.launcher.launch(ctx);
|
||||
}
|
||||
|
||||
public int getID() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public int getManufacturerStringResource() {
|
||||
return this.manufacturer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Device{"
|
||||
+ "id=" + this.id
|
||||
+ ",manufacturer=" + this.manufacturer
|
||||
+ ",buildProp=" + this.buildProp
|
||||
+ ",pattern=" + this.buildRegex
|
||||
+ ",launchSpec=" + this.launcher
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package info.varden.hauk.system.powersaving;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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.utils.Log;
|
||||
|
||||
/**
|
||||
* A checker class that checks if the device Hauk is currently running on has system battery saving
|
||||
* settings that are so aggressive that they can interfere with Hauk's operation.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class DeviceChecker {
|
||||
/**
|
||||
* Android application context.
|
||||
*/
|
||||
private final Context ctx;
|
||||
|
||||
public DeviceChecker(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not the current device has aggressive battery savings, and shows a warning
|
||||
* dialog if this is the case.
|
||||
*/
|
||||
public void performCheck() {
|
||||
Log.i("Checking for aggressive battery savings"); //NON-NLS
|
||||
Device device = identifyDevice();
|
||||
if (device != null) {
|
||||
Log.i("Found device %s, checking for user prompt state", device); //NON-NLS
|
||||
|
||||
// The user should only be displayed the warning once, so we check if the user has already
|
||||
// acknowledged the warning in device spec settings.
|
||||
SharedPreferences prefs = this.ctx.getSharedPreferences(Constants.SHARED_PREFS_DEVICE_SPECS, Context.MODE_PRIVATE);
|
||||
if (prefs.getInt(Constants.DEVICE_PREF_WARNED_BATTERY_SAVINGS, -1) != device.getID()) {
|
||||
|
||||
// If not, show a warning to the user about their device's battery saving settings.
|
||||
Log.i("User has not been warned about this device previously, prompting"); //NON-NLS
|
||||
DialogService dialogSvc = new DialogService(this.ctx);
|
||||
dialogSvc.showDialog(
|
||||
R.string.battery_savings_title,
|
||||
String.format(this.ctx.getString(R.string.battery_savings_body), this.ctx.getString(device.getManufacturerStringResource())),
|
||||
Buttons.Two.SETTINGS_DISMISS,
|
||||
new WarningDialog(this.ctx, prefs, device)
|
||||
);
|
||||
} else {
|
||||
Log.i("User has been warned about this device previously, ignoring"); //NON-NLS
|
||||
}
|
||||
} else {
|
||||
Log.i("Device does not have aggressive battery saving functions"); //NON-NLS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to identify a device from the list of devices in {@link Device}.
|
||||
*
|
||||
* @return A device if one matches, null otherwise.
|
||||
*/
|
||||
@Nullable
|
||||
private static Device identifyDevice() {
|
||||
for (Device device : Device.values()) {
|
||||
Log.d("Checking device %s", device); //NON-NLS
|
||||
if (device.matches()) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package info.varden.hauk.system.powersaving;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import info.varden.hauk.Constants;
|
||||
import info.varden.hauk.dialog.CustomDialogBuilder;
|
||||
|
||||
/**
|
||||
* A dialog that prompts the user to open system settings to ignore power savings for Hauk.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class WarningDialog implements CustomDialogBuilder {
|
||||
/**
|
||||
* Android application context.
|
||||
*/
|
||||
private final Context ctx;
|
||||
|
||||
/**
|
||||
* Preference object to save warning acknowledgement state in.
|
||||
*/
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
/**
|
||||
* The power saving device that has been identified.
|
||||
*/
|
||||
private final Device device;
|
||||
|
||||
WarningDialog(Context ctx, SharedPreferences prefs, Device device) {
|
||||
this.ctx = ctx;
|
||||
this.prefs = prefs;
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the fact that the user has acknowledged the warning, so they are not warned again
|
||||
* later.
|
||||
*/
|
||||
private void saveAcknowledgement() {
|
||||
SharedPreferences.Editor editor = this.prefs.edit();
|
||||
editor.putInt(Constants.DEVICE_PREF_WARNED_BATTERY_SAVINGS, this.device.getID());
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositive() {
|
||||
// Dismiss button clicked.
|
||||
saveAcknowledgement();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNegative() {
|
||||
// Open settings button clicked.
|
||||
saveAcknowledgement();
|
||||
this.device.launch(this.ctx);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View createView(Context ctx) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
package info.varden.hauk.system.preferences;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
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
|
||||
* Hauk on the device.
|
||||
*
|
||||
* @param <T> The type of data to store in the preference.
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param prefs The shared preferences to retrieve the value from.
|
||||
*/
|
||||
abstract T get(SharedPreferences prefs);
|
||||
|
||||
/**
|
||||
* Sets the value of the preference to the given value in the preference object.
|
||||
*
|
||||
* @param prefs The shared preferences to write the value to.
|
||||
* @param value The value to write.
|
||||
*/
|
||||
abstract void set(SharedPreferences.Editor prefs, T value);
|
||||
|
||||
/**
|
||||
* Checks whether or not the preference exists in the given preference object.
|
||||
*
|
||||
* @param prefs The shared preferences to check for preference existence in.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
public static final class String extends Preference<java.lang.String> {
|
||||
private final java.lang.String key;
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
java.lang.String get(SharedPreferences prefs) {
|
||||
return prefs.getString(this.key, this.def);
|
||||
}
|
||||
|
||||
@Override
|
||||
void set(SharedPreferences.Editor prefs, java.lang.String value) {
|
||||
prefs.putString(this.key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isSensitive() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicateStringLiteralInspection")
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
return "Preference<String>{key=" + this.key + ",default=" + this.def + "}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an encrypted String-value preference.
|
||||
*/
|
||||
public static final class EncryptedString extends Preference<java.lang.String> {
|
||||
private final java.lang.String key;
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
java.lang.String get(SharedPreferences prefs) {
|
||||
if (!has(prefs)) return this.def;
|
||||
EncryptedData data = StringSerializer.deserialize(prefs.getString(this.key, null));
|
||||
try {
|
||||
return new KeyStoreHelper(KeyStoreAlias.PREFERENCES).decryptString(data);
|
||||
} catch (EncryptionException ex) {
|
||||
Log.e("Failed to retrieve preference %s due to a decryption error", ex, this.key); //NON-NLS
|
||||
return this.def;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void set(SharedPreferences.Editor prefs, java.lang.String value) {
|
||||
try {
|
||||
EncryptedData data = new KeyStoreHelper(KeyStoreAlias.PREFERENCES).encryptString(value);
|
||||
prefs.putString(this.key, StringSerializer.serialize(data));
|
||||
} catch (EncryptionException ex) {
|
||||
Log.e("Failed to store preference %s due to an encryption error", ex, this.key); //NON-NLS
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isSensitive() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicateStringLiteralInspection")
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
return "Preference<String+Encrypted>{key=" + this.key + ",default=" + this.def + "}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an Integer-value preference.
|
||||
*/
|
||||
public static final class Integer extends Preference<java.lang.Integer> {
|
||||
private final java.lang.String key;
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
java.lang.Integer get(SharedPreferences prefs) {
|
||||
return prefs.getInt(this.key, this.def);
|
||||
}
|
||||
|
||||
@Override
|
||||
void set(SharedPreferences.Editor prefs, java.lang.Integer value) {
|
||||
prefs.putInt(this.key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isSensitive() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicateStringLiteralInspection")
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
return "Preference<Integer>{key=" + this.key + ",default=" + this.def + "}";
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
public static final class Boolean extends Preference<java.lang.Boolean> {
|
||||
private final java.lang.String key;
|
||||
private final boolean def;
|
||||
|
||||
@SuppressWarnings("BooleanParameter")
|
||||
public Boolean(java.lang.String key, boolean def) {
|
||||
super(key, def, java.lang.Boolean.class);
|
||||
this.key = key;
|
||||
this.def = def;
|
||||
}
|
||||
|
||||
@Override
|
||||
java.lang.Boolean get(SharedPreferences prefs) {
|
||||
return prefs.getBoolean(this.key, this.def);
|
||||
}
|
||||
|
||||
@Override
|
||||
void set(SharedPreferences.Editor prefs, java.lang.Boolean value) {
|
||||
prefs.putBoolean(this.key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isSensitive() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicateStringLiteralInspection")
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
return "Preference<Boolean>{key=" + this.key + ",default=" + this.def + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -21,7 +22,7 @@ public final class PreferenceManager {
|
|||
* Returns the value of a preference from the saved app preferences.
|
||||
*
|
||||
* @param pair The preference to return the current value for.
|
||||
* @param <T> The type of preference to return.
|
||||
* @param <T> The type of preference to return.
|
||||
* @return The value of the preference, or its default if not defined.
|
||||
* @see Constants
|
||||
*/
|
||||
|
|
@ -33,15 +34,39 @@ public final class PreferenceManager {
|
|||
/**
|
||||
* Sets the value of a preference and saves it to device storage.
|
||||
*
|
||||
* @param pair The preference whose value to update.
|
||||
* @param pair The preference whose value to update.
|
||||
* @param value The value to save for the preference.
|
||||
* @param <T> The type of preference to set.
|
||||
* @param <T> The type of preference to set.
|
||||
* @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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a preference exists in device storage.
|
||||
*
|
||||
* @param pair The preference to check for existence of.
|
||||
* @param <T> The type of preference to check.
|
||||
* @return true if the preference exists, false otherwise.
|
||||
*/
|
||||
public <T> boolean has(Preference<T> pair) {
|
||||
return pair.has(this.prefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the given preference from device storage.
|
||||
*
|
||||
* @param pair The preference to clear.
|
||||
* @param <T> The type of preference to clear.
|
||||
*/
|
||||
public <T> void clear(Preference<T> pair) {
|
||||
Log.v("Clearing preference %s", pair); //NON-NLS
|
||||
SharedPreferences.Editor editor = this.prefs.edit();
|
||||
pair.clear(editor);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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() + "}";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package info.varden.hauk.system.security;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Structure that contains encrypted data along with an initialization vector.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class EncryptedData implements Serializable {
|
||||
private static final long serialVersionUID = -6247689274316948477L;
|
||||
|
||||
private final byte[] iv;
|
||||
private final byte[] data;
|
||||
|
||||
/**
|
||||
* Creates an encrypted data instance.
|
||||
*
|
||||
* @param iv An encryption initialization vector.
|
||||
* @param data Encrypted binary data.
|
||||
*/
|
||||
EncryptedData(byte[] iv, byte[] data) {
|
||||
this.iv = iv;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
byte[] getIV() {
|
||||
return this.iv.clone();
|
||||
}
|
||||
|
||||
byte[] getMessage() {
|
||||
return this.data.clone();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package info.varden.hauk.system.security;
|
||||
|
||||
/**
|
||||
* A wrapper exception that is thrown if errors happen during encryption or decryption in
|
||||
* {@link KeyStoreHelper}.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class EncryptionException extends Exception {
|
||||
private static final long serialVersionUID = 1413652344744489876L;
|
||||
|
||||
EncryptionException(Exception ex) {
|
||||
super(ex);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package info.varden.hauk.system.security;
|
||||
|
||||
/**
|
||||
* A list of pre-defined encryption aliases for use in encrypting data with {@link KeyStoreHelper}.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public enum KeyStoreAlias {
|
||||
/**
|
||||
* Key store alias for use in encrypting and decrypting shared preferences.
|
||||
*/
|
||||
@SuppressWarnings("HardCodedStringLiteral")
|
||||
PREFERENCES("sharedPrefs");
|
||||
|
||||
/**
|
||||
* The alias of the key in the key store.
|
||||
*/
|
||||
private final String alias;
|
||||
|
||||
KeyStoreAlias(String alias) {
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
String getAlias() {
|
||||
return this.alias;
|
||||
}
|
||||
|
||||
@SuppressWarnings("NullableProblems")
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.alias;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package info.varden.hauk.system.security;
|
||||
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyStore;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
|
||||
import info.varden.hauk.utils.Log;
|
||||
|
||||
/**
|
||||
* Helper class that interacts with the Android key store and offers simple methods for encrypting
|
||||
* and decrypting data.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class KeyStoreHelper {
|
||||
@SuppressWarnings("HardCodedStringLiteral")
|
||||
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
|
||||
@SuppressWarnings("HardCodedStringLiteral")
|
||||
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
||||
private static final int GCM_AUTH_TAG_LENGTH = 128;
|
||||
|
||||
private static KeyStore store = null;
|
||||
private SecretKey key = null;
|
||||
|
||||
/**
|
||||
* Retrieves a key store helper for the given key store alias.
|
||||
*
|
||||
* @param alias The alias to retrieve a helper for.
|
||||
*/
|
||||
public KeyStoreHelper(KeyStoreAlias alias) {
|
||||
try {
|
||||
// Load the key store if not already loaded.
|
||||
if (store == null) loadKeyStore();
|
||||
|
||||
// Check if the alias exists. If not, create it.
|
||||
if (!store.containsAlias(alias.getAlias())) {
|
||||
Log.i("Generating new key for alias %s", alias); //NON-NLS
|
||||
KeyGenerator keygen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
|
||||
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias.getAlias(), KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.build();
|
||||
keygen.init(spec);
|
||||
this.key = keygen.generateKey();
|
||||
} else {
|
||||
Log.i("Loading existing key for alias %s", alias); //NON-NLS
|
||||
KeyStore.SecretKeyEntry keyEntry = (KeyStore.SecretKeyEntry) store.getEntry(alias.getAlias(), null);
|
||||
this.key = keyEntry.getSecretKey();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("Unable to load key store or generate keys", e); //NON-NLS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the given data.
|
||||
*
|
||||
* @param data The data to encrypt.
|
||||
* @return The encrypted data and IV.
|
||||
* @throws EncryptionException if there was an error while encrypting.
|
||||
*/
|
||||
private EncryptedData encrypt(byte[] data) throws EncryptionException {
|
||||
Log.v("Encrypting data"); //NON-NLS
|
||||
|
||||
// Catch errors during initialization.
|
||||
if (this.key == null) throw new EncryptionException(new InvalidKeyException("Encryption key is null"));
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, this.key);
|
||||
byte[] iv = cipher.getIV();
|
||||
byte[] message = cipher.doFinal(data);
|
||||
return new EncryptedData(iv, message);
|
||||
} catch (Exception e) {
|
||||
throw new EncryptionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the given data.
|
||||
*
|
||||
* @param data The data to decrypt.
|
||||
* @return The cleartext data.
|
||||
* @throws EncryptionException if there was an error while decrypting.
|
||||
*/
|
||||
private byte[] decrypt(EncryptedData data) throws EncryptionException {
|
||||
Log.v("Decrypting data"); //NON-NLS
|
||||
|
||||
// Catch errors during initialization.
|
||||
if (this.key == null) throw new EncryptionException(new InvalidKeyException("Decryption key is null"));
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_AUTH_TAG_LENGTH, data.getIV());
|
||||
cipher.init(Cipher.DECRYPT_MODE, this.key, spec);
|
||||
return cipher.doFinal(data.getMessage());
|
||||
} catch (Exception e) {
|
||||
throw new EncryptionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the given string.
|
||||
*
|
||||
* @param data The string to encrypt.
|
||||
* @return The encrypted data and IV.
|
||||
* @throws EncryptionException if there was an error while encrypting.
|
||||
*/
|
||||
public EncryptedData encryptString(String data) throws EncryptionException {
|
||||
return encrypt(data.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the given string.
|
||||
*
|
||||
* @param data The string to decrypt.
|
||||
* @return The cleartext string.
|
||||
* @throws EncryptionException if there was an error while decrypting.
|
||||
*/
|
||||
public String decryptString(EncryptedData data) throws EncryptionException {
|
||||
return new String(decrypt(data), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the Android key store.
|
||||
*
|
||||
* @throws Exception if the loading failed.
|
||||
*/
|
||||
private static void loadKeyStore() throws Exception {
|
||||
store = KeyStore.getInstance(ANDROID_KEY_STORE);
|
||||
store.load(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,23 @@ package info.varden.hauk.ui;
|
|||
import android.Manifest;
|
||||
import android.app.ProgressDialog;
|
||||
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;
|
||||
|
|
@ -24,25 +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;
|
||||
|
||||
/**
|
||||
|
|
@ -93,19 +99,19 @@ public final class MainActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Ensure that all deprecated preferences have been migrated before we continue.
|
||||
new DeprecationMigrator(this).migrate();
|
||||
|
||||
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 password
|
||||
// immediately.
|
||||
((CompoundButton) findViewById(R.id.chkRemember)).setOnCheckedChangeListener(
|
||||
new RememberPasswordPreferenceChangedListener(this, (EditText) findViewById(R.id.txtPassword))
|
||||
);
|
||||
|
||||
// Add an event handler to the sharing mode selector.
|
||||
//noinspection OverlyStrongTypeCast
|
||||
((Spinner) findViewById(R.id.selMode)).setOnItemSelectedListener(new SelectionModeChangedListener(
|
||||
|
|
@ -115,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) {
|
||||
|
|
@ -122,11 +129,32 @@ 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)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Check for aggressive power saving devices and warn the user if applicable.
|
||||
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
|
||||
|
|
@ -135,15 +163,26 @@ public final class MainActivity extends AppCompatActivity {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -151,66 +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 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 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, server);
|
||||
prefs.set(Constants.PREF_DURATION, duration);
|
||||
prefs.set(Constants.PREF_INTERVAL, interval);
|
||||
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 password"); //NON-NLS
|
||||
prefs.set(Constants.PREF_REMEMBER_PASSWORD, true);
|
||||
prefs.set(Constants.PREF_PASSWORD, password);
|
||||
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, password, duration, interval);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,6 +280,13 @@ public final class MainActivity extends AppCompatActivity {
|
|||
this.dialogSvc.showDialog(R.string.explain_adopt_title, R.string.explain_adopt_body);
|
||||
}
|
||||
|
||||
/**
|
||||
* On-tap handler for the header logo and link that opens the Hauk project page on GitHub.
|
||||
*/
|
||||
public void openProjectSite(View view) {
|
||||
new OpenLinkListener(this, R.string.label_source_link).onClick(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called by onCreate() to initialize class-level variables for usage in this
|
||||
* activity.
|
||||
|
|
@ -240,10 +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.txtPassword),
|
||||
findViewById(R.id.txtDuration),
|
||||
findViewById(R.id.txtInterval),
|
||||
|
||||
findViewById(R.id.selUnit),
|
||||
findViewById(R.id.selMode),
|
||||
|
|
@ -271,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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -280,17 +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));
|
||||
((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.txtPassword)).setText(prefs.get(Constants.PREF_PASSWORD));
|
||||
((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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -335,6 +387,11 @@ public final class MainActivity extends AppCompatActivity {
|
|||
((Spinner) findViewById(R.id.selMode)).setSelection(downgradeTo.getIndex());
|
||||
MainActivity.this.dialogSvc.showDialog(R.string.err_outdated, String.format(getString(R.string.err_ver_group), Constants.VERSION_COMPAT_GROUP_SHARE, backendVersion));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onE2EForciblyDisabled(Version backendVersion) {
|
||||
MainActivity.this.dialogSvc.showDialog(R.string.err_outdated, String.format(getString(R.string.err_ver_e2e), Constants.VERSION_COMPAT_E2E_ENCRYPTION, backendVersion));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -416,7 +473,7 @@ public final class MainActivity extends AppCompatActivity {
|
|||
*/
|
||||
private final class SessionListenerImpl implements SessionListener {
|
||||
@Override
|
||||
public void onSessionCreated(Session session) {
|
||||
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();
|
||||
|
|
@ -425,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);
|
||||
|
|
@ -439,9 +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);
|
||||
// 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)));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View createView(Context ctx) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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_PASSWORD, isChecked ? this.passwordBox.getText().toString() : "");
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
|
||||
import info.varden.hauk.Constants;
|
||||
import info.varden.hauk.R;
|
||||
import info.varden.hauk.struct.Share;
|
||||
import info.varden.hauk.utils.Log;
|
||||
|
|
@ -35,8 +36,7 @@ public final class ShareLinkClickListener implements View.OnClickListener {
|
|||
public void onClick(View view) {
|
||||
Log.i("User requested to share %s", this.share); //NON-NLS
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
//noinspection HardCodedStringLiteral
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.setType(Constants.INTENT_TYPE_COPY_LINK);
|
||||
shareIntent.putExtra(Intent.EXTRA_SUBJECT, this.ctx.getString(R.string.share_subject));
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, this.share.getViewURL());
|
||||
this.ctx.startActivity(Intent.createChooser(shareIntent, this.ctx.getString(R.string.share_via)));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
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.
|
||||
*
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public final class DeprecationMigrator {
|
||||
/**
|
||||
* Android application context.
|
||||
*/
|
||||
private final Context ctx;
|
||||
|
||||
public DeprecationMigrator(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there are un-migrated settings, and migrates these.
|
||||
*/
|
||||
public void migrate() {
|
||||
PreferenceManager prefs = new PreferenceManager(this.ctx);
|
||||
if (prefs.has(Constants.PREF_SERVER)) {
|
||||
Log.i("Encrypting previously stored server"); //NON-NLS
|
||||
String server = prefs.get(Constants.PREF_SERVER);
|
||||
prefs.set(Constants.PREF_SERVER_ENCRYPTED, server);
|
||||
prefs.clear(Constants.PREF_SERVER);
|
||||
}
|
||||
if (prefs.has(Constants.PREF_USERNAME)) {
|
||||
Log.i("Encrypting previously stored username"); //NON-NLS
|
||||
String user = prefs.get(Constants.PREF_USERNAME);
|
||||
prefs.set(Constants.PREF_USERNAME_ENCRYPTED, user);
|
||||
prefs.clear(Constants.PREF_USERNAME);
|
||||
}
|
||||
if (prefs.has(Constants.PREF_PASSWORD)) {
|
||||
Log.i("Encrypting previously stored password"); //NON-NLS
|
||||
String pass = prefs.get(Constants.PREF_PASSWORD);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,12 @@
|
|||
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.
|
||||
*
|
||||
|
|
@ -11,10 +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() {
|
||||
return Thread.currentThread().getStackTrace()[STACK_DEPTH].toString();
|
||||
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 timestamp + ": " + caller + ": ";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -31,98 +44,98 @@ public enum Log {
|
|||
}
|
||||
|
||||
public static void e(String msg) {
|
||||
android.util.Log.e(getCaller(), msg);
|
||||
android.util.Log.e(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
|
||||
}
|
||||
|
||||
public static void e(String msg, Object... args) {
|
||||
android.util.Log.e(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(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(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(getCaller(), msg);
|
||||
android.util.Log.w(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
|
||||
}
|
||||
|
||||
public static void w(String msg, Object... args) {
|
||||
android.util.Log.w(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(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(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(getCaller(), msg);
|
||||
android.util.Log.i(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
|
||||
}
|
||||
|
||||
public static void i(String msg, Object... args) {
|
||||
android.util.Log.i(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(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(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(getCaller(), msg);
|
||||
android.util.Log.v(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
|
||||
}
|
||||
|
||||
public static void v(String msg, Object... args) {
|
||||
android.util.Log.v(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(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(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(getCaller(), msg);
|
||||
android.util.Log.d(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
|
||||
}
|
||||
|
||||
public static void d(String msg, Object... args) {
|
||||
android.util.Log.d(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(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(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(getCaller(), msg);
|
||||
android.util.Log.wtf(BuildConfig.APPLICATION_ID, getLogPrefix() + msg);
|
||||
}
|
||||
|
||||
public static void wtf(String msg, Object... args) {
|
||||
android.util.Log.wtf(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(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(getCaller(), String.format(msg, argsToStrings(args)), tr);
|
||||
android.util.Log.wtf(BuildConfig.APPLICATION_ID, getLogPrefix() + String.format(msg, argsToStrings(args)), tr);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
package info.varden.hauk.utils;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* Represents a preference key to default value mapping pair for use with storing preferences for
|
||||
* Hauk on the device.
|
||||
*
|
||||
* @param <T> The type of data to store in the preference.
|
||||
* @author Marius Lindvall
|
||||
*/
|
||||
public abstract class Preference<T> {
|
||||
|
||||
/**
|
||||
* Gets the value of the preference from the given preference object.
|
||||
*
|
||||
* @param prefs The shared preferences to retrieve the value from.
|
||||
*/
|
||||
abstract T get(SharedPreferences prefs);
|
||||
|
||||
/**
|
||||
* Sets the value of the preference to the given value in the preference object.
|
||||
*
|
||||
* @param prefs The shared preferences to write the value to.
|
||||
* @param value The value to write.
|
||||
*/
|
||||
abstract void set(SharedPreferences.Editor prefs, T value);
|
||||
|
||||
/**
|
||||
* Represents a String-value preference.
|
||||
*/
|
||||
public static final class String extends Preference<java.lang.String> {
|
||||
private final java.lang.String key;
|
||||
private final java.lang.String def;
|
||||
|
||||
public String(java.lang.String key, java.lang.String def) {
|
||||
this.key = key;
|
||||
this.def = def;
|
||||
}
|
||||
|
||||
@Override
|
||||
java.lang.String get(SharedPreferences prefs) {
|
||||
return prefs.getString(this.key, this.def);
|
||||
}
|
||||
|
||||
@Override
|
||||
void set(SharedPreferences.Editor prefs, java.lang.String value) {
|
||||
prefs.putString(this.key, value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicateStringLiteralInspection")
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
return "Preference<String>{key=" + this.key + ",default=" + this.def + "}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an Integer-value preference.
|
||||
*/
|
||||
public static final class Integer extends Preference<java.lang.Integer> {
|
||||
private final java.lang.String key;
|
||||
private final int def;
|
||||
|
||||
public Integer(java.lang.String key, int def) {
|
||||
this.key = key;
|
||||
this.def = def;
|
||||
}
|
||||
|
||||
@Override
|
||||
java.lang.Integer get(SharedPreferences prefs) {
|
||||
return prefs.getInt(this.key, this.def);
|
||||
}
|
||||
|
||||
@Override
|
||||
void set(SharedPreferences.Editor prefs, java.lang.Integer value) {
|
||||
prefs.putInt(this.key, value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicateStringLiteralInspection")
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
return "Preference<Integer>{key=" + this.key + ",default=" + this.def + "}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Boolean-value preference.
|
||||
*/
|
||||
public static final class Boolean extends Preference<java.lang.Boolean> {
|
||||
private final java.lang.String key;
|
||||
private final boolean def;
|
||||
|
||||
@SuppressWarnings("BooleanParameter")
|
||||
public Boolean(java.lang.String key, boolean def) {
|
||||
this.key = key;
|
||||
this.def = def;
|
||||
}
|
||||
|
||||
@Override
|
||||
java.lang.Boolean get(SharedPreferences prefs) {
|
||||
return prefs.getBoolean(this.key, this.def);
|
||||
}
|
||||
|
||||
@Override
|
||||
void set(SharedPreferences.Editor prefs, java.lang.Boolean value) {
|
||||
prefs.putBoolean(this.key, value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicateStringLiteralInspection")
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
return "Preference<Boolean>{key=" + this.key + ",default=" + this.def + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue