mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-06-30 12:49:55 +00:00
Compare commits
325 commits
Author | SHA1 | Date | |
---|---|---|---|
|
9d32a426f9 | ||
|
36bd8f945a | ||
|
ab9cdabddc | ||
|
d35c67d9d3 | ||
|
313b33793f | ||
|
94aabdcd88 | ||
|
d6b372bad5 | ||
|
af4e3d9566 | ||
|
8d667cd26c | ||
|
1335be6787 | ||
|
efaa6afda6 | ||
|
1c30557779 | ||
|
6f270144e2 | ||
|
62c0d273a5 | ||
|
3c4e8b44a0 | ||
|
63d2666230 | ||
|
6c40cfa748 | ||
|
6758ec498b | ||
|
819865230f | ||
|
97762fa146 | ||
|
60eea0bcb2 | ||
|
3818f9408d | ||
|
03c00d51ba | ||
|
afa1fbd3ae | ||
|
c81e08bf1f | ||
|
a5dc861336 | ||
|
fbfdd50069 | ||
|
19fe7bd4d2 | ||
|
7882ecc33a | ||
|
d39b44f0c3 | ||
|
7c6e3ae2a8 | ||
|
78ee38ba7d | ||
|
8ddf8c58da | ||
|
ce29d120a9 | ||
|
6bbb42fb83 | ||
|
e8d712ec71 | ||
|
ad2dc803fb | ||
|
3d50ab1b65 | ||
|
a4812c530d | ||
|
9ab949a59e | ||
|
e8bf7b0506 | ||
|
ec92fb2b31 | ||
|
919e6854e8 | ||
|
d98e23a1e5 | ||
|
e59df63e94 | ||
|
14643b4000 | ||
|
5439067e9f | ||
|
81a26ccad8 | ||
|
1fb36b0578 | ||
|
de74daef33 | ||
|
fe8b638818 | ||
|
05a415bb38 | ||
|
920df1d9be | ||
|
19a77209d8 | ||
|
aec16f22c1 | ||
|
a76f3394f3 | ||
|
6039cfa20e | ||
|
0eb1194578 | ||
|
170f626c9e | ||
|
c616a4f43c | ||
|
411a677fbf | ||
|
d48f2ead28 | ||
|
8147d07606 | ||
|
fa073371b5 | ||
|
3a9e27bacb | ||
|
51f656dd6b | ||
|
3efe74d375 | ||
|
f9ada47956 | ||
|
5213bafc97 | ||
|
503ce87c91 | ||
|
d2fcb24d79 | ||
|
337cb74f72 | ||
|
b5b29a4f84 | ||
|
3e3df919b2 | ||
|
79ba822ccf | ||
|
5f885cba29 | ||
|
5dba1db93d | ||
|
161b79f0d4 | ||
|
c250a17a19 | ||
|
843e5f1ab5 | ||
|
9d383b85d8 | ||
|
6d8eec0e21 | ||
|
fb8765f8f0 | ||
|
337d2c3507 | ||
|
8eabef2050 | ||
|
eb7b8881a0 | ||
|
b70654152d | ||
|
37ebcd3a4b | ||
|
9751a38ebd | ||
|
2ecde423a3 | ||
|
e8f06660dc | ||
|
c8d5be6462 | ||
|
bc29242f55 | ||
|
d395bbeb8d | ||
|
939fa0e1ec | ||
|
44358b3c95 | ||
|
413e793c7b | ||
|
e4c9a584f4 | ||
|
c9e8d4dbdf | ||
|
0573dbb2fc | ||
|
7753b482b1 | ||
|
cc5ce485b1 | ||
|
9131cae944 | ||
|
08d900c0c0 | ||
|
24d3d0ae8f | ||
|
45831e117c | ||
|
c559ed9e56 | ||
|
8e3279bb7e | ||
|
7ea2f5c4a5 | ||
|
9ef3315a70 | ||
|
4a9f189897 | ||
|
92de13b176 | ||
|
1e383463ae | ||
|
dbaec2d83f | ||
|
257a40eefa | ||
|
baa8068d51 | ||
|
d433957c2f | ||
|
6a54650635 | ||
|
bab59e8d04 | ||
|
83689a4c59 | ||
|
356fa8a36e | ||
|
58002c31ef | ||
|
d81d741fee | ||
|
17f106f70d | ||
|
91b632b9cf | ||
|
8b8e071831 | ||
|
9c151d83c1 | ||
|
e63ec4d1e8 | ||
|
f8603395fa | ||
|
df30e42318 | ||
|
6cb58789a2 | ||
|
eb6e26a8e4 | ||
|
8c1cc9a475 | ||
|
aab046ca04 | ||
|
d40e619cab | ||
|
0046e8827e | ||
|
5640b8be83 | ||
|
3bc3448b5c | ||
|
7472e32b25 | ||
|
3425256c29 | ||
|
b92956dece | ||
|
20c5236cfa | ||
|
7e1daf731f | ||
|
2add8aab14 | ||
|
99e633d61a | ||
|
7ce72e046a | ||
|
991da65af0 | ||
|
9eae773efb | ||
|
4ddc42ea51 | ||
|
a46c816167 | ||
|
71c0ad2a08 | ||
|
bc5cb488f5 | ||
|
010e2628e8 | ||
|
db4c738c8f | ||
|
e79c2c174b | ||
|
655881e852 | ||
|
29eccaf9cf | ||
|
f796e4542a | ||
|
a10693e79e | ||
|
b76e7a369c | ||
|
4ea19a2b7a | ||
|
6a67ca43e4 | ||
|
fc8cdc6502 | ||
|
27a723205e | ||
|
7d76be577d | ||
|
62f25d9ae9 | ||
|
9d374a2098 | ||
|
e53688d30d | ||
|
ec237ecb4e | ||
|
8960ffffb4 | ||
|
2e9efa0617 | ||
|
2050d29236 | ||
|
c1ffe4a23e | ||
|
52f8c709b5 | ||
|
676a7f603b | ||
|
94d1cc6608 | ||
|
f7862dcdf0 | ||
|
d1695aa712 | ||
|
f1ff402db4 | ||
|
fd2ae9423e | ||
|
46e1421c28 | ||
|
359621612a | ||
|
edf2201fb3 | ||
|
1201b505f7 | ||
|
327f97c51d | ||
|
29ebe31f8a | ||
|
372bbaa3fb | ||
|
2165ac4b2b | ||
|
46ceeeafb9 | ||
|
2b2c4fc0ce | ||
|
2864f9b30c | ||
|
c17f30b89e | ||
|
9c07b981d4 | ||
|
2b69dc3a84 | ||
|
892116fcd7 | ||
|
ee1dd322b8 | ||
|
56285eb468 | ||
|
7e8b86ecf5 | ||
|
ea09c67027 | ||
|
d6468420ee | ||
|
65a57f2b9c | ||
|
40f630170c | ||
|
2b04ae0622 | ||
|
c28548debb | ||
|
802d449bfb | ||
|
69126242bd | ||
|
dee881bc05 | ||
|
171da34b13 | ||
|
a582c2053c | ||
|
9b96bbde54 | ||
|
3c124deae1 | ||
|
559e68e0d2 | ||
|
06437132b5 | ||
|
8e9a1bda92 | ||
|
0d34f0749d | ||
|
f44fe389d7 | ||
|
1644b35e87 | ||
|
49a2b5d34d | ||
|
006815d36b | ||
|
ec617e0c94 | ||
|
3962d50fa6 | ||
|
9815e510df | ||
|
f9f37d30b2 | ||
|
4c4acf0cd0 | ||
|
3a66851df5 | ||
|
4311bd9bd8 | ||
|
09c789b250 | ||
|
60c72d48ee | ||
|
8001ecb482 | ||
|
8912d75870 | ||
|
e70499bcff | ||
|
4c28bf2a12 | ||
|
584b0acb2b | ||
|
45ced0de60 | ||
|
7ec786231e | ||
|
9737c85f86 | ||
|
bf7c60d620 | ||
|
6fc9cd5a71 | ||
|
9bae4d6bbc | ||
|
7ce43a0afd | ||
|
2b2cac1ada | ||
|
2f18907ce1 | ||
|
ec01a4a96d | ||
|
76f6ebd216 | ||
|
f1e14e6645 | ||
|
52ecf12576 | ||
|
dfd720b406 | ||
|
8995626d16 | ||
|
cffe15735c | ||
|
8512986a6b | ||
|
c5b8ee9215 | ||
|
2e44a81c69 | ||
|
f76d84ef87 | ||
|
3d59114230 | ||
|
22c9ab7c03 | ||
|
f8ad3d16fc | ||
|
fbd3bf3ff5 | ||
|
8bbbe3611a | ||
|
2d0e201060 | ||
|
b59350337f | ||
|
6d73e5101c | ||
|
d16d56c4b0 | ||
|
59bae27556 | ||
|
fcde086ae3 | ||
|
0e2fa929e6 | ||
|
8951c19581 | ||
|
32e462bdce | ||
|
6bd8521661 | ||
|
f7bac4331e | ||
|
243a52ebed | ||
|
f91b6f0466 | ||
|
57ec695718 | ||
|
224ec2553c | ||
|
5acacf63e1 | ||
|
bfbb3ef2c4 | ||
|
f1c9c6c5fc | ||
|
566bcac3e0 | ||
|
4d729d1bef | ||
|
1acb9db489 | ||
|
98bcdc7615 | ||
|
7c1a954e4d | ||
|
a1d00b47fe | ||
|
ca530f229b | ||
|
b86bb286e8 | ||
|
52abb08201 | ||
|
ff233090f8 | ||
|
adaae9e6d6 | ||
|
3dd70de5df | ||
|
da2244f511 | ||
|
08c73922cc | ||
|
88caafd61c | ||
|
45220241aa | ||
|
60e93559c3 | ||
|
e1f4696115 | ||
|
1c86c5fd51 | ||
|
92e9e047a7 | ||
|
c13d4e7f8d | ||
|
d09e81232a | ||
|
03f1a0e8ab | ||
|
305e157fc5 | ||
|
5c86e5c099 | ||
|
0760bfc618 | ||
|
9414b5c420 | ||
|
e7a1058618 | ||
|
1a6f85ccb6 | ||
|
b84ecf15da | ||
|
31b8162ab4 | ||
|
7def7eb4f7 | ||
|
8ca45d2322 | ||
|
1c1dee560c | ||
|
ca4a3e2f74 | ||
|
37964da4a5 | ||
|
dd9c307dea | ||
|
c65ecd9c54 | ||
|
72511fc02b | ||
|
79ade74c0c | ||
|
8164e91dd0 | ||
|
1ccbe88ce6 | ||
|
9b3e7136bd | ||
|
9cabd9f309 | ||
|
27e56d60b5 | ||
|
aac77442bf | ||
|
c9cf6729e0 | ||
|
b916697391 | ||
|
b205438982 |
473 changed files with 24075 additions and 6928 deletions
|
@ -1,10 +0,0 @@
|
|||
project_id: "372633"
|
||||
api_token: "<api-token-here>"
|
||||
base_path: "../app/src/main"
|
||||
base_url: "https://api.crowdin.com"
|
||||
preserve_hierarchy: true
|
||||
|
||||
files:
|
||||
- source: "res/values/strings.xml"
|
||||
dest: "strings.xml"
|
||||
translation: "res/values-%android_code%/%original_file_name%"
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1,4 +1,4 @@
|
|||
buy_me_a_coffee: beemdevelopment
|
||||
custom:
|
||||
- "https://www.buymeacoffee.com/beemdevelopment"
|
||||
- "https://www.blockchain.com/btc/address/bc1q26kyxqjkc6tu477pzy0whagwhs4ypv93qls22n"
|
||||
- "https://nanocrawler.cc/explorer/account/nano_1aegisc559b1x4p3839egnu579jkd4htpidy14eo9e31gzqmwuafypnj4q94"
|
||||
|
|
29
.github/workflows/build-app-workflow.yaml
vendored
29
.github/workflows/build-app-workflow.yaml
vendored
|
@ -5,36 +5,37 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/wrapper-validation-action@55e685c48d84285a5b0418cd094606e199cca3b6
|
||||
- uses: actions/setup-java@v3
|
||||
uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: 'gradle'
|
||||
- name: Build the app
|
||||
run: ./gradlew build
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: apk
|
||||
path: app/build/outputs/apk/debug/app-debug.apk
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
# This is probably pretty expensive for GitHub, so restrict the repositories that this job runs on
|
||||
if: github.repository == 'beemdevelopment/Aegis' || github.repository == 'alexbakker/Aegis'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: 'gradle'
|
||||
- name: Install HAXM
|
||||
run: brew install --cask intel-haxm
|
||||
- name: Enable KVM group perms
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
- name: Tests
|
||||
uses: reactivecircus/android-emulator-runner@50986b1464923454c95e261820bc626f38490ec0
|
||||
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d
|
||||
with:
|
||||
api-level: 31
|
||||
arch: x86_64
|
||||
|
@ -52,8 +53,8 @@ jobs:
|
|||
adb logcat -d > artifacts/logcat.txt
|
||||
cp -r app/build/reports/androidTests/connected/* artifacts/report/
|
||||
if adb shell '[ -e /sdcard/Pictures/screenshots ]'; then adb pull /sdcard/Pictures/screenshots artifacts/; fi
|
||||
# test ! -f tests_failing
|
||||
- uses: actions/upload-artifact@v3
|
||||
test ! -f tests_failing
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: instrumented-test-report
|
||||
|
|
9
.github/workflows/codeql-analysis.yml
vendored
9
.github/workflows/codeql-analysis.yml
vendored
|
@ -14,9 +14,10 @@ jobs:
|
|||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
if: github.event_name != 'schedule' || github.repository == 'beemdevelopment/Aegis'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Exclude paths
|
||||
# The importers are excluded from analysis, because some of the apps Aegis
|
||||
# can import from don't have such great crypto, which will cause false
|
||||
|
@ -24,18 +25,18 @@ jobs:
|
|||
run: |
|
||||
find app/src/main/java/com/beemdevelopment/aegis/importers ! \( -name AegisImporter.java -o -name "DatabaseImporter*" \) -type f -exec rm -f {} +
|
||||
sed -i '/Importer.class/d' app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
|
||||
- uses: actions/setup-java@v3
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: 'gradle'
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: java
|
||||
- name: Build
|
||||
run: ./gradlew assembleDebug
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
|
16
.github/workflows/crowdin.yml
vendored
16
.github/workflows/crowdin.yml
vendored
|
@ -10,22 +10,16 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'beemdevelopment/Aegis'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install crowdin-cli
|
||||
run: |
|
||||
wget https://github.com/crowdin/crowdin-cli/releases/download/3.7.2/crowdin-cli.zip
|
||||
echo "ee9f838b819ccedc33c9b2537055e5ba7d7934561b24df1e1a6274cbd6e27f2d crowdin-cli.zip" | sha256sum -c
|
||||
wget https://github.com/crowdin/crowdin-cli/releases/download/4.6.1/crowdin-cli.zip
|
||||
echo "7afd70de3a747ac631a5bad7866008163ae1d50c4606b5773f0b90a5481ffde2 crowdin-cli.zip" | sha256sum -c
|
||||
unzip crowdin-cli.zip -d crowdin-cli
|
||||
- name: Upload to Crowdin
|
||||
env:
|
||||
CROWDIN_TOKEN: "${{ secrets.CROWDIN_TOKEN }}"
|
||||
CROWDIN_PERSONAL_TOKEN: "${{ secrets.CROWDIN_TOKEN }}"
|
||||
run: |
|
||||
java -jar ./crowdin-cli/3.7.2/crowdin-cli.jar upload sources \
|
||||
java -jar ./crowdin-cli/4.6.1/crowdin-cli.jar upload sources \
|
||||
--no-progress \
|
||||
--token "$CROWDIN_TOKEN" \
|
||||
--project-id 372633 \
|
||||
--base-path app/src/main \
|
||||
--source res/values/strings.xml \
|
||||
--translation "res/values-%android_code%/%original_file_name%" \
|
||||
--dest strings.xml \
|
||||
--branch master
|
||||
|
|
8
FAQ.md
8
FAQ.md
|
@ -86,6 +86,14 @@ Another common setup is to configure Aegis to back up to a folder on local
|
|||
storage of your device and then have a separate app (like
|
||||
[Syncthing](https://syncthing.net/)) sync that folder anywhere you want.
|
||||
|
||||
## Encrypted Backups
|
||||
|
||||
### Why do I not get prompted to enter an encryption password when exporting?
|
||||
|
||||
Aegis uses the same password you have configured to encrypt your vault as the
|
||||
password which is used when exporting and importing your vault; so when prompted,
|
||||
you will enter that when importing your vault.
|
||||
|
||||
## Importing
|
||||
|
||||
### When importing from Authenticator Plus, an error is shown claiming that Accounts.txt is missing
|
||||
|
|
31
README.md
31
README.md
|
@ -123,20 +123,33 @@ documentation](docs/iconpacks.md).
|
|||
Unofficial monochrome-styled 2FA icons.
|
||||
|
||||
[<img width=500 alt="aegis-icons preview"
|
||||
src="https://raw.githubusercontent.com/aegis-icons/aegis-icons/master/showcase.png">](https://github.com/aegis-icons/aegis-icons)
|
||||
src="metadata/en-US/images/iconPacks/aegis-icons.png">](https://github.com/aegis-icons/aegis-icons)
|
||||
|
||||
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons) *
|
||||
- [delta-aegis-icons](https://github.com/Delta-Icons/aegis-icons)
|
||||
|
||||
Delta version of the unofficial monochrome-styled 2FA icon pack aegis-icons.
|
||||
|
||||
[<img width=500 alt="delta-icons preview"
|
||||
src="metadata/en-US/images/iconPacks/delta-icons.png">](https://github.com/Delta-Icons/aegis-icons)
|
||||
|
||||
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons) \*
|
||||
|
||||
This project periodically generates an icon pack for Aegis based on [Simple
|
||||
Icons](https://simpleicons.org/).
|
||||
|
||||
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) *
|
||||
[<img width=500 alt="aegis-simple-icons preview"
|
||||
src="metadata/en-US/images/iconPacks/aegis-simple-icons.png">](https://github.com/alexbakker/aegis-simple-icons)
|
||||
|
||||
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) \*
|
||||
|
||||
This is a variant on the aegis-simple-icons pack where the icons contain no solid background and just the outlines are being used.
|
||||
|
||||
|
||||
[<img width=500 alt="aegis-simple-icons-outlined preview"
|
||||
src="metadata/en-US/images/iconPacks/aegis-simple-icons-outlined.png">](https://github.com/michaelschattgen/aegis-simple-icons-outlined)
|
||||
|
||||
\* The icons are automatically generated, so
|
||||
not all of them are as high quality as the ones you'll find in
|
||||
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
|
||||
not all of them are as high quality as the ones you'll find in
|
||||
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@ -151,3 +164,9 @@ Swing by our Matrix room to interact with other contributors:
|
|||
|
||||
This project is licensed under the GNU General Public License v3.0. See the
|
||||
[LICENSE](LICENSE) file for details.
|
||||
|
||||
A couple of libraries vendored in Aegis' repository are licensed under a
|
||||
different license:
|
||||
|
||||
- [TextDrawable](app/src/main/java/com/amulyakhare/textdrawable)
|
||||
- [TrustedIntents](app/src/main/java/info/guardianproject/trustedintents)
|
||||
|
|
138
app/build.gradle
138
app/build.gradle
|
@ -1,6 +1,7 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
apply plugin: 'dagger.hilt.android.plugin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
|
||||
def getCmdOutput = { cmd ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
|
@ -19,21 +20,27 @@ def fileProviderAuthority = "${packageName}.fileprovider"
|
|||
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
|
||||
|
||||
android {
|
||||
compileSdk 33
|
||||
compileSdk 35
|
||||
|
||||
namespace packageName
|
||||
|
||||
defaultConfig {
|
||||
applicationId "${packageName}"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 60
|
||||
versionName "2.2"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 35
|
||||
versionCode 79
|
||||
versionName "3.4"
|
||||
multiDexEnabled true
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
|
||||
buildConfigField "java.util.concurrent.atomic.AtomicBoolean", "TEST", "new java.util.concurrent.atomic.AtomicBoolean(false)"
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.schemaLocation": "$projectDir/schemas"]
|
||||
}
|
||||
}
|
||||
|
||||
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
@ -86,28 +93,41 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
// Required to make the APK reproducible
|
||||
aaptOptions {
|
||||
cruncherEnabled = false
|
||||
}
|
||||
defaultConfig {
|
||||
vectorDrawables.generatedDensities = []
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
// R8 doesn't remove these resources, so exclude them manually. This reduces APK size by 4MB.
|
||||
resources {
|
||||
excludes += ['/org/bouncycastle/pqc/**/*.properties']
|
||||
excludes += [
|
||||
'/org/bouncycastle/pqc/**/*.properties',
|
||||
'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility 1.8
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
lint {
|
||||
abortOnError true
|
||||
checkDependencies true
|
||||
disable 'MissingQuantity', 'MissingTranslation'
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.8.0'
|
||||
artifact = 'com.google.protobuf:protoc:3.25.1'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
|
@ -120,37 +140,46 @@ protobuf {
|
|||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def cameraxVersion = '1.2.3'
|
||||
def glideVersion = '4.16.0'
|
||||
def guavaVersion = '32.1.2'
|
||||
def hiltVersion = '2.47'
|
||||
def junitVersion = '4.13.2'
|
||||
def libsuVersion = '5.2.0'
|
||||
aboutLibraries {
|
||||
// Tasks for aboutLibraries are not run automatically to keep the build reproducible
|
||||
// To update manually: ./gradlew app:exportLibraryDefinitions -PaboutLibraries.exportPath=src/main/res/raw
|
||||
prettyPrint = true
|
||||
configPath = "app/config"
|
||||
fetchRemoteFunding = false
|
||||
registerAndroidTasks = false
|
||||
exclusionPatterns = [~"javax.annotation.*"]
|
||||
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||
}
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.6.0'
|
||||
dependencies {
|
||||
def cameraxVersion = '1.4.2'
|
||||
def glideVersion = '4.16.0'
|
||||
def guavaVersion = '33.4.8'
|
||||
def hiltVersion = '2.56.2'
|
||||
def junitVersion = '4.13.2'
|
||||
def libsuVersion = '6.0.0'
|
||||
def roomVersion = '2.7.1'
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.9.1'
|
||||
annotationProcessor "androidx.room:room-compiler:$roomVersion"
|
||||
annotationProcessor "com.google.dagger:hilt-compiler:$hiltVersion"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
||||
|
||||
// Ridiculous fix for a bunch of "Duplicate class" build errors:
|
||||
implementation (platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.activity:activity:1.7.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.activity:activity:1.10.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.camera:camera-camera2:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
|
||||
implementation 'androidx.camera:camera-view:1.2.3'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation "androidx.core:core:1.10.1"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.6.1"
|
||||
implementation "androidx.camera:camera-view:$cameraxVersion"
|
||||
implementation 'androidx.core:core:1.16.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation 'androidx.documentfile:documentfile:1.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.9.0'
|
||||
implementation "androidx.preference:preference:1.2.1"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.1'
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.4.0'
|
||||
implementation "androidx.room:room-runtime:$roomVersion"
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation "com.google.dagger:hilt-android:$hiltVersion"
|
||||
implementation 'com.github.avito-tech:krop:0.52'
|
||||
|
@ -162,36 +191,35 @@ dependencies {
|
|||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
implementation "com.google.guava:guava:${guavaVersion}-android"
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.22.0'
|
||||
implementation 'com.google.zxing:core:3.5.2'
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
|
||||
implementation 'com.nulab-inc:zxcvbn:1.8.2'
|
||||
implementation 'de.hdodenhof:circleimageview:3.1.0'
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.2.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:4.31.0'
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
implementation('com.mikepenz:aboutlibraries:11.2.3') {
|
||||
exclude group: 'com.mikepenz', module: 'aboutlibraries-core'
|
||||
}
|
||||
implementation 'com.mikepenz:aboutlibraries-core-android:11.2.3'
|
||||
implementation 'com.nulab-inc:zxcvbn:1.9.0'
|
||||
implementation 'net.lingala.zip4j:zip4j:2.11.5'
|
||||
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
|
||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.76'
|
||||
implementation "org.simpleflatmapper:sfm-csv:8.2.3"
|
||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.80'
|
||||
implementation 'org.simpleflatmapper:sfm-csv:8.2.3'
|
||||
|
||||
androidTestAnnotationProcessor "com.google.dagger:hilt-android-compiler:$hiltVersion"
|
||||
androidTestImplementation "com.google.dagger:hilt-android-testing:$hiltVersion"
|
||||
androidTestImplementation 'androidx.test:core:1.5.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
|
||||
androidTestImplementation 'androidx.test:core:1.6.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.6.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
|
||||
androidTestImplementation "junit:junit:${junitVersion}"
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.2'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.5.1'
|
||||
|
||||
testImplementation 'androidx.test:core:1.5.0'
|
||||
testImplementation 'androidx.test:core:1.6.1'
|
||||
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
|
||||
testImplementation "junit:junit:${junitVersion}"
|
||||
testImplementation 'org.json:json:20230618'
|
||||
testImplementation 'org.robolectric:robolectric:4.10.3'
|
||||
testImplementation 'org.json:json:20250517'
|
||||
testImplementation 'org.robolectric:robolectric:4.14.1'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||
}
|
||||
|
|
6
app/config/libraries/krop.json
Normal file
6
app/config/libraries/krop.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"uniqueId": "com.github.avito-tech:krop",
|
||||
"licenses": [
|
||||
"MIT"
|
||||
]
|
||||
}
|
6
app/config/libraries/libsu.json
Normal file
6
app/config/libraries/libsu.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"uniqueId": "com.github.topjohnwu.libsu:.*::regex",
|
||||
"licenses": [
|
||||
"Apache-2.0"
|
||||
]
|
||||
}
|
15
app/config/libraries/textdrawable.json
Normal file
15
app/config/libraries/textdrawable.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"uniqueId": "com.amulyakhare:com.amulyakhare.textdrawable",
|
||||
"funding": [
|
||||
|
||||
],
|
||||
"developers": [
|
||||
|
||||
],
|
||||
"artifactVersion": "1.0.1",
|
||||
"description": "This light-weight library provides images with letter/text like the Gmail app. It extends the Drawable class thus can be used with existing/custom/network ImageView classes. Also included is a fluent interface for creating drawables and a customizable ColorGenerator.",
|
||||
"name": "textdrawable",
|
||||
"licenses": [
|
||||
"MIT"
|
||||
]
|
||||
}
|
23
app/config/libraries/trustedintents.json
Normal file
23
app/config/libraries/trustedintents.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"uniqueId": "info.guardianproject.trustedintents:trustedintents",
|
||||
"funding": [
|
||||
|
||||
],
|
||||
"developers": [
|
||||
{
|
||||
"name": "Guardian Project"
|
||||
}
|
||||
],
|
||||
"artifactVersion": "0.2",
|
||||
"description": "TrustedIntents is a library for flexible trusted interactions between Android apps. It is modeled after Android's `signature` protection level for permissions. The key difference is that the framework allows the trusted signature to be set, rather than requiring to match the current app's signature.",
|
||||
"scm": {
|
||||
"connection": "scm:https://github.com/guardianproject/TrustedIntents.git",
|
||||
"url": "scm:https://github.com/guardianproject/TrustedIntents",
|
||||
"developerConnection": "scm:git@github.com:guardianproject/TrustedIntents.git"
|
||||
},
|
||||
"name": "TrustedIntents",
|
||||
"website": "https://guardianproject.info/code/trustedintents",
|
||||
"licenses": [
|
||||
"3ca920d1875f7ad7ab04a2a331958577"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"hash": "3ca920d1875f7ad7ab04a2a331958577",
|
||||
"url": "https://github.com/guardianproject/TrustedIntents/blob/master/LICENSE.txt",
|
||||
"name": "LGPLv2.1"
|
||||
}
|
|
@ -1,10 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="MissingTranslation" severity="ignore" />
|
||||
<issue id="MissingQuantity" severity="ignore" />
|
||||
<issue id="InvalidPackage">
|
||||
<ignore regexp="X509LDAPCertStoreSpi" />
|
||||
</issue>
|
||||
<issue id="NotificationPermission">
|
||||
<ignore regexp="com.bumptech.glide.request.target.NotificationTarget" />
|
||||
</issue>
|
||||
<issue id="ResourceType" severity="Warning" />
|
||||
<issue id="UnusedResources" severity="error">
|
||||
<ignore path="res/raw/aboutlibraries.json" />
|
||||
<ignore regexp="res/mipmap.*/ic_launcher_debug.*.png" />
|
||||
</issue>
|
||||
</lint>
|
||||
|
|
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
|
@ -1,7 +1,10 @@
|
|||
-keepattributes LineNumberTable,SourceFile
|
||||
-renamesourcefileattribute SourceFile
|
||||
-dontobfuscate
|
||||
|
||||
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
|
||||
-keep class com.beemdevelopment.aegis.ui.fragments.preferences.*
|
||||
-keep class com.beemdevelopment.aegis.importers.** { *; }
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
||||
-dontobfuscate
|
||||
-dontwarn javax.naming.**
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "392278bdb797d013cb2ada67a3b1cc60",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "audit_logs",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `event_type` TEXT NOT NULL, `reference` TEXT, `timestamp` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "_eventType",
|
||||
"columnName": "event_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "_reference",
|
||||
"columnName": "reference",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "_timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '392278bdb797d013cb2ada67a3b1cc60')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -2,15 +2,19 @@ package com.beemdevelopment.aegis;
|
|||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.matcher.BoundedMatcher;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.SCryptParameters;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryHolder;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
|
@ -20,6 +24,7 @@ import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
|||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vectors.VaultEntries;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
|
@ -178,4 +183,21 @@ public abstract class AegisTest {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected static Matcher<RecyclerView.ViewHolder> withOtpType(Class<? extends OtpInfo> otpClass) {
|
||||
return new BoundedMatcher<RecyclerView.ViewHolder, EntryHolder>(EntryHolder.class) {
|
||||
@Override
|
||||
public boolean matchesSafely(EntryHolder holder) {
|
||||
return holder != null
|
||||
&& holder.getEntry() != null
|
||||
&& holder.getEntry().getInfo().getClass().equals(otpClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText(String.format("with otp type '%s'", otpClass.getSimpleName()));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ package com.beemdevelopment.aegis;
|
|||
import android.app.Application;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.runner.AndroidJUnitRunner;
|
||||
|
||||
|
|
|
@ -61,13 +61,20 @@ import org.junit.Test;
|
|||
import org.junit.rules.RuleChain;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
@ -183,7 +190,9 @@ public class BackupExportTest extends AegisTest {
|
|||
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withId(R.id.checkbox_accept)).perform(click());
|
||||
doExport();
|
||||
File file = doExport();
|
||||
|
||||
checkHtmlExport(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -196,7 +205,9 @@ public class BackupExportTest extends AegisTest {
|
|||
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withId(R.id.checkbox_accept)).perform(click());
|
||||
doExport();
|
||||
File file = doExport();
|
||||
|
||||
checkHtmlExport(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -380,6 +391,26 @@ public class BackupExportTest extends AegisTest {
|
|||
checkReadEntries(entries);
|
||||
}
|
||||
|
||||
private void checkHtmlExport(File file) {
|
||||
try (InputStream inStream = new FileInputStream(file)) {
|
||||
Reader inReader = new InputStreamReader(inStream, StandardCharsets.UTF_8);
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
XmlPullParser parser = factory.newPullParser();
|
||||
parser.setInput(inReader);
|
||||
while (parser.getEventType() != XmlPullParser.START_TAG) {
|
||||
parser.next();
|
||||
}
|
||||
if (!parser.getName().toLowerCase(Locale.ROOT).equals("html")) {
|
||||
throw new RuntimeException("not an html document!");
|
||||
}
|
||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||
parser.next();
|
||||
}
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
throw new RuntimeException("Unable to read html export file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkReadEntries(Collection<VaultEntry> entries) {
|
||||
List<VaultEntry> vectors = VaultEntries.get();
|
||||
assertEquals(vectors.size(), entries.size());
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
|
||||
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
|
||||
import static androidx.test.espresso.action.ViewActions.clearText;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.action.ViewActions.pressBack;
|
||||
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
|
||||
|
@ -18,19 +18,17 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
|||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.espresso.matcher.RootMatchers;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
|
@ -41,6 +39,7 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
|
|||
import com.beemdevelopment.aegis.otp.YandexInfo;
|
||||
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
@ -55,6 +54,7 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest;
|
||||
|
||||
|
@ -104,19 +104,26 @@ public class OverallTest extends AegisTest {
|
|||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, clickChildViewWithId(R.id.buttonRefresh)));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnHolderItem(withOtpType(HotpInfo.class), clickChildViewWithId(R.id.buttonRefresh)));
|
||||
}
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
AtomicBoolean isErrorCardShown = new AtomicBoolean(false);
|
||||
_activityRule.getScenario().onActivity(activity -> {
|
||||
isErrorCardShown.set(((EntryAdapter)((RecyclerView) activity.findViewById(R.id.rvKeyProfiles)).getAdapter()).isErrorCardShown());
|
||||
});
|
||||
|
||||
int entryPosOffset = isErrorCardShown.get() ? 1 : 0;
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
|
||||
onView(withId(R.id.action_copy)).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 1, longClick()));
|
||||
onView(withId(R.id.action_edit)).perform(click());
|
||||
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
|
||||
onView(withId(R.id.dropdown_group)).perform(click());
|
||||
onView(withText(R.string.new_group)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
onView(withId(R.id.text_group)).perform(click());
|
||||
onView(withId(R.id.addGroup)).inRoot(RootMatchers.isDialog()).perform(click());
|
||||
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withText(R.string.save)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
|
@ -129,13 +136,13 @@ public class OverallTest extends AegisTest {
|
|||
changeGroupFilter(_groupName);
|
||||
changeGroupFilter(null);
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(4, click()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 2, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 3, click()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 4, click()));
|
||||
onView(withId(R.id.action_share_qr)).perform(click());
|
||||
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
|
||||
onView(allOf(isDescendantOfA(withClassName(containsString("ActionBarContextView"))), withClassName(containsString("OverflowMenuButton")))).perform(click());
|
||||
onView(withText(R.string.action_delete)).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
@ -170,12 +177,10 @@ public class OverallTest extends AegisTest {
|
|||
}
|
||||
|
||||
private void changeGroupFilter(String text) {
|
||||
onView(withId(R.id.chip_group)).perform(click());
|
||||
if (text == null) {
|
||||
onView(withId(R.id.btnClear)).perform(click());
|
||||
onView(allOf(withText(R.string.no_group), isDescendantOfA(withId(R.id.groupChipGroup)))).perform(click());
|
||||
} else {
|
||||
onView(withText(text)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
onView(allOf(withText(text), isDescendantOfA(withId(R.id.groupChipGroup)))).perform(click());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,7 +188,7 @@ public class OverallTest extends AegisTest {
|
|||
onView(withId(R.id.fab)).perform(click());
|
||||
onView(withId(R.id.fab_enter)).perform(click());
|
||||
|
||||
onView(withId(R.id.accordian_header)).perform(click());
|
||||
onView(withId(R.id.accordian_header)).perform(scrollTo(), click());
|
||||
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
|
||||
|
||||
|
@ -203,7 +208,7 @@ public class OverallTest extends AegisTest {
|
|||
throw new RuntimeException(String.format("Unexpected entry type: %s", entry.getInfo().getClass().getSimpleName()));
|
||||
}
|
||||
|
||||
onView(withId(R.id.dropdown_type)).perform(click());
|
||||
onView(withId(R.id.dropdown_type)).perform(scrollTo(), click());
|
||||
onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
|
@ -32,11 +31,10 @@ public class PanicTriggerTest extends AegisTest {
|
|||
@Test
|
||||
public void testPanicTriggerDisabled() {
|
||||
assertFalse(_prefs.isPanicTriggerEnabled());
|
||||
assertTrue(_vaultManager.isVaultLoaded());
|
||||
launchPanic();
|
||||
assertTrue(_vaultManager.isVaultLoaded());
|
||||
_vaultManager.getVault();
|
||||
assertFalse(_vaultManager.isVaultFileLoaded());
|
||||
assertNull(_vaultManager.getVaultFileError());
|
||||
assertTrue(VaultRepository.fileExists(getApp()));
|
||||
}
|
||||
|
||||
|
@ -44,11 +42,10 @@ public class PanicTriggerTest extends AegisTest {
|
|||
public void testPanicTriggerEnabled() {
|
||||
_prefs.setIsPanicTriggerEnabled(true);
|
||||
assertTrue(_prefs.isPanicTriggerEnabled());
|
||||
assertTrue(_vaultManager.isVaultLoaded());
|
||||
launchPanic();
|
||||
assertFalse(_vaultManager.isVaultLoaded());
|
||||
assertThrows(IllegalStateException.class, () -> _vaultManager.getVault());
|
||||
assertFalse(_vaultManager.isVaultFileLoaded());
|
||||
assertNull(_vaultManager.getVaultFileError());
|
||||
assertFalse(VaultRepository.fileExists(getApp()));
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- NOTE: Disabled for now. See issue: #1047
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
@ -27,8 +28,8 @@
|
|||
android:icon="@mipmap/${iconName}"
|
||||
android:label="Aegis"
|
||||
android:supportsRtl="true"
|
||||
android:largeHeap="true"
|
||||
android:theme="@style/Theme.Aegis.Launch"
|
||||
tools:replace="android:theme"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity android:name=".ui.TransferEntriesActivity"
|
||||
android:label="@string/title_activity_transfer" />
|
||||
|
@ -82,6 +83,10 @@
|
|||
<activity
|
||||
android:name=".ui.GroupManagerActivity"
|
||||
android:label="@string/title_activity_manage_groups" />
|
||||
<activity android:name=".ui.AssignIconsActivity"
|
||||
android:label="@string/title_activity_assign_icons"/>
|
||||
<activity android:name=".ui.LicensesActivity"
|
||||
android:label="@string/title_activity_licenses"/>
|
||||
<activity
|
||||
android:name=".ui.PanicResponderActivity"
|
||||
android:exported="true"
|
||||
|
@ -146,7 +151,7 @@
|
|||
</application>
|
||||
|
||||
<queries>
|
||||
<package android:name="me.jmh.authenticatorpro" />
|
||||
<package android:name="com.stratumauth.app" />
|
||||
<package android:name="com.authy.authy" />
|
||||
<package android:name="org.fedorahosted.freeotp" />
|
||||
<package android:name="org.liberty.android.freeotpplus" />
|
||||
|
@ -155,7 +160,7 @@
|
|||
<package android:name="com.valvesoftware.android.steam.community" />
|
||||
<package android:name="com.authenticator.authservice2" />
|
||||
<package android:name="com.duosecurity.duomobile" />
|
||||
<package android:name="com.blizzard.bma" />
|
||||
<package android:name="com.blizzard.messenger" />
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -31,6 +31,169 @@
|
|||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 3.4</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Haptic feedback when an entry is about to expire</li>
|
||||
<li>Brightness increase is now toggleable in the entry transfer view</li>
|
||||
<li>Filter on multiple groups simultaneously</li>
|
||||
<li>Color contrast on hidden codes has been improved</li>
|
||||
<li>Prompt before the user is about to save an entry with a duplicate name/issuer combination</li>
|
||||
<li>New languages: Estonian, Korean, Malayalam, Norwegian (Bokmål) and Serbian</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>A crash could occur if an entry with period 7 exists and code expiry indication is enabled</li>
|
||||
<li>The Portuguese (Brazilian) locale was used even if Portuguese was configured</li>
|
||||
<li>FreeOTP import would fail if the algorithm or digits field was not specified for an entry</li>
|
||||
<li>The divider between entries would be missing in certain filter configurations</li>
|
||||
<li>The snackbar in try entry importing view could obstruct the name of an entry</li>
|
||||
</ul>
|
||||
<h4>Miscellaneous</h4>
|
||||
<ul>
|
||||
<li>Android 6 or newer is now required the run the app</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.4</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Icons are now resized to 512x512 to reduce the size of the vault file and to reduce the chance of encountering out of memory conditions</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.3</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Some users ran into out of memory conditions due to large icons in their vault file. We've introduced a temporary measure that should help in most cases, but we'll follow up with a more comprehensive fix soon.</li>
|
||||
<li>Window insets were not always applied correctly, causing parts of the UI to appear off-screen</li>
|
||||
<li>The 2FAS importer did not tolerate spaces for secrets and was not always able to extract the issuer</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Find entries by searching in multiple fields simultaneously</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Entries would not actually be added to the Aegis vault in some cases when importing from Google Authenticator export QR codes</li>
|
||||
<li>The lock button was sometimes shown for unencrypted vaults</li>
|
||||
<li>The sort category menu item did not always reflect the current sorting</li>
|
||||
<li>The next code was not always easy to read because its color had low contrast with the background</li>
|
||||
<li>Entry selection was not cancelled when changing the group filter</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Codes were not shown in case the tiles view mode was combined with hidden account names</li>
|
||||
</ul>
|
||||
<h3>Version 3.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Significant improvements to group filtering
|
||||
<ul>
|
||||
<li>Groups can now be filtered on straight from the main view instead of through a dialog</li>
|
||||
<li>Ability to assign multiple entries to a group in one go</li>
|
||||
<li>Support for reordering groups</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Codes now change color when they're about to expire</li>
|
||||
<li>Option to show the next code ahead of time</li>
|
||||
<li>Support for backing up to a single file (This enables support for more cloud providers, such as Google Drive)</li>
|
||||
<li>Various minor improvements to make QR code exports easier to scan</li>
|
||||
<li>Support for importing from Ente Auth</li>
|
||||
<li>Support for importing FreeOTP 2 backups</li>
|
||||
<li>Updated translations</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>QR codes exported for Google Authenticator could not be scanned on iOS</li>
|
||||
<li>The code would be copied after a single tap in case "Tap to reveal" and "Copy tokens to the clipboard" were enabled simultaneously</li>
|
||||
<li>Various other minor UI, stability and performance improvements</li>
|
||||
</ul>
|
||||
<h3>Version 3.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>The ability to add a single entry to multiple groups</li>
|
||||
<li>Option to keep an infinite number of backups</li>
|
||||
<li>Option to customize which fields to search for in entries</li>
|
||||
<li>Allow hiding entry names in the tiled view mode</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>With "Tap to reveal" enabled, the size of the shown dots would not be consistent with the size of the code digits, on some devices</li>
|
||||
<li>After importing a backup, the UI would in some cases incorrectly claim that biometric unlock is enabled</li>
|
||||
<li>The export dialog was not fully visible on some devices</li>
|
||||
<li>Various other minor UI, stability and performance improvements</li>
|
||||
</ul>
|
||||
<h3>Version 3.1.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<p>
|
||||
A recent Android Pixel update introduced a bug causing Aegis to sometimes show a black screen after unlocking the vault.
|
||||
We have reported this issue to the Google Issue Tracker (<a href="https://issuetracker.google.com/issues/352963108">link</a>) and
|
||||
are awaiting a response from Google. In the meantime, we have implemented a workaround that eliminates this bug.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Group filter now gets applied properly upon unlocking the vault</li>
|
||||
<li>Advanced entry settings now gets shown correctly</li>
|
||||
<li>Keyboard when searching for entries now gets hidden when the user starts scrolling through the list</li>
|
||||
</ul>
|
||||
<h3>Version 3.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>A new audit log has been added to check all important events that occurred in your vault</li>
|
||||
<li>Added the ability to rename groups</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Group selection will now be remembered again upon launch</li>
|
||||
<li>Various UI improvements</li>
|
||||
<li>Stability fixes</li>
|
||||
</ul>
|
||||
<h3>Version 3.0.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for importing from the new Battle.net app</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Visual glitches when AMOLED theme was used on old Android versions</li>
|
||||
<li>Minor UI improvements</li>
|
||||
</ul>
|
||||
<h3>Version 3.0</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Material 3 (and Material You)</li>
|
||||
<li>Automatic assignment of icons to entries</li>
|
||||
<li>Ability to select all entries in one go</li>
|
||||
<li>Support for importing 2FAS schema v4 backups</li>
|
||||
<li>Sort entries based on the last time they were used</li>
|
||||
<li>Some clarifications related to importing and backup permission errors</li>
|
||||
<li>Preparations for the ability to assign a single entry to multiple groups</li>
|
||||
<li>Performance improvements when scrolling through an entry list with lots of icons</li>
|
||||
<li>A new look for the third-party licenses list</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Directly importing from Authy using root would fail</li>
|
||||
<li>Minor glitches related to animation duration scale settings</li>
|
||||
<li>Various stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 2.2.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>An optional name field for icon packs to bypass filename character restrictions</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The Authenticator Pro importer only supported the legacy backup format</li>
|
||||
<li>A crash could occur in the tile service</li>
|
||||
</ul>
|
||||
<h3>Version 2.2.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Ability to automatically skip potential duplicates when importing entries</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Biometrics button on the unlock screen was unresponsive</li>
|
||||
</ul>
|
||||
<h3>Version 2.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
|
|
22
app/src/main/java/com/amulyakhare/textdrawable/LICENSE
Normal file
22
app/src/main/java/com/amulyakhare/textdrawable/LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Amulya Khare
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
316
app/src/main/java/com/amulyakhare/textdrawable/TextDrawable.java
Normal file
316
app/src/main/java/com/amulyakhare/textdrawable/TextDrawable.java
Normal file
|
@ -0,0 +1,316 @@
|
|||
package com.amulyakhare.textdrawable;
|
||||
|
||||
import android.graphics.*;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.OvalShape;
|
||||
import android.graphics.drawable.shapes.RectShape;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
|
||||
/**
|
||||
* @author amulya
|
||||
* @datetime 14 Oct 2014, 3:53 PM
|
||||
*/
|
||||
public class TextDrawable extends ShapeDrawable {
|
||||
|
||||
private final Paint textPaint;
|
||||
private final Paint borderPaint;
|
||||
private static final float SHADE_FACTOR = 0.9f;
|
||||
private final String text;
|
||||
private final int color;
|
||||
private final RectShape shape;
|
||||
private final int height;
|
||||
private final int width;
|
||||
private final int fontSize;
|
||||
private final float radius;
|
||||
private final int borderThickness;
|
||||
|
||||
private TextDrawable(Builder builder) {
|
||||
super(builder.shape);
|
||||
|
||||
// shape properties
|
||||
shape = builder.shape;
|
||||
height = builder.height;
|
||||
width = builder.width;
|
||||
radius = builder.radius;
|
||||
|
||||
// text and color
|
||||
text = builder.toUpperCase ? builder.text.toUpperCase() : builder.text;
|
||||
color = builder.color;
|
||||
|
||||
// text paint settings
|
||||
fontSize = builder.fontSize;
|
||||
textPaint = new Paint();
|
||||
textPaint.setColor(builder.textColor);
|
||||
textPaint.setAntiAlias(true);
|
||||
textPaint.setFakeBoldText(builder.isBold);
|
||||
textPaint.setStyle(Paint.Style.FILL);
|
||||
textPaint.setTypeface(builder.font);
|
||||
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||
textPaint.setStrokeWidth(builder.borderThickness);
|
||||
|
||||
// border paint settings
|
||||
borderThickness = builder.borderThickness;
|
||||
borderPaint = new Paint();
|
||||
borderPaint.setColor(getDarkerShade(color));
|
||||
borderPaint.setStyle(Paint.Style.STROKE);
|
||||
borderPaint.setStrokeWidth(borderThickness);
|
||||
|
||||
// drawable paint color
|
||||
Paint paint = getPaint();
|
||||
paint.setColor(color);
|
||||
|
||||
}
|
||||
|
||||
private int getDarkerShade(int color) {
|
||||
return Color.rgb((int)(SHADE_FACTOR * Color.red(color)),
|
||||
(int)(SHADE_FACTOR * Color.green(color)),
|
||||
(int)(SHADE_FACTOR * Color.blue(color)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
super.draw(canvas);
|
||||
Rect r = getBounds();
|
||||
|
||||
|
||||
// draw border
|
||||
if (borderThickness > 0) {
|
||||
drawBorder(canvas);
|
||||
}
|
||||
|
||||
int count = canvas.save();
|
||||
canvas.translate(r.left, r.top);
|
||||
|
||||
// draw text
|
||||
int width = this.width < 0 ? r.width() : this.width;
|
||||
int height = this.height < 0 ? r.height() : this.height;
|
||||
int fontSize = this.fontSize < 0 ? (Math.min(width, height) / 2) : this.fontSize;
|
||||
textPaint.setTextSize(fontSize);
|
||||
canvas.drawText(text, width / 2, height / 2 - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint);
|
||||
|
||||
canvas.restoreToCount(count);
|
||||
|
||||
}
|
||||
|
||||
private void drawBorder(Canvas canvas) {
|
||||
RectF rect = new RectF(getBounds());
|
||||
rect.inset(borderThickness/2, borderThickness/2);
|
||||
|
||||
if (shape instanceof OvalShape) {
|
||||
canvas.drawOval(rect, borderPaint);
|
||||
}
|
||||
else if (shape instanceof RoundRectShape) {
|
||||
canvas.drawRoundRect(rect, radius, radius, borderPaint);
|
||||
}
|
||||
else {
|
||||
canvas.drawRect(rect, borderPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
textPaint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter cf) {
|
||||
textPaint.setColorFilter(cf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public static IShapeBuilder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder implements IConfigBuilder, IShapeBuilder, IBuilder {
|
||||
|
||||
private String text;
|
||||
|
||||
private int color;
|
||||
|
||||
private int borderThickness;
|
||||
|
||||
private int width;
|
||||
|
||||
private int height;
|
||||
|
||||
private Typeface font;
|
||||
|
||||
private RectShape shape;
|
||||
|
||||
public int textColor;
|
||||
|
||||
private int fontSize;
|
||||
|
||||
private boolean isBold;
|
||||
|
||||
private boolean toUpperCase;
|
||||
|
||||
public float radius;
|
||||
|
||||
private Builder() {
|
||||
text = "";
|
||||
color = Color.GRAY;
|
||||
textColor = Color.WHITE;
|
||||
borderThickness = 0;
|
||||
width = -1;
|
||||
height = -1;
|
||||
shape = new RectShape();
|
||||
font = Typeface.create("sans-serif-light", Typeface.NORMAL);
|
||||
fontSize = -1;
|
||||
isBold = false;
|
||||
toUpperCase = false;
|
||||
}
|
||||
|
||||
public IConfigBuilder width(int width) {
|
||||
this.width = width;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder height(int height) {
|
||||
this.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder textColor(int color) {
|
||||
this.textColor = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder withBorder(int thickness) {
|
||||
this.borderThickness = thickness;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder useFont(Typeface font) {
|
||||
this.font = font;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder fontSize(int size) {
|
||||
this.fontSize = size;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder bold() {
|
||||
this.isBold = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IConfigBuilder toUpperCase() {
|
||||
this.toUpperCase = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IConfigBuilder beginConfig() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IShapeBuilder endConfig() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBuilder rect() {
|
||||
this.shape = new RectShape();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBuilder round() {
|
||||
this.shape = new OvalShape();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBuilder roundRect(int radius) {
|
||||
this.radius = radius;
|
||||
float[] radii = {radius, radius, radius, radius, radius, radius, radius, radius};
|
||||
this.shape = new RoundRectShape(radii, null, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextDrawable buildRect(String text, int color) {
|
||||
rect();
|
||||
return build(text, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextDrawable buildRoundRect(String text, int color, int radius) {
|
||||
roundRect(radius);
|
||||
return build(text, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextDrawable buildRound(String text, int color) {
|
||||
round();
|
||||
return build(text, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextDrawable build(String text, int color) {
|
||||
this.color = color;
|
||||
this.text = text;
|
||||
return new TextDrawable(this);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IConfigBuilder {
|
||||
public IConfigBuilder width(int width);
|
||||
|
||||
public IConfigBuilder height(int height);
|
||||
|
||||
public IConfigBuilder textColor(int color);
|
||||
|
||||
public IConfigBuilder withBorder(int thickness);
|
||||
|
||||
public IConfigBuilder useFont(Typeface font);
|
||||
|
||||
public IConfigBuilder fontSize(int size);
|
||||
|
||||
public IConfigBuilder bold();
|
||||
|
||||
public IConfigBuilder toUpperCase();
|
||||
|
||||
public IShapeBuilder endConfig();
|
||||
}
|
||||
|
||||
public static interface IBuilder {
|
||||
|
||||
public TextDrawable build(String text, int color);
|
||||
}
|
||||
|
||||
public static interface IShapeBuilder {
|
||||
|
||||
public IConfigBuilder beginConfig();
|
||||
|
||||
public IBuilder rect();
|
||||
|
||||
public IBuilder round();
|
||||
|
||||
public IBuilder roundRect(int radius);
|
||||
|
||||
public TextDrawable buildRect(String text, int color);
|
||||
|
||||
public TextDrawable buildRoundRect(String text, int color, int radius);
|
||||
|
||||
public TextDrawable buildRound(String text, int color);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package com.amulyakhare.textdrawable.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* @author amulya
|
||||
* @datetime 14 Oct 2014, 5:20 PM
|
||||
*/
|
||||
public class ColorGenerator {
|
||||
|
||||
public static ColorGenerator DEFAULT;
|
||||
|
||||
public static ColorGenerator MATERIAL;
|
||||
|
||||
static {
|
||||
DEFAULT = create(Arrays.asList(
|
||||
0xfff16364,
|
||||
0xfff58559,
|
||||
0xfff9a43e,
|
||||
0xffe4c62e,
|
||||
0xff67bf74,
|
||||
0xff59a2be,
|
||||
0xff2093cd,
|
||||
0xffad62a7,
|
||||
0xff805781
|
||||
));
|
||||
MATERIAL = create(Arrays.asList(
|
||||
0xffe57373,
|
||||
0xfff06292,
|
||||
0xffba68c8,
|
||||
0xff9575cd,
|
||||
0xff7986cb,
|
||||
0xff64b5f6,
|
||||
0xff4fc3f7,
|
||||
0xff4dd0e1,
|
||||
0xff4db6ac,
|
||||
0xff81c784,
|
||||
0xffaed581,
|
||||
0xffff8a65,
|
||||
0xffd4e157,
|
||||
0xffffd54f,
|
||||
0xffffb74d,
|
||||
0xffa1887f,
|
||||
0xff90a4ae
|
||||
));
|
||||
}
|
||||
|
||||
private final List<Integer> mColors;
|
||||
private final Random mRandom;
|
||||
|
||||
public static ColorGenerator create(List<Integer> colorList) {
|
||||
return new ColorGenerator(colorList);
|
||||
}
|
||||
|
||||
private ColorGenerator(List<Integer> colorList) {
|
||||
mColors = colorList;
|
||||
mRandom = new Random(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public int getRandomColor() {
|
||||
return mColors.get(mRandom.nextInt(mColors.size()));
|
||||
}
|
||||
|
||||
public int getColor(Object key) {
|
||||
return mColors.get(Math.abs(key.hashCode()) % mColors.size());
|
||||
}
|
||||
}
|
|
@ -22,8 +22,6 @@ import com.beemdevelopment.aegis.receivers.VaultLockReceiver;
|
|||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.util.Collections;
|
||||
|
@ -48,9 +46,6 @@ public abstract class AegisApplicationBase extends Application {
|
|||
super.onCreate();
|
||||
_vaultManager = EarlyEntryPoints.get(this, EntryPoint.class).getVaultManager();
|
||||
|
||||
Iconics.init(this);
|
||||
Iconics.registerFont(new MaterialDesignIconic());
|
||||
|
||||
VaultLockReceiver lockReceiver = new VaultLockReceiver();
|
||||
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
|
||||
ContextCompat.registerReceiver(this, lockReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
|
||||
|
|
|
@ -8,6 +8,8 @@ import android.os.Build;
|
|||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import com.beemdevelopment.aegis.database.AppDatabase;
|
||||
import com.beemdevelopment.aegis.database.AuditLogRepository;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
|
@ -25,12 +27,16 @@ public class AegisBackupAgent extends BackupAgent {
|
|||
|
||||
private Preferences _prefs;
|
||||
|
||||
private AuditLogRepository _auditLogRepository;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Cannot use injection with Dagger Hilt here, because the app is launched in a restricted mode on restore
|
||||
_prefs = new Preferences(this);
|
||||
AppDatabase appDatabase = AegisModule.provideAppDatabase(this);
|
||||
_auditLogRepository = AegisModule.provideAuditLogRepository(appDatabase);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -53,6 +59,7 @@ public class AegisBackupAgent extends BackupAgent {
|
|||
// report any runtime exceptions, in addition to the expected IOExceptions.
|
||||
try {
|
||||
fullBackup(data);
|
||||
_auditLogRepository.addAndroidBackupCreatedEvent();
|
||||
_prefs.setAndroidBackupResult(new Preferences.BackupResult(null));
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
|
||||
|
|
|
@ -2,6 +2,11 @@ package com.beemdevelopment.aegis;
|
|||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.room.Room;
|
||||
|
||||
import com.beemdevelopment.aegis.database.AppDatabase;
|
||||
import com.beemdevelopment.aegis.database.AuditLogDao;
|
||||
import com.beemdevelopment.aegis.database.AuditLogRepository;
|
||||
import com.beemdevelopment.aegis.icons.IconPackManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
|
||||
|
@ -24,12 +29,27 @@ public class AegisModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static VaultManager provideVaultManager(@ApplicationContext Context context) {
|
||||
return new VaultManager(context);
|
||||
public static AuditLogRepository provideAuditLogRepository(AppDatabase appDatabase) {
|
||||
AuditLogDao auditLogDao = appDatabase.auditLogDao();
|
||||
return new AuditLogRepository(auditLogDao);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static VaultManager provideVaultManager(@ApplicationContext Context context, AuditLogRepository auditLogRepository) {
|
||||
return new VaultManager(context, auditLogRepository);
|
||||
}
|
||||
|
||||
@Provides
|
||||
public static Preferences providePreferences(@ApplicationContext Context context) {
|
||||
return new Preferences(context);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static AppDatabase provideAppDatabase(@ApplicationContext Context context) {
|
||||
return Room.databaseBuilder(context.getApplicationContext(),
|
||||
AppDatabase.class, "aegis-db")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum BackupsVersioningStrategy {
|
||||
UNDEFINED,
|
||||
MULTIPLE_BACKUPS,
|
||||
SINGLE_BACKUP
|
||||
}
|
42
app/src/main/java/com/beemdevelopment/aegis/EventType.java
Normal file
42
app/src/main/java/com/beemdevelopment/aegis/EventType.java
Normal file
|
@ -0,0 +1,42 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum EventType {
|
||||
|
||||
VAULT_UNLOCKED,
|
||||
VAULT_BACKUP_CREATED,
|
||||
VAULT_ANDROID_BACKUP_CREATED,
|
||||
VAULT_EXPORTED,
|
||||
ENTRY_SHARED,
|
||||
VAULT_UNLOCK_FAILED_PASSWORD,
|
||||
VAULT_UNLOCK_FAILED_BIOMETRICS;
|
||||
private static EventType[] _values;
|
||||
|
||||
static {
|
||||
_values = values();
|
||||
}
|
||||
|
||||
public static EventType fromInteger(int x) {
|
||||
return _values[x];
|
||||
}
|
||||
|
||||
public static int getEventTitleRes(EventType eventType) {
|
||||
switch (eventType) {
|
||||
case VAULT_UNLOCKED:
|
||||
return R.string.event_title_vault_unlocked;
|
||||
case VAULT_BACKUP_CREATED:
|
||||
return R.string.event_title_backup_created;
|
||||
case VAULT_ANDROID_BACKUP_CREATED:
|
||||
return R.string.event_title_android_backup_created;
|
||||
case VAULT_EXPORTED:
|
||||
return R.string.event_title_vault_exported;
|
||||
case ENTRY_SHARED:
|
||||
return R.string.event_title_entry_shared;
|
||||
case VAULT_UNLOCK_FAILED_PASSWORD:
|
||||
return R.string.event_title_vault_unlock_failed_password;
|
||||
case VAULT_UNLOCK_FAILED_BIOMETRICS:
|
||||
return R.string.event_title_vault_unlock_failed_biometrics;
|
||||
default:
|
||||
return R.string.event_unknown;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum GroupPlaceholderType {
|
||||
ALL,
|
||||
NEW_GROUP,
|
||||
NO_GROUP;
|
||||
|
||||
public int getStringRes() {
|
||||
switch (this) {
|
||||
case ALL:
|
||||
return R.string.all;
|
||||
case NEW_GROUP:
|
||||
return R.string.new_group;
|
||||
case NO_GROUP:
|
||||
return R.string.no_group;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unexpected placeholder type: " + this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,12 +5,15 @@ import android.content.SharedPreferences;
|
|||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.provider.DocumentsContractCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.util.TimeUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultBackupPermissionException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
@ -20,9 +23,11 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Preferences {
|
||||
|
@ -31,12 +36,26 @@ public class Preferences {
|
|||
public static final int AUTO_LOCK_ON_MINIMIZE = 1 << 2;
|
||||
public static final int AUTO_LOCK_ON_DEVICE_LOCK = 1 << 3;
|
||||
|
||||
public static final int SEARCH_IN_ISSUER = 1 << 0;
|
||||
public static final int SEARCH_IN_NAME = 1 << 1;
|
||||
public static final int SEARCH_IN_NOTE = 1 << 2;
|
||||
public static final int SEARCH_IN_GROUPS = 1 << 3;
|
||||
|
||||
public static final int BACKUPS_VERSIONS_INFINITE = -1;
|
||||
|
||||
public static final int[] AUTO_LOCK_SETTINGS = {
|
||||
AUTO_LOCK_ON_BACK_BUTTON,
|
||||
AUTO_LOCK_ON_MINIMIZE,
|
||||
AUTO_LOCK_ON_DEVICE_LOCK
|
||||
};
|
||||
|
||||
public static final int[] SEARCH_BEHAVIOR_SETTINGS = {
|
||||
SEARCH_IN_ISSUER,
|
||||
SEARCH_IN_NAME,
|
||||
SEARCH_IN_NOTE,
|
||||
SEARCH_IN_GROUPS
|
||||
};
|
||||
|
||||
private SharedPreferences _prefs;
|
||||
|
||||
public Preferences(Context context) {
|
||||
|
@ -67,10 +86,18 @@ public class Preferences {
|
|||
return _prefs.getBoolean("pref_tap_to_reveal", false);
|
||||
}
|
||||
|
||||
public boolean isGroupMultiselectEnabled() {
|
||||
return _prefs.getBoolean("pref_groups_multiselect", false);
|
||||
}
|
||||
|
||||
public boolean isEntryHighlightEnabled() {
|
||||
return _prefs.getBoolean("pref_highlight_entry", false);
|
||||
}
|
||||
|
||||
public boolean isHapticFeedbackEnabled() {
|
||||
return _prefs.getBoolean("pref_haptic_feedback", true);
|
||||
}
|
||||
|
||||
public boolean isPauseFocusedEnabled() {
|
||||
boolean dependenciesEnabled = isTapToRevealEnabled() || isEntryHighlightEnabled();
|
||||
if (!dependenciesEnabled) return false;
|
||||
|
@ -136,12 +163,24 @@ public class Preferences {
|
|||
return _prefs.getBoolean("pref_show_icons", true);
|
||||
}
|
||||
|
||||
public boolean getShowNextCode() {
|
||||
return _prefs.getBoolean("pref_show_next_code", false);
|
||||
}
|
||||
|
||||
public boolean getShowExpirationState() {
|
||||
return _prefs.getBoolean("pref_expiration_state", true);
|
||||
}
|
||||
|
||||
public CodeGrouping getCodeGroupSize() {
|
||||
String value = _prefs.getString("pref_code_group_size_string", "GROUPING_THREES");
|
||||
|
||||
return CodeGrouping.valueOf(value);
|
||||
}
|
||||
|
||||
public void setCodeGroupSize(CodeGrouping codeGroupSize) {
|
||||
_prefs.edit().putString("pref_code_group_size_string", codeGroupSize.name()).apply();
|
||||
}
|
||||
|
||||
public boolean isIntroDone() {
|
||||
return _prefs.getBoolean("pref_intro", false);
|
||||
}
|
||||
|
@ -155,6 +194,20 @@ public class Preferences {
|
|||
return _prefs.getInt("pref_auto_lock_mask", def);
|
||||
}
|
||||
|
||||
public int getSearchBehaviorMask() {
|
||||
final int def = SEARCH_IN_ISSUER | SEARCH_IN_NAME;
|
||||
|
||||
return _prefs.getInt("pref_search_behavior_mask", def);
|
||||
}
|
||||
|
||||
public boolean isSearchBehaviorTypeEnabled(int searchBehaviorType) {
|
||||
return (getSearchBehaviorMask() & searchBehaviorType) == searchBehaviorType;
|
||||
}
|
||||
|
||||
public void setSearchBehaviorMask(int searchBehavior) {
|
||||
_prefs.edit().putInt("pref_search_behavior_mask", searchBehavior).apply();
|
||||
}
|
||||
|
||||
public boolean isAutoLockEnabled() {
|
||||
return getAutoLockMask() != AUTO_LOCK_OFF;
|
||||
}
|
||||
|
@ -195,6 +248,10 @@ public class Preferences {
|
|||
_prefs.edit().putInt("pref_current_theme", theme.ordinal()).apply();
|
||||
}
|
||||
|
||||
public boolean isDynamicColorsEnabled() {
|
||||
return _prefs.getBoolean("pref_dynamic_colors", false);
|
||||
}
|
||||
|
||||
public ViewMode getCurrentViewMode() {
|
||||
return ViewMode.fromInteger(_prefs.getInt("pref_current_view_mode", 0));
|
||||
}
|
||||
|
@ -224,10 +281,51 @@ public class Preferences {
|
|||
setUsageCount(usageCounts);
|
||||
}
|
||||
|
||||
public long getLastUsedTimestamp(UUID uuid) {
|
||||
Map<UUID, Long> timestamps = getLastUsedTimestamps();
|
||||
if (timestamps != null && timestamps.size() > 0){
|
||||
Long timestamp = timestamps.get(uuid);
|
||||
return timestamp != null ? timestamp : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void clearUsageCount() {
|
||||
_prefs.edit().remove("pref_usage_count").apply();
|
||||
}
|
||||
|
||||
public Map<UUID, Long> getLastUsedTimestamps() {
|
||||
Map<UUID, Long> lastUsedTimestamps = new HashMap<>();
|
||||
String lastUsedTimestamp = _prefs.getString("pref_last_used_timestamps", "");
|
||||
try {
|
||||
JSONArray arr = new JSONArray(lastUsedTimestamp);
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
JSONObject json = arr.getJSONObject(i);
|
||||
lastUsedTimestamps.put(UUID.fromString(json.getString("uuid")), json.getLong("timestamp"));
|
||||
}
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
|
||||
return lastUsedTimestamps;
|
||||
}
|
||||
|
||||
public void setLastUsedTimestamps(Map<UUID, Long> lastUsedTimestamps) {
|
||||
JSONArray lastUsedTimestampJson = new JSONArray();
|
||||
for (Map.Entry<UUID, Long> entry : lastUsedTimestamps.entrySet()) {
|
||||
JSONObject entryJson = new JSONObject();
|
||||
try {
|
||||
entryJson.put("uuid", entry.getKey());
|
||||
entryJson.put("timestamp", entry.getValue());
|
||||
lastUsedTimestampJson.put(entryJson);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
_prefs.edit().putString("pref_last_used_timestamps", lastUsedTimestampJson.toString()).apply();
|
||||
}
|
||||
|
||||
public Map<UUID, Integer> getUsageCounts() {
|
||||
Map<UUID, Integer> usageCounts = new HashMap<>();
|
||||
String usageCount = _prefs.getString("pref_usage_count", "");
|
||||
|
@ -263,8 +361,16 @@ public class Preferences {
|
|||
return _prefs.getInt("pref_timeout", -1);
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return _prefs.getString("pref_lang", "system");
|
||||
}
|
||||
|
||||
public void setLanguage(String lang) {
|
||||
_prefs.edit().putString("pref_lang", lang).apply();
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
String lang = _prefs.getString("pref_lang", "system");
|
||||
String lang = getLanguage();
|
||||
|
||||
if (lang.equals("system")) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
@ -478,26 +584,39 @@ public class Preferences {
|
|||
return _prefs.getBoolean("pref_minimize_on_copy", false);
|
||||
}
|
||||
|
||||
public void setGroupFilter(List<String> groupFilter) {
|
||||
public void setGroupFilter(Set<UUID> groupFilter) {
|
||||
JSONArray json = new JSONArray(groupFilter);
|
||||
_prefs.edit().putString("pref_group_filter", json.toString()).apply();
|
||||
_prefs.edit().putString("pref_group_filter_uuids", json.toString()).apply();
|
||||
}
|
||||
|
||||
public List<String> getGroupFilter() {
|
||||
String raw = _prefs.getString("pref_group_filter", null);
|
||||
public Set<UUID> getGroupFilter() {
|
||||
String raw = _prefs.getString("pref_group_filter_uuids", null);
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
try {
|
||||
JSONArray json = new JSONArray(raw);
|
||||
List<String> filter = new ArrayList<>();
|
||||
Set<UUID> filter = new HashSet<>();
|
||||
for (int i = 0; i < json.length(); i++) {
|
||||
filter.add(json.isNull(i) ? null : json.optString(i));
|
||||
filter.add(json.isNull(i) ? null : UUID.fromString(json.getString(i)));
|
||||
}
|
||||
return filter;
|
||||
} catch (JSONException e) {
|
||||
return Collections.emptyList();
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public BackupsVersioningStrategy getBackupVersioningStrategy() {
|
||||
Uri uri = getBackupsLocation();
|
||||
if (uri == null) {
|
||||
return BackupsVersioningStrategy.UNDEFINED;
|
||||
}
|
||||
if (DocumentsContractCompat.isTreeUri(uri)) {
|
||||
return BackupsVersioningStrategy.MULTIPLE_BACKUPS;
|
||||
} else {
|
||||
return BackupsVersioningStrategy.SINGLE_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -505,14 +624,16 @@ public class Preferences {
|
|||
private final Date _time;
|
||||
private boolean _isBuiltIn;
|
||||
private final String _error;
|
||||
private final boolean _isPermissionError;
|
||||
|
||||
public BackupResult(@Nullable Exception e) {
|
||||
this(new Date(), e == null ? null : e.toString());
|
||||
this(new Date(), e == null ? null : e.toString(), e instanceof VaultBackupPermissionException);
|
||||
}
|
||||
|
||||
private BackupResult(Date time, @Nullable String error) {
|
||||
private BackupResult(Date time, @Nullable String error, boolean isPermissionError) {
|
||||
_time = time;
|
||||
_error = error;
|
||||
_isPermissionError = isPermissionError;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -540,12 +661,17 @@ public class Preferences {
|
|||
_isBuiltIn = isBuiltIn;
|
||||
}
|
||||
|
||||
public boolean isPermissionError() {
|
||||
return _isPermissionError;
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try {
|
||||
obj.put("time", _time.getTime());
|
||||
obj.put("error", _error == null ? JSONObject.NULL : _error);
|
||||
obj.put("isPermissionError", _isPermissionError);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
@ -557,7 +683,8 @@ public class Preferences {
|
|||
JSONObject obj = new JSONObject(json);
|
||||
long time = obj.getLong("time");
|
||||
String error = JsonUtils.optString(obj, "error");
|
||||
return new BackupResult(new Date(time), error);
|
||||
boolean isPermissionError = obj.optBoolean("isPermissionError");
|
||||
return new BackupResult(new Date(time), error, isPermissionError);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import com.beemdevelopment.aegis.helpers.comparators.LastUsedComparator;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.UsageCountComparator;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
|
||||
|
@ -14,7 +15,8 @@ public enum SortCategory {
|
|||
ACCOUNT_REVERSED,
|
||||
ISSUER,
|
||||
ISSUER_REVERSED,
|
||||
USAGE_COUNT;
|
||||
USAGE_COUNT,
|
||||
LAST_USED;
|
||||
|
||||
private static SortCategory[] _values;
|
||||
|
||||
|
@ -31,20 +33,22 @@ public enum SortCategory {
|
|||
|
||||
switch (this) {
|
||||
case ACCOUNT:
|
||||
comparator = new AccountNameComparator();
|
||||
comparator = new AccountNameComparator().thenComparing(new IssuerNameComparator());
|
||||
break;
|
||||
case ACCOUNT_REVERSED:
|
||||
comparator = Collections.reverseOrder(new AccountNameComparator());
|
||||
comparator = Collections.reverseOrder(new AccountNameComparator().thenComparing(new IssuerNameComparator()));
|
||||
break;
|
||||
case ISSUER:
|
||||
comparator = new IssuerNameComparator();
|
||||
comparator = new IssuerNameComparator().thenComparing(new AccountNameComparator());
|
||||
break;
|
||||
case ISSUER_REVERSED:
|
||||
comparator = Collections.reverseOrder(new IssuerNameComparator());
|
||||
comparator = Collections.reverseOrder(new IssuerNameComparator().thenComparing(new AccountNameComparator()));
|
||||
break;
|
||||
case USAGE_COUNT:
|
||||
comparator = Collections.reverseOrder(new UsageCountComparator());
|
||||
break;
|
||||
case LAST_USED:
|
||||
comparator = Collections.reverseOrder(new LastUsedComparator());
|
||||
}
|
||||
|
||||
return comparator;
|
||||
|
@ -64,6 +68,8 @@ public enum SortCategory {
|
|||
return R.id.menu_sort_alphabetically_reverse;
|
||||
case USAGE_COUNT:
|
||||
return R.id.menu_sort_usage_count;
|
||||
case LAST_USED:
|
||||
return R.id.menu_sort_last_used;
|
||||
default:
|
||||
return R.id.menu_sort_custom;
|
||||
}
|
||||
|
|
|
@ -10,20 +10,8 @@ public class ThemeMap {
|
|||
}
|
||||
|
||||
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_Default,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_Default,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Default
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> NO_ACTION_BAR = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_NoActionBar,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_NoActionBar,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_NoActionBar
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_Fullscreen,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_Fullscreen,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Fullscreen
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_Amoled
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class VibrationPatterns {
|
||||
public static final long[] EXPIRING = {475, 20, 5, 20, 965, 20, 5, 20, 965, 20, 5, 20, 420};
|
||||
public static final long[] REFRESH_CODE = {0, 100};
|
||||
|
||||
public static long getLengthInMillis(long[] pattern) {
|
||||
return Arrays.stream(pattern).sum();
|
||||
}
|
||||
}
|
|
@ -35,19 +35,19 @@ public enum ViewMode {
|
|||
}
|
||||
|
||||
/**
|
||||
* Retrieves the height (in dp) that the divider between entries should have in this view mode.
|
||||
* Retrieves the offset (in dp) that should exist between entries in this view mode.
|
||||
*/
|
||||
public float getDividerHeight() {
|
||||
public float getItemOffset() {
|
||||
if (this == ViewMode.COMPACT) {
|
||||
return 0;
|
||||
return 1;
|
||||
} else if (this == ViewMode.TILES) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
return 20;
|
||||
return 8;
|
||||
}
|
||||
|
||||
public int getColumnSpan() {
|
||||
public int getSpanCount() {
|
||||
if (this == ViewMode.TILES) {
|
||||
return 2;
|
||||
}
|
||||
|
@ -55,14 +55,6 @@ public enum ViewMode {
|
|||
return 1;
|
||||
}
|
||||
|
||||
public float getDividerWidth() {
|
||||
if (this == ViewMode.TILES) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public String getFormattedAccountName(String accountName) {
|
||||
if (this == ViewMode.TILES) {
|
||||
return accountName;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.bc.SCrypt;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
@ -23,7 +21,6 @@ import javax.crypto.KeyGenerator;
|
|||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class CryptoUtils {
|
||||
|
@ -66,13 +63,7 @@ public class CryptoUtils {
|
|||
// generate the nonce if none is given
|
||||
// we are not allowed to do this ourselves as "setRandomizedEncryptionRequired" is set to true
|
||||
if (nonce != null) {
|
||||
AlgorithmParameterSpec spec;
|
||||
// apparently kitkat doesn't support GCMParameterSpec
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
spec = new IvParameterSpec(nonce);
|
||||
} else {
|
||||
spec = new GCMParameterSpec(CRYPTO_AEAD_TAG_SIZE * 8, nonce);
|
||||
}
|
||||
AlgorithmParameterSpec spec = new GCMParameterSpec(CRYPTO_AEAD_TAG_SIZE * 8, nonce);
|
||||
cipher.init(opmode, key, spec);
|
||||
} else {
|
||||
cipher.init(opmode, key);
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
|
@ -45,10 +42,6 @@ public class KeyStoreHandle {
|
|||
}
|
||||
|
||||
public SecretKey generateKey(String id) throws KeyStoreHandleException {
|
||||
if (!isSupported()) {
|
||||
throw new KeyStoreHandleException("Symmetric KeyStore keys are not supported in this version of Android");
|
||||
}
|
||||
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME);
|
||||
generator.init(new KeyGenParameterSpec.Builder(id,
|
||||
|
@ -87,14 +80,13 @@ public class KeyStoreHandle {
|
|||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
|
||||
if (isSupported() && isKeyPermanentlyInvalidated(key)) {
|
||||
if (isKeyPermanentlyInvalidated(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
|
||||
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
|
||||
try {
|
||||
|
@ -127,8 +119,4 @@ public class KeyStoreHandle {
|
|||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,10 @@ public class YAOTP {
|
|||
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||
long seconds = System.currentTimeMillis() / 1000;
|
||||
return generateOTP(secret, pin, digits, otpAlgo, seconds, period);
|
||||
return generateOTP(secret, pin, digits, otpAlgo, period, seconds);
|
||||
}
|
||||
|
||||
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long seconds, long period)
|
||||
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period, long seconds)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||
byte[] pinWithHash;
|
||||
byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8);
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.beemdevelopment.aegis.database;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.room.Database;
|
||||
import androidx.room.Room;
|
||||
import androidx.room.RoomDatabase;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Database(entities = {AuditLogEntry.class}, version = 1)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
public abstract AuditLogDao auditLogDao();
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.beemdevelopment.aegis.database;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
public interface AuditLogDao {
|
||||
@Insert
|
||||
void insert(AuditLogEntry log);
|
||||
|
||||
@Query("SELECT * FROM audit_logs WHERE timestamp >= strftime('%s', 'now', '-30 days') ORDER BY timestamp DESC")
|
||||
LiveData<List<AuditLogEntry>> getAll();
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package com.beemdevelopment.aegis.database;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import com.beemdevelopment.aegis.EventType;
|
||||
|
||||
@Entity(tableName = "audit_logs")
|
||||
public class AuditLogEntry {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
protected long id;
|
||||
|
||||
@NonNull
|
||||
@ColumnInfo(name = "event_type")
|
||||
private final EventType _eventType;
|
||||
|
||||
@ColumnInfo(name = "reference")
|
||||
private final String _reference;
|
||||
|
||||
@ColumnInfo(name = "timestamp")
|
||||
private final long _timestamp;
|
||||
|
||||
@Ignore
|
||||
public AuditLogEntry(@NonNull EventType eventType) {
|
||||
this(eventType, null);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public AuditLogEntry(@NonNull EventType eventType, @Nullable String reference) {
|
||||
_eventType = eventType;
|
||||
_reference = reference;
|
||||
_timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
AuditLogEntry(long id, @NonNull EventType eventType, @Nullable String reference, long timestamp) {
|
||||
this.id = id;
|
||||
_eventType = eventType;
|
||||
_reference = reference;
|
||||
_timestamp = timestamp;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public EventType getEventType() {
|
||||
return _eventType;
|
||||
}
|
||||
|
||||
public String getReference() {
|
||||
return _reference;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return _timestamp;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package com.beemdevelopment.aegis.database;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.beemdevelopment.aegis.EventType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class AuditLogRepository {
|
||||
private final AuditLogDao _auditLogDao;
|
||||
private final Executor _executor;
|
||||
|
||||
public AuditLogRepository(AuditLogDao auditLogDao) {
|
||||
_auditLogDao = auditLogDao;
|
||||
_executor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
public LiveData<List<AuditLogEntry>> getAllAuditLogEntries() {
|
||||
return _auditLogDao.getAll();
|
||||
}
|
||||
|
||||
public void addVaultUnlockedEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCKED);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void addBackupCreatedEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_BACKUP_CREATED);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void addAndroidBackupCreatedEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_ANDROID_BACKUP_CREATED);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void addVaultExportedEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_EXPORTED);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void addEntrySharedEvent(String reference) {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.ENTRY_SHARED, reference);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void addVaultUnlockFailedPasswordEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_PASSWORD);
|
||||
insert(auditLogEntry);
|
||||
|
||||
}
|
||||
|
||||
public void addVaultUnlockFailedBiometricsEvent() {
|
||||
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_BIOMETRICS);
|
||||
insert(auditLogEntry);
|
||||
}
|
||||
|
||||
public void insert(AuditLogEntry auditLogEntry) {
|
||||
_executor.execute(() -> {
|
||||
_auditLogDao.insert(auditLogEntry);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,13 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BitmapHelper {
|
||||
private BitmapHelper() {
|
||||
|
@ -28,4 +35,29 @@ public class BitmapHelper {
|
|||
|
||||
return Bitmap.createScaledBitmap(bitmap, width, height, true);
|
||||
}
|
||||
|
||||
public static boolean isVaultEntryIconOptimized(VaultEntryIcon icon) {
|
||||
BitmapFactory.Options opts = new BitmapFactory.Options();
|
||||
opts.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeByteArray(icon.getBytes(), 0, icon.getBytes().length, opts);
|
||||
return opts.outWidth <= VaultEntryIcon.MAX_DIMENS && opts.outHeight <= VaultEntryIcon.MAX_DIMENS;
|
||||
}
|
||||
|
||||
public static VaultEntryIcon toVaultEntryIcon(Bitmap bitmap, IconType iconType) {
|
||||
if (bitmap.getWidth() > VaultEntryIcon.MAX_DIMENS
|
||||
|| bitmap.getHeight() > VaultEntryIcon.MAX_DIMENS) {
|
||||
bitmap = resize(bitmap, VaultEntryIcon.MAX_DIMENS, VaultEntryIcon.MAX_DIMENS);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
if (Objects.equals(iconType, IconType.PNG)) {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
} else {
|
||||
iconType = IconType.JPEG;
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
}
|
||||
|
||||
byte[] data = stream.toByteArray();
|
||||
return new VaultEntryIcon(data, iconType);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class CenterVerticalSpan extends MetricAffectingSpan {
|
||||
Rect _substringBounds;
|
||||
|
||||
public CenterVerticalSpan(Rect substringBounds) {
|
||||
_substringBounds = substringBounds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(@NonNull TextPaint textPaint) {
|
||||
applyBaselineShift(textPaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint textPaint) {
|
||||
applyBaselineShift(textPaint);
|
||||
}
|
||||
|
||||
private void applyBaselineShift(TextPaint textPaint) {
|
||||
float topDifference = textPaint.getFontMetrics().top - _substringBounds.top;
|
||||
textPaint.baselineShift -= (topDifference / 2f);
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.os.Build;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
|
||||
public class IconViewHelper {
|
||||
private IconViewHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layer type of the given ImageView based on the given IconType. If the
|
||||
* icon type is SVG and SDK <= 27, the layer type is set to software. Otherwise, it
|
||||
* is set to hardware.
|
||||
*/
|
||||
public static void setLayerType(ImageView view, IconType iconType) {
|
||||
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
|
||||
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
|
||||
return;
|
||||
}
|
||||
|
||||
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
}
|
|
@ -1,14 +1,60 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.google.common.base.Strings;
|
||||
import com.nulabinc.zxcvbn.Strength;
|
||||
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||
|
||||
public class PasswordStrengthHelper {
|
||||
// Material design color palette
|
||||
private static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
|
||||
// Limit the password length to prevent zxcvbn4j from exploding
|
||||
private static final int MAX_PASSWORD_LENGTH = 64;
|
||||
|
||||
public static String getString(int score, Context context) {
|
||||
// Material design color palette
|
||||
private final static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
|
||||
|
||||
private final Zxcvbn _zxcvbn = new Zxcvbn();
|
||||
private final EditText _textPassword;
|
||||
private final ProgressBar _barPasswordStrength;
|
||||
private final TextView _textPasswordStrength;
|
||||
private final TextInputLayout _textPasswordWrapper;
|
||||
|
||||
public PasswordStrengthHelper(
|
||||
EditText textPassword,
|
||||
ProgressBar barPasswordStrength,
|
||||
TextView textPasswordStrength,
|
||||
TextInputLayout textPasswordWrapper
|
||||
) {
|
||||
_textPassword = textPassword;
|
||||
_barPasswordStrength = barPasswordStrength;
|
||||
_textPasswordStrength = textPasswordStrength;
|
||||
_textPasswordWrapper = textPasswordWrapper;
|
||||
}
|
||||
|
||||
public void measure(Context context) {
|
||||
if (_textPassword.getText().length() > MAX_PASSWORD_LENGTH) {
|
||||
_barPasswordStrength.setProgress(0);
|
||||
_textPasswordStrength.setText(R.string.password_strength_unknown);
|
||||
} else {
|
||||
Strength strength = _zxcvbn.measure(_textPassword.getText());
|
||||
_barPasswordStrength.setProgress(strength.getScore());
|
||||
_barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(getColor(strength.getScore()))));
|
||||
_textPasswordStrength.setText((_textPassword.getText().length() != 0) ? getString(strength.getScore(), context) : "");
|
||||
String warning = strength.getFeedback().getWarning();
|
||||
_textPasswordWrapper.setError(warning);
|
||||
_textPasswordWrapper.setErrorEnabled(!Strings.isNullOrEmpty(warning));
|
||||
strength.wipe();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getString(int score, Context context) {
|
||||
if (score < 0 || score > 4) {
|
||||
throw new IllegalArgumentException("Not a valid zxcvbn score");
|
||||
}
|
||||
|
@ -17,7 +63,7 @@ public class PasswordStrengthHelper {
|
|||
return strings[score];
|
||||
}
|
||||
|
||||
public static String getColor(int score) {
|
||||
private static String getColor(int score) {
|
||||
if (score < 0 || score > 4) {
|
||||
throw new IllegalArgumentException("Not a valid zxcvbn score");
|
||||
}
|
||||
|
|
|
@ -56,16 +56,20 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||
// It's not clear when this can happen, but sometimes the ViewHolder
|
||||
// that's passed to this function has a position of -1, leading
|
||||
// to a crash down the line.
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
int position = viewHolder.getBindingAdapterPosition();
|
||||
if (position == NO_POSITION) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int swipeFlags = 0;
|
||||
|
||||
EntryAdapter adapter = (EntryAdapter) recyclerView.getAdapter();
|
||||
if (adapter == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int swipeFlags = 0;
|
||||
if (adapter.isPositionFooter(position)
|
||||
|| adapter.getEntryAt(position) != _selectedEntry
|
||||
|| adapter.isPositionErrorCard(position)
|
||||
|| adapter.getEntryAtPosition(position) != _selectedEntry
|
||||
|| !isLongPressDragEnabled()) {
|
||||
return makeMovementFlags(0, swipeFlags);
|
||||
}
|
||||
|
@ -76,12 +80,13 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
|
||||
RecyclerView.ViewHolder target) {
|
||||
if (target.getAdapterPosition() < _adapter.getShownFavoritesCount()){
|
||||
int targetIndex = _adapter.translateEntryPosToIndex(target.getBindingAdapterPosition());
|
||||
if (targetIndex < _adapter.getShownFavoritesCount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int firstPosition = viewHolder.getLayoutPosition();
|
||||
int secondPosition = target.getAdapterPosition();
|
||||
int secondPosition = target.getBindingAdapterPosition();
|
||||
|
||||
_adapter.onItemMove(firstPosition, secondPosition);
|
||||
_positionChanged = true;
|
||||
|
@ -90,7 +95,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
_adapter.onItemDismiss(viewHolder.getAdapterPosition());
|
||||
_adapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -98,7 +103,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||
super.clearView(recyclerView, viewHolder);
|
||||
|
||||
if (_positionChanged) {
|
||||
_adapter.onItemDrop(viewHolder.getAdapterPosition());
|
||||
_adapter.onItemDrop(viewHolder.getBindingAdapterPosition());
|
||||
_positionChanged = false;
|
||||
_adapter.refresh(false);
|
||||
}
|
||||
|
|
|
@ -1,23 +1,58 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.util.TypedValue;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.google.android.material.color.DynamicColors;
|
||||
import com.google.android.material.color.DynamicColorsOptions;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ThemeHelper {
|
||||
private ThemeHelper() {
|
||||
private final AppCompatActivity _activity;
|
||||
private final Preferences _prefs;
|
||||
|
||||
public ThemeHelper(AppCompatActivity activity, Preferences prefs) {
|
||||
_activity = activity;
|
||||
_prefs = prefs;
|
||||
}
|
||||
|
||||
public static int getThemeColor(int attributeId, Resources.Theme currentTheme) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
currentTheme.resolveAttribute(attributeId, typedValue, true);
|
||||
@ColorInt int color = typedValue.data;
|
||||
/**
|
||||
* Sets the theme of the activity. The actual style that is set is picked from the
|
||||
* given map, based on the theme configured by the user.
|
||||
*/
|
||||
public void setTheme(Map<Theme, Integer> themeMap) {
|
||||
int theme = themeMap.get(getConfiguredTheme());
|
||||
_activity.setTheme(theme);
|
||||
|
||||
return color;
|
||||
if (_prefs.isDynamicColorsEnabled()) {
|
||||
DynamicColorsOptions.Builder optsBuilder = new DynamicColorsOptions.Builder();
|
||||
if (getConfiguredTheme().equals(Theme.AMOLED)) {
|
||||
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Amoled);
|
||||
} else if (getConfiguredTheme().equals(Theme.DARK)) {
|
||||
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Dark);
|
||||
}
|
||||
|
||||
DynamicColors.applyToActivityIfAvailable(_activity, optsBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
public Theme getConfiguredTheme() {
|
||||
Theme theme = _prefs.getCurrentTheme();
|
||||
|
||||
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
|
||||
int currentNightMode = _activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
|
||||
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
|
||||
} else {
|
||||
theme = Theme.LIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.helpers;
|
|||
|
||||
import android.os.Handler;
|
||||
|
||||
import com.beemdevelopment.aegis.VibrationPatterns;
|
||||
|
||||
public class UiRefresher {
|
||||
private boolean _running;
|
||||
private Listener _listener;
|
||||
|
@ -23,7 +25,6 @@ public class UiRefresher {
|
|||
}
|
||||
_running = true;
|
||||
|
||||
_listener.onRefresh();
|
||||
_handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
@ -31,6 +32,27 @@ public class UiRefresher {
|
|||
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
|
||||
}
|
||||
}, _listener.getMillisTillNextRefresh());
|
||||
|
||||
_handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
_listener.onExpiring();
|
||||
_handler.postDelayed(this, getNextRun());
|
||||
}
|
||||
}, getInitialRun());
|
||||
}
|
||||
|
||||
private long getInitialRun() {
|
||||
long sum = _listener.getMillisTillNextRefresh() - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
|
||||
if (sum < 0) {
|
||||
return getNextRun();
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
private long getNextRun() {
|
||||
return (_listener.getMillisTillNextRefresh() + _listener.getPeriodMillis()) - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
|
@ -40,6 +62,8 @@ public class UiRefresher {
|
|||
|
||||
public interface Listener {
|
||||
void onRefresh();
|
||||
void onExpiring();
|
||||
long getMillisTillNextRefresh();
|
||||
long getPeriodMillis();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.os.VibratorManager;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
|
||||
public class VibrationHelper {
|
||||
private Preferences _preferences;
|
||||
|
||||
public VibrationHelper(Context context) {
|
||||
_preferences = new Preferences(context);
|
||||
}
|
||||
|
||||
public void vibratePattern(Context context, long[] pattern) {
|
||||
if (!isHapticFeedbackEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
VibratorManager vibratorManager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
|
||||
if (vibratorManager != null) {
|
||||
Vibrator vibrator = vibratorManager.getDefaultVibrator();
|
||||
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
|
||||
vibrator.vibrate(effect);
|
||||
}
|
||||
} else {
|
||||
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
if (vibrator != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
|
||||
vibrator.vibrate(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isHapticFeedbackEnabled() {
|
||||
return _preferences.isHapticFeedbackEnabled();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
|
||||
public class ViewHelper {
|
||||
private ViewHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static void setupAppBarInsets(AppBarLayout appBar) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(appBar, (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
targetView.setPadding(
|
||||
insets.left,
|
||||
insets.top,
|
||||
insets.right,
|
||||
0
|
||||
);
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.beemdevelopment.aegis.helpers.comparators;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class LastUsedComparator implements Comparator<VaultEntry> {
|
||||
@Override
|
||||
public int compare(VaultEntry a, VaultEntry b) {
|
||||
return Long.compare(a.getLastUsedTimestamp(), b.getLastUsedTimestamp());
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package com.beemdevelopment.aegis.icons;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
|
@ -17,7 +18,6 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IconPack {
|
||||
private UUID _uuid;
|
||||
|
@ -58,9 +58,21 @@ public class IconPack {
|
|||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return _icons.stream()
|
||||
.filter(i -> i.isSuggestedFor(issuer))
|
||||
.collect(Collectors.toList());
|
||||
List<Icon> icons = new ArrayList<>();
|
||||
for (Icon icon : _icons) {
|
||||
MatchType matchType = icon.getMatchFor(issuer);
|
||||
if (matchType != null) {
|
||||
// Inverse matches (entry issuer contains icon name) are less likely
|
||||
// to be good, so position them at the end of the list.
|
||||
if (matchType.equals(MatchType.NORMAL)) {
|
||||
icons.add(0, icon);
|
||||
} else if (matchType.equals(MatchType.INVERSE)) {
|
||||
icons.add(icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return icons;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -120,13 +132,15 @@ public class IconPack {
|
|||
|
||||
public static class Icon implements Serializable {
|
||||
private final String _relFilename;
|
||||
private final String _name;
|
||||
private final String _category;
|
||||
private final List<String> _issuers;
|
||||
|
||||
private File _file;
|
||||
|
||||
protected Icon(String filename, String category, List<String> issuers) {
|
||||
protected Icon(String filename, String name, String category, List<String> issuers) {
|
||||
_relFilename = filename;
|
||||
_name = name;
|
||||
_category = category;
|
||||
_issuers = issuers;
|
||||
}
|
||||
|
@ -149,6 +163,9 @@ public class IconPack {
|
|||
}
|
||||
|
||||
public String getName() {
|
||||
if (_name != null) {
|
||||
return _name;
|
||||
}
|
||||
return Files.getNameWithoutExtension(new File(_relFilename).getName());
|
||||
}
|
||||
|
||||
|
@ -156,19 +173,29 @@ public class IconPack {
|
|||
return _category;
|
||||
}
|
||||
|
||||
public List<String> getIssuers() {
|
||||
return Collections.unmodifiableList(_issuers);
|
||||
}
|
||||
private MatchType getMatchFor(String issuer) {
|
||||
String lowerEntryIssuer = issuer.toLowerCase();
|
||||
|
||||
public boolean isSuggestedFor(String issuer) {
|
||||
String lowerIssuer = issuer.toLowerCase();
|
||||
return getIssuers().stream()
|
||||
.map(String::toLowerCase)
|
||||
.anyMatch(is -> is.contains(lowerIssuer) || lowerIssuer.contains(is));
|
||||
boolean inverseMatch = false;
|
||||
for (String is : _issuers) {
|
||||
String lowerIconIssuer = is.toLowerCase();
|
||||
if (lowerIconIssuer.contains(lowerEntryIssuer)) {
|
||||
return MatchType.NORMAL;
|
||||
}
|
||||
if (lowerEntryIssuer.contains(lowerIconIssuer)) {
|
||||
inverseMatch = true;
|
||||
}
|
||||
}
|
||||
if (inverseMatch) {
|
||||
return MatchType.INVERSE;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Icon fromJson(JSONObject obj) throws JSONException {
|
||||
String filename = obj.getString("filename");
|
||||
String name = JsonUtils.optString(obj, "name");
|
||||
String category = obj.isNull("category") ? null : obj.getString("category");
|
||||
JSONArray array = obj.getJSONArray("issuer");
|
||||
|
||||
|
@ -178,7 +205,12 @@ public class IconPack {
|
|||
issuers.add(issuer);
|
||||
}
|
||||
|
||||
return new Icon(filename, category, issuers);
|
||||
return new Icon(filename, name, category, issuers);
|
||||
}
|
||||
}
|
||||
|
||||
private enum MatchType {
|
||||
NORMAL,
|
||||
INVERSE
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,10 @@ public class IconPackManager {
|
|||
return packs.get(0);
|
||||
}
|
||||
|
||||
public boolean hasIconPack() {
|
||||
return _iconPacks.size() > 0;
|
||||
}
|
||||
|
||||
public List<IconPack> getIconPacks() {
|
||||
return new ArrayList<>(_iconPacks);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.beemdevelopment.aegis.vault.VaultEntryException;
|
|||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
@ -27,6 +28,7 @@ import org.json.JSONObject;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AegisImporter extends DatabaseImporter {
|
||||
|
||||
|
@ -132,11 +134,31 @@ public class AegisImporter extends DatabaseImporter {
|
|||
Result result = new Result();
|
||||
|
||||
try {
|
||||
JSONArray array = _obj.getJSONArray("entries");
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject entryObj = array.getJSONObject(i);
|
||||
if (_obj.has("groups")) {
|
||||
JSONArray groupArray = _obj.getJSONArray("groups");
|
||||
for (int i = 0; i < groupArray.length(); i++) {
|
||||
JSONObject groupObj = groupArray.getJSONObject(i);
|
||||
try {
|
||||
VaultGroup group = convertGroup(groupObj);
|
||||
if (!result.getGroups().has(group)) {
|
||||
result.addGroup(group);
|
||||
}
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray entryArray = _obj.getJSONArray("entries");
|
||||
for (int i = 0; i < entryArray.length(); i++) {
|
||||
JSONObject entryObj = entryArray.getJSONObject(i);
|
||||
try {
|
||||
VaultEntry entry = convertEntry(entryObj);
|
||||
for (UUID groupUuid : entry.getGroups()) {
|
||||
if (!result.getGroups().has(groupUuid)) {
|
||||
entry.getGroups().remove(groupUuid);
|
||||
}
|
||||
}
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
|
@ -156,5 +178,13 @@ public class AegisImporter extends DatabaseImporter {
|
|||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static VaultGroup convertGroup(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
return VaultGroup.fromJson(obj);
|
||||
} catch (VaultEntryException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
|||
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
|
@ -182,7 +183,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
|||
context.getResources().getString(R.string.andotp_old_format)
|
||||
};
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.choose_andotp_importer)
|
||||
.setSingleChoiceItems(choices, 0, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
|
|
|
@ -75,8 +75,13 @@ public class AuthyImporter extends DatabaseImporter {
|
|||
JSONArray array;
|
||||
JSONArray authyArray;
|
||||
try {
|
||||
array = readFile(new SuFile(path, String.format("%s.xml", _authFilename)), String.format("%s.key", _authFilename));
|
||||
authyArray = readFile(new SuFile(path, String.format("%s.xml", _authyFilename)), String.format("%s.key", _authyFilename));
|
||||
SuFile file1 = new SuFile(path, String.format("%s.xml", _authFilename));
|
||||
file1.setShell(shell);
|
||||
SuFile file2 = new SuFile(path, String.format("%s.xml", _authyFilename));
|
||||
file2.setShell(shell);
|
||||
|
||||
array = readFile(file1, String.format("%s.key", _authFilename));
|
||||
authyArray = readFile(file2, String.format("%s.key", _authyFilename));
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
|
|||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.common.base.Strings;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
|
@ -18,12 +19,10 @@ import org.xmlpull.v1.XmlPullParserException;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BattleNetImporter extends DatabaseImporter {
|
||||
private static final String _pkgName = "com.blizzard.bma";
|
||||
private static final String _subPath = "shared_prefs/com.blizzard.bma.AUTH_STORE.xml";
|
||||
private static final String _pkgName = "com.blizzard.messenger";
|
||||
private static final String _subPath = "shared_prefs/com.blizzard.messenger.authenticator_preferences.xml";
|
||||
|
||||
private static final byte[] _key;
|
||||
|
||||
|
@ -46,71 +45,80 @@ public class BattleNetImporter extends DatabaseImporter {
|
|||
|
||||
@Override
|
||||
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
final String serialKey = "com.blizzard.messenger.AUTHENTICATOR_SERIAL";
|
||||
final String secretKey = "com.blizzard.messenger.AUTHENTICATOR_DEVICE_SECRET";
|
||||
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(stream, null);
|
||||
parser.nextTag();
|
||||
|
||||
List<String> entries = new ArrayList<>();
|
||||
String serial = "";
|
||||
String secretValue = null;
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (entry.Name.equals("com.blizzard.bma.AUTH_STORE.HASH")) {
|
||||
entries.add(entry.Value);
|
||||
break;
|
||||
if (entry.Name.equals(secretKey)) {
|
||||
secretValue = entry.Value;
|
||||
} else if (entry.Name.equals(serialKey)) {
|
||||
serial = entry.Value;
|
||||
}
|
||||
}
|
||||
return new BattleNetImporter.State(entries);
|
||||
|
||||
if (secretValue == null) {
|
||||
throw new DatabaseImporterException(String.format("Key not found: %s", secretKey));
|
||||
}
|
||||
|
||||
return new BattleNetImporter.State(serial, secretValue);
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private final List<String> _entries;
|
||||
private final String _serial;
|
||||
private final String _secretValue;
|
||||
|
||||
public State(List<String> entries) {
|
||||
public State(String serial, String secretValue) {
|
||||
super(false);
|
||||
_entries = entries;
|
||||
_serial = serial;
|
||||
_secretValue = secretValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
for (String str : _entries) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(str);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
try {
|
||||
VaultEntry entry = convertEntry(_serial, _secretValue);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static VaultEntry convertEntry(String hashString) throws DatabaseImporterEntryException {
|
||||
private static VaultEntry convertEntry(String serial, String secretString) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] hash = Hex.decode(hashString);
|
||||
if (hash.length != _key.length) {
|
||||
throw new DatabaseImporterEntryException(String.format("Unexpected hash length: %d", hash.length), hashString);
|
||||
if (!Strings.isNullOrEmpty(serial)) {
|
||||
serial = unmask(serial);
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < hash.length; i++) {
|
||||
char c = (char) (hash[i] ^ _key[i]);
|
||||
sb.append(c);
|
||||
}
|
||||
|
||||
final int secretLen = 40;
|
||||
byte[] secret = Hex.decode(sb.substring(0, secretLen));
|
||||
String serial = sb.substring(secretLen);
|
||||
|
||||
byte[] secret = Hex.decode(unmask(secretString));
|
||||
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, 8, TotpInfo.DEFAULT_PERIOD);
|
||||
return new VaultEntry(info, serial, "Battle.net");
|
||||
} catch (OtpInfoException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, hashString);
|
||||
throw new DatabaseImporterEntryException(e, secretString);
|
||||
}
|
||||
}
|
||||
|
||||
private static String unmask(String s) throws EncodingException {
|
||||
byte[] ds = Hex.decode(s);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < ds.length; i++) {
|
||||
char c = (char) (ds[i] ^ _key[i]);
|
||||
sb.append(c);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import androidx.annotation.StringRes;
|
|||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
@ -33,17 +34,18 @@ public abstract class DatabaseImporter {
|
|||
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
|
||||
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
|
||||
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
|
||||
_importers.add(new Definition("Authenticator Pro", AuthenticatorProImporter.class, R.string.importer_help_authenticator_pro, true));
|
||||
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
|
||||
_importers.add(new Definition("Battle.net Authenticator", BattleNetImporter.class, R.string.importer_help_battle_net_authenticator, true));
|
||||
_importers.add(new Definition("Bitwarden", BitwardenImporter.class, R.string.importer_help_bitwarden, false));
|
||||
_importers.add(new Definition("Duo", DuoImporter.class, R.string.importer_help_duo, true));
|
||||
_importers.add(new Definition("Ente Auth", EnteAuthImporter.class, R.string.importer_help_ente_auth, false));
|
||||
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
|
||||
_importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
|
||||
_importers.add(new Definition("FreeOTP+ (JSON)", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
|
||||
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
|
||||
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
|
||||
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
|
||||
_importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true));
|
||||
_importers.add(new Definition("Stratum (Authenticator Pro)", StratumImporter.class, R.string.importer_help_stratum, true));
|
||||
_importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
|
||||
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
|
||||
}
|
||||
|
@ -168,12 +170,17 @@ public abstract class DatabaseImporter {
|
|||
|
||||
public static class Result {
|
||||
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||
private UUIDMap<VaultGroup> _groups = new UUIDMap<>();
|
||||
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
public void addGroup(VaultGroup group) {
|
||||
_groups.add(group);
|
||||
}
|
||||
|
||||
public void addError(DatabaseImporterEntryException error) {
|
||||
_errors.add(error);
|
||||
}
|
||||
|
@ -182,6 +189,10 @@ public abstract class DatabaseImporter {
|
|||
return _entries;
|
||||
}
|
||||
|
||||
public UUIDMap<VaultGroup> getGroups() {
|
||||
return _groups;
|
||||
}
|
||||
|
||||
public List<DatabaseImporterEntryException> getErrors() {
|
||||
return _errors;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class EnteAuthImporter extends DatabaseImporter {
|
||||
public EnteAuthImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] bytes = IOUtils.readAll(stream);
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(requireContext());
|
||||
return importer.read(new ByteArrayInputStream(bytes), isInternal);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,26 +4,53 @@ import android.content.Context;
|
|||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.ContextHelper;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Encodable;
|
||||
import org.bouncycastle.asn1.ASN1OctetString;
|
||||
import org.bouncycastle.asn1.ASN1Primitive;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class FreeOtpImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs/tokens.xml";
|
||||
|
@ -40,6 +67,24 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try (BufferedInputStream bufInStream = new BufferedInputStream(stream);
|
||||
DataInputStream dataInStream = new DataInputStream(bufInStream)) {
|
||||
|
||||
dataInStream.mark(2);
|
||||
int magic = dataInStream.readUnsignedShort();
|
||||
dataInStream.reset();
|
||||
|
||||
if (magic == SerializedHashMapParser.MAGIC) {
|
||||
return readV2(dataInStream);
|
||||
} else {
|
||||
return readV1(bufInStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private DecryptedStateV1 readV1(InputStream stream) throws DatabaseImporterException {
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
|
@ -52,16 +97,184 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
entries.add(new JSONObject(entry.Value));
|
||||
}
|
||||
}
|
||||
return new State(entries);
|
||||
return new DecryptedStateV1(entries);
|
||||
} catch (XmlPullParserException | IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private List<JSONObject> _entries;
|
||||
private EncryptedState readV2(DataInputStream stream) throws DatabaseImporterException {
|
||||
try {
|
||||
Map<String, String> entries = SerializedHashMapParser.parse(stream);
|
||||
JSONObject mkObj = new JSONObject(entries.get("masterKey"));
|
||||
return new EncryptedState(mkObj, entries);
|
||||
} catch (IOException | JSONException | SerializedHashMapParser.ParseException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public State(List<JSONObject> entries) {
|
||||
public static class EncryptedState extends State {
|
||||
private static final int MASTER_KEY_SIZE = 32 * 8;
|
||||
|
||||
private final String _mkAlgo;
|
||||
private final String _mkCipher;
|
||||
private final byte[] _mkCipherText;
|
||||
private final byte[] _mkParameters;
|
||||
private final byte[] _mkToken;
|
||||
private final byte[] _mkSalt;
|
||||
private final int _mkIterations;
|
||||
private final Map<String, String> _entries;
|
||||
|
||||
private EncryptedState(JSONObject mkObj, Map<String, String> entries)
|
||||
throws DatabaseImporterException, JSONException {
|
||||
super(true);
|
||||
|
||||
_mkAlgo = mkObj.getString("mAlgorithm");
|
||||
if (!_mkAlgo.equals("PBKDF2withHmacSHA1") && !_mkAlgo.equals("PBKDF2withHmacSHA512")) {
|
||||
throw new DatabaseImporterException(String.format("Unexpected master key KDF: %s", _mkAlgo));
|
||||
}
|
||||
JSONObject keyObj = mkObj.getJSONObject("mEncryptedKey");
|
||||
_mkCipher = keyObj.getString("mCipher");
|
||||
if (!_mkCipher.equals("AES/GCM/NoPadding")) {
|
||||
throw new DatabaseImporterException(String.format("Unexpected master key cipher: %s", _mkCipher));
|
||||
}
|
||||
_mkCipherText = toBytes(keyObj.getJSONArray("mCipherText"));
|
||||
_mkParameters = toBytes(keyObj.getJSONArray("mParameters"));
|
||||
_mkToken = keyObj.getString("mToken").getBytes(StandardCharsets.UTF_8);
|
||||
_mkSalt = toBytes(mkObj.getJSONArray("mSalt"));
|
||||
_mkIterations = mkObj.getInt("mIterations");
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
public State decrypt(char[] password) throws DatabaseImporterException {
|
||||
PBKDFTask.Params params = new PBKDFTask.Params(_mkAlgo, MASTER_KEY_SIZE, password, _mkSalt, _mkIterations);
|
||||
SecretKey passKey = PBKDFTask.deriveKey(params);
|
||||
return decrypt(passKey);
|
||||
}
|
||||
|
||||
public State decrypt(SecretKey passKey) throws DatabaseImporterException {
|
||||
byte[] masterKeyBytes;
|
||||
try {
|
||||
byte[] nonce = parseNonce(_mkParameters);
|
||||
IvParameterSpec spec = new IvParameterSpec(nonce);
|
||||
Cipher cipher = Cipher.getInstance(_mkCipher);
|
||||
cipher.init(Cipher.DECRYPT_MODE, passKey, spec);
|
||||
cipher.updateAAD(_mkToken);
|
||||
masterKeyBytes = cipher.doFinal(_mkCipherText);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException |
|
||||
IllegalBlockSizeException | InvalidKeyException |
|
||||
InvalidAlgorithmParameterException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
SecretKey masterKey = new SecretKeySpec(masterKeyBytes, 0, masterKeyBytes.length, "AES");
|
||||
return new DecryptedStateV2(_entries, masterKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.importer_warning_title_freeotp2)
|
||||
.setMessage(R.string.importer_warning_message_freeotp2)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, password -> {
|
||||
PBKDFTask.Params params = getKeyDerivationParams(password, _mkAlgo);
|
||||
PBKDFTask task = new PBKDFTask(context, key -> {
|
||||
try {
|
||||
State state = decrypt(key);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
|
||||
task.execute(lifecycle, params);
|
||||
}, dialog1 -> listener.onCanceled());
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
private PBKDFTask.Params getKeyDerivationParams(char[] password, String algo) {
|
||||
return new PBKDFTask.Params(algo, MASTER_KEY_SIZE, password, _mkSalt, _mkIterations);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedStateV2 extends DatabaseImporter.State {
|
||||
private final Map<String, String> _entries;
|
||||
private final SecretKey _masterKey;
|
||||
|
||||
public DecryptedStateV2(Map<String, String> entries, SecretKey masterKey) {
|
||||
super(false);
|
||||
_entries = entries;
|
||||
_masterKey = masterKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() throws DatabaseImporterException {
|
||||
Result result = new Result();
|
||||
|
||||
for (Map.Entry<String, String> entry : _entries.entrySet()) {
|
||||
if (entry.getKey().endsWith("-token") || entry.getKey().equals("masterKey")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject encObj = new JSONObject(entry.getValue());
|
||||
String tokenKey = String.format("%s-token", entry.getKey());
|
||||
JSONObject tokenObj = new JSONObject(_entries.get(tokenKey));
|
||||
|
||||
VaultEntry vaultEntry = convertEntry(encObj, tokenObj);
|
||||
result.addEntry(vaultEntry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private VaultEntry convertEntry(JSONObject encObj, JSONObject tokenObj)
|
||||
throws DatabaseImporterEntryException {
|
||||
try {
|
||||
JSONObject keyObj = new JSONObject(encObj.getString("key"));
|
||||
String cipherName = keyObj.getString("mCipher");
|
||||
if (!cipherName.equals("AES/GCM/NoPadding")) {
|
||||
throw new DatabaseImporterException(String.format("Unexpected cipher: %s", cipherName));
|
||||
}
|
||||
byte[] cipherText = toBytes(keyObj.getJSONArray("mCipherText"));
|
||||
byte[] parameters = toBytes(keyObj.getJSONArray("mParameters"));
|
||||
byte[] token = keyObj.getString("mToken").getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] nonce = parseNonce(parameters);
|
||||
IvParameterSpec spec = new IvParameterSpec(nonce);
|
||||
Cipher cipher = Cipher.getInstance(cipherName);
|
||||
cipher.init(Cipher.DECRYPT_MODE, _masterKey, spec);
|
||||
cipher.updateAAD(token);
|
||||
byte[] secretBytes = cipher.doFinal(cipherText);
|
||||
|
||||
JSONArray secretArray = new JSONArray();
|
||||
for (byte b : secretBytes) {
|
||||
secretArray.put(b);
|
||||
}
|
||||
tokenObj.put("secret", secretArray);
|
||||
|
||||
return DecryptedStateV1.convertEntry(tokenObj);
|
||||
} catch (DatabaseImporterException | JSONException | NoSuchAlgorithmException |
|
||||
NoSuchPaddingException | InvalidAlgorithmParameterException |
|
||||
InvalidKeyException | BadPaddingException | IllegalBlockSizeException |
|
||||
IOException e) {
|
||||
throw new DatabaseImporterEntryException(e, tokenObj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedStateV1 extends DatabaseImporter.State {
|
||||
private final List<JSONObject> _entries;
|
||||
|
||||
public DecryptedStateV1(List<JSONObject> entries) {
|
||||
super(false);
|
||||
_entries = entries;
|
||||
}
|
||||
|
@ -85,8 +298,8 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
String type = obj.getString("type").toLowerCase(Locale.ROOT);
|
||||
String algo = obj.getString("algo");
|
||||
int digits = obj.getInt("digits");
|
||||
String algo = obj.optString("algo", OtpInfo.DEFAULT_ALGORITHM);
|
||||
int digits = obj.optInt("digits", OtpInfo.DEFAULT_DIGITS);
|
||||
byte[] secret = toBytes(obj.getJSONArray("secret"));
|
||||
|
||||
String issuer = obj.getString("issuerExt");
|
||||
|
@ -95,7 +308,7 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
OtpInfo info;
|
||||
switch (type) {
|
||||
case "totp":
|
||||
int period = obj.getInt("period");
|
||||
int period = obj.optInt("period", TotpInfo.DEFAULT_PERIOD);
|
||||
if (issuer.equals("Steam")) {
|
||||
info = new SteamInfo(secret, algo, digits, period);
|
||||
} else {
|
||||
|
@ -116,6 +329,23 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
}
|
||||
}
|
||||
|
||||
private static byte[] parseNonce(byte[] parameters) throws IOException {
|
||||
ASN1Primitive prim = ASN1Sequence.fromByteArray(parameters);
|
||||
if (prim instanceof ASN1OctetString) {
|
||||
return ((ASN1OctetString) prim).getOctets();
|
||||
}
|
||||
|
||||
if (prim instanceof ASN1Sequence) {
|
||||
for (ASN1Encodable enc : (ASN1Sequence) prim) {
|
||||
if (enc instanceof ASN1OctetString) {
|
||||
return ((ASN1OctetString) enc).getOctets();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("Unable to find nonce in parameters");
|
||||
}
|
||||
|
||||
private static byte[] toBytes(JSONArray array) throws JSONException {
|
||||
byte[] bytes = new byte[array.length()];
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
|
@ -123,4 +353,119 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
}
|
||||
return bytes;
|
||||
}
|
||||
private static class SerializedHashMapParser {
|
||||
private static final int MAGIC = 0xaced;
|
||||
private static final int VERSION = 5;
|
||||
private static final long SERIAL_VERSION_UID = 362498820763181265L;
|
||||
|
||||
private static final byte TC_NULL = 0x70;
|
||||
private static final byte TC_CLASSDESC = 0x72;
|
||||
private static final byte TC_OBJECT = 0x73;
|
||||
private static final byte TC_STRING = 0x74;
|
||||
|
||||
private SerializedHashMapParser() {
|
||||
|
||||
}
|
||||
|
||||
public static Map<String, String> parse(DataInputStream inStream)
|
||||
throws IOException, ParseException {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
|
||||
// Read/validate the magic number and version
|
||||
int magic = inStream.readUnsignedShort();
|
||||
int version = inStream.readUnsignedShort();
|
||||
if (magic != MAGIC || version != VERSION) {
|
||||
throw new ParseException("Not a serialized Java Object");
|
||||
}
|
||||
|
||||
// Read the class descriptor info for HashMap
|
||||
byte b = inStream.readByte();
|
||||
if (b != TC_OBJECT) {
|
||||
throw new ParseException("Expected an object, found: " + b);
|
||||
}
|
||||
b = inStream.readByte();
|
||||
if (b != TC_CLASSDESC) {
|
||||
throw new ParseException("Expected a class desc, found: " + b);
|
||||
}
|
||||
parseClassDescriptor(inStream);
|
||||
|
||||
// Not interested in the capacity of the map
|
||||
inStream.readInt();
|
||||
// Read the number of elements in the HashMap
|
||||
int size = inStream.readInt();
|
||||
|
||||
// Parse each key-value pair in the map
|
||||
for (int i = 0; i < size; i++) {
|
||||
String key = parseStringObject(inStream);
|
||||
String value = parseStringObject(inStream);
|
||||
map.put(key, value);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void parseClassDescriptor(DataInputStream inputStream)
|
||||
throws IOException, ParseException {
|
||||
// Check whether we're dealing with a HashMap and a version we support
|
||||
String className = parseUTF(inputStream);
|
||||
if (!className.equals(HashMap.class.getName())) {
|
||||
throw new ParseException(String.format("Unexpected class name: %s", className));
|
||||
}
|
||||
long serialVersionUID = inputStream.readLong();
|
||||
if (serialVersionUID != SERIAL_VERSION_UID) {
|
||||
throw new ParseException(String.format("Unexpected serial version UID: %d", serialVersionUID));
|
||||
}
|
||||
|
||||
// Read past all of the fields in the class
|
||||
byte fieldDescriptor = inputStream.readByte();
|
||||
if (fieldDescriptor == TC_NULL) {
|
||||
return;
|
||||
}
|
||||
int totalFieldSkip = 0;
|
||||
int fieldCount = inputStream.readUnsignedShort();
|
||||
for (int i = 0; i < fieldCount; i++) {
|
||||
char fieldType = (char) inputStream.readByte();
|
||||
parseUTF(inputStream);
|
||||
switch (fieldType) {
|
||||
case 'F': // float (4 bytes)
|
||||
case 'I': // int (4 bytes)
|
||||
totalFieldSkip += 4;
|
||||
break;
|
||||
default:
|
||||
throw new ParseException(String.format("Unexpected field type: %s", fieldType));
|
||||
}
|
||||
}
|
||||
inputStream.skipBytes(totalFieldSkip);
|
||||
|
||||
// Not sure what these bytes are, just skip them
|
||||
inputStream.skipBytes(4);
|
||||
}
|
||||
|
||||
private static String parseStringObject(DataInputStream inputStream)
|
||||
throws IOException, ParseException {
|
||||
byte objectType = inputStream.readByte();
|
||||
if (objectType != TC_STRING) {
|
||||
throw new ParseException(String.format("Expected a string object, found: %d", objectType));
|
||||
}
|
||||
|
||||
int length = inputStream.readUnsignedShort();
|
||||
byte[] strBytes = new byte[length];
|
||||
inputStream.readFully(strBytes);
|
||||
|
||||
return new String(strBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String parseUTF(DataInputStream inputStream) throws IOException {
|
||||
int length = inputStream.readUnsignedShort();
|
||||
byte[] strBytes = new byte[length];
|
||||
inputStream.readFully(strBytes);
|
||||
return new String(strBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static class ParseException extends Exception {
|
||||
public ParseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ public class FreeOtpPlusImporter extends DatabaseImporter {
|
|||
entries.add(array.getJSONObject(i));
|
||||
}
|
||||
|
||||
state = new FreeOtpImporter.State(entries);
|
||||
state = new FreeOtpImporter.DecryptedStateV1(entries);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,10 @@ import org.json.JSONObject;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class SteamImporter extends DatabaseImporter {
|
||||
private static final String _subDir = "files";
|
||||
|
@ -57,29 +61,43 @@ public class SteamImporter extends DatabaseImporter {
|
|||
try {
|
||||
byte[] bytes = IOUtils.readAll(stream);
|
||||
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
||||
return new State(obj);
|
||||
|
||||
List<JSONObject> objs = new ArrayList<>();
|
||||
if (obj.has("accounts")) {
|
||||
JSONObject accounts = obj.getJSONObject("accounts");
|
||||
Iterator<String> keys = accounts.keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
objs.add(accounts.getJSONObject(key));
|
||||
}
|
||||
} else {
|
||||
objs.add(obj);
|
||||
}
|
||||
return new State(objs);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private JSONObject _obj;
|
||||
private final List<JSONObject> _objs;
|
||||
|
||||
private State(JSONObject obj) {
|
||||
private State(List<JSONObject> objs) {
|
||||
super(false);
|
||||
_obj = obj;
|
||||
_objs = objs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
try {
|
||||
VaultEntry entry = convertEntry(_obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
for (JSONObject obj : _objs) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -16,11 +16,13 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
|
|||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.Argon2Task;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.bouncycastle.crypto.params.Argon2Parameters;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -43,12 +45,11 @@ import javax.crypto.NoSuchPaddingException;
|
|||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
public class AuthenticatorProImporter extends DatabaseImporter {
|
||||
private static final String HEADER = "AuthenticatorPro";
|
||||
private static final int ITERATIONS = 64000;
|
||||
private static final int KEY_SIZE = 32 * Byte.SIZE;
|
||||
private static final String PKG_NAME = "me.jmh.authenticatorpro";
|
||||
private static final String PKG_DB_PATH = "files/proauth.db3";
|
||||
public class StratumImporter extends DatabaseImporter {
|
||||
private static final String HEADER = "AUTHENTICATORPRO";
|
||||
private static final String HEADER_LEGACY = "AuthenticatorPro";
|
||||
private static final String PKG_NAME = "com.stratumauth.app";
|
||||
private static final String PKG_DB_PATH = "databases/authenticator.db3";
|
||||
|
||||
private enum Algorithm {
|
||||
SHA1,
|
||||
|
@ -56,7 +57,7 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
|||
SHA512
|
||||
}
|
||||
|
||||
public AuthenticatorProImporter(Context context) {
|
||||
public StratumImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
|
@ -90,24 +91,19 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
|||
}
|
||||
}
|
||||
|
||||
private static EncryptedState readEncrypted(DataInputStream stream) throws DatabaseImporterException {
|
||||
private static State readEncrypted(DataInputStream stream) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] headerBytes = new byte[HEADER.getBytes(StandardCharsets.UTF_8).length];
|
||||
stream.readFully(headerBytes);
|
||||
String header = new String(headerBytes, StandardCharsets.UTF_8);
|
||||
if (!header.equals(HEADER)) {
|
||||
throw new DatabaseImporterException("Invalid file header");
|
||||
switch (header) {
|
||||
case HEADER:
|
||||
return EncryptedState.parseHeader(stream);
|
||||
case HEADER_LEGACY:
|
||||
return LegacyEncryptedState.parseHeader(stream);
|
||||
default:
|
||||
throw new DatabaseImporterException("Invalid file header");
|
||||
}
|
||||
|
||||
int saltSize = 20;
|
||||
byte[] salt = new byte[saltSize];
|
||||
stream.readFully(salt);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
int ivSize = cipher.getBlockSize();
|
||||
byte[] iv = new byte[ivSize];
|
||||
stream.readFully(iv);
|
||||
return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
|
||||
} catch (UTFDataFormatException e) {
|
||||
throw new DatabaseImporterException("Invalid file header");
|
||||
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException e) {
|
||||
|
@ -130,6 +126,13 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
|||
}
|
||||
|
||||
static class EncryptedState extends State {
|
||||
private static final int KEY_SIZE = 32;
|
||||
private static final int MEMORY_COST = 16; // 2^16 KiB = 64 MiB
|
||||
private static final int PARALLELISM = 4;
|
||||
private static final int ITERATIONS = 3;
|
||||
private static final int SALT_SIZE = 16;
|
||||
private static final int IV_SIZE = 12;
|
||||
|
||||
private final Cipher _cipher;
|
||||
private final byte[] _salt;
|
||||
private final byte[] _iv;
|
||||
|
@ -143,6 +146,81 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
|||
_data = data;
|
||||
}
|
||||
|
||||
public JsonState decrypt(char[] password) throws DatabaseImporterException {
|
||||
Argon2Task.Params params = getKeyDerivationParams(password);
|
||||
SecretKey key = Argon2Task.deriveKey(params);
|
||||
return decrypt(key);
|
||||
}
|
||||
|
||||
public JsonState decrypt(SecretKey key) throws DatabaseImporterException {
|
||||
try {
|
||||
_cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv));
|
||||
byte[] decrypted = _cipher.doFinal(_data);
|
||||
return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8)));
|
||||
} catch (InvalidAlgorithmParameterException | IllegalBlockSizeException
|
||||
| JSONException | InvalidKeyException | BadPaddingException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
|
||||
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
|
||||
Argon2Task.Params params = getKeyDerivationParams(password);
|
||||
Argon2Task task = new Argon2Task(context, key -> {
|
||||
try {
|
||||
StratumImporter.JsonState state = decrypt(key);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
|
||||
task.execute(lifecycle, params);
|
||||
}, dialog -> listener.onCanceled());
|
||||
}
|
||||
|
||||
private Argon2Task.Params getKeyDerivationParams(char[] password) {
|
||||
Argon2Parameters argon2Params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
|
||||
.withIterations(ITERATIONS)
|
||||
.withParallelism(PARALLELISM)
|
||||
.withMemoryPowOfTwo(MEMORY_COST)
|
||||
.withSalt(_salt)
|
||||
.build();
|
||||
return new Argon2Task.Params(password, argon2Params, KEY_SIZE);
|
||||
}
|
||||
|
||||
private static EncryptedState parseHeader(DataInputStream stream)
|
||||
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
|
||||
byte[] salt = new byte[SALT_SIZE];
|
||||
stream.readFully(salt);
|
||||
|
||||
byte[] iv = new byte[IV_SIZE];
|
||||
stream.readFully(iv);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
|
||||
}
|
||||
}
|
||||
|
||||
static class LegacyEncryptedState extends State {
|
||||
private static final int ITERATIONS = 64000;
|
||||
private static final int KEY_SIZE = 32 * Byte.SIZE;
|
||||
private static final int SALT_SIZE = 20;
|
||||
|
||||
private final Cipher _cipher;
|
||||
private final byte[] _salt;
|
||||
private final byte[] _iv;
|
||||
private final byte[] _data;
|
||||
|
||||
public LegacyEncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
|
||||
super(true);
|
||||
_cipher = cipher;
|
||||
_salt = salt;
|
||||
_iv = iv;
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public JsonState decrypt(char[] password) throws DatabaseImporterException {
|
||||
PBKDFTask.Params params = getKeyDerivationParams(password);
|
||||
SecretKey key = PBKDFTask.deriveKey(params);
|
||||
|
@ -166,7 +244,7 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
|||
PBKDFTask.Params params = getKeyDerivationParams(password);
|
||||
PBKDFTask task = new PBKDFTask(context, key -> {
|
||||
try {
|
||||
AuthenticatorProImporter.JsonState state = decrypt(key);
|
||||
StratumImporter.JsonState state = decrypt(key);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
|
@ -180,6 +258,18 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
|||
private PBKDFTask.Params getKeyDerivationParams(char[] password) {
|
||||
return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, _salt, ITERATIONS);
|
||||
}
|
||||
|
||||
private static LegacyEncryptedState parseHeader(DataInputStream stream)
|
||||
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
|
||||
byte[] salt = new byte[SALT_SIZE];
|
||||
stream.readFully(salt);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
int ivSize = cipher.getBlockSize();
|
||||
byte[] iv = new byte[ivSize];
|
||||
stream.readFully(iv);
|
||||
return new LegacyEncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
|
||||
}
|
||||
}
|
||||
|
||||
private static class JsonState extends State {
|
|
@ -4,8 +4,6 @@ import android.content.Context;
|
|||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
|
@ -18,6 +16,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
|||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
|
@ -154,7 +153,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
|
|||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
|
||||
.setMessage(R.string.choose_totpauth_importer)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
|
|
|
@ -4,17 +4,19 @@ import android.content.Context;
|
|||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.common.base.Strings;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
|
@ -60,7 +62,7 @@ public class TwoFASImporter extends DatabaseImporter {
|
|||
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
|
||||
JSONObject obj = new JSONObject(json);
|
||||
int version = obj.getInt("schemaVersion");
|
||||
if (version > 3) {
|
||||
if (version > 4) {
|
||||
throw new DatabaseImporterException(String.format("Unsupported schema version: %d", version));
|
||||
}
|
||||
|
||||
|
@ -172,9 +174,12 @@ public class TwoFASImporter extends DatabaseImporter {
|
|||
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret = Base32.decode(obj.getString("secret"));
|
||||
byte[] secret = GoogleAuthInfo.parseSecret(obj.getString("secret"));
|
||||
JSONObject info = obj.getJSONObject("otp");
|
||||
String issuer = info.optString("issuer");
|
||||
String issuer = obj.optString("name");
|
||||
if (Strings.isNullOrEmpty(issuer)) {
|
||||
issuer = info.optString("issuer");
|
||||
}
|
||||
String name = info.optString("account");
|
||||
int digits = info.optInt("digits", TotpInfo.DEFAULT_DIGITS);
|
||||
String algorithm = info.optString("algorithm", TotpInfo.DEFAULT_ALGORITHM);
|
||||
|
@ -187,6 +192,9 @@ public class TwoFASImporter extends DatabaseImporter {
|
|||
} else if (tokenType.equals("HOTP")) {
|
||||
long counter = info.optLong("counter", 0);
|
||||
otp = new HotpInfo(secret, algorithm, digits, counter);
|
||||
} else if (tokenType.equals("STEAM")) {
|
||||
int period = info.optInt("period", TotpInfo.DEFAULT_PERIOD);
|
||||
otp = new SteamInfo(secret, algorithm, digits, period);
|
||||
} else {
|
||||
throw new DatabaseImporterEntryException(String.format("Unrecognized tokenType: %s", tokenType), obj.toString());
|
||||
}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
package com.beemdevelopment.aegis.licenses;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import de.psdev.licensesdialog.licenses.License;
|
||||
|
||||
public class GlideLicense extends License {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Glide License";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSummaryTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.glide_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readFullTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.glide_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return "https://github.com/bumptech/glide/blob/master/LICENSE";
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package com.beemdevelopment.aegis.licenses;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import de.psdev.licensesdialog.licenses.License;
|
||||
|
||||
public class ProtobufLicense extends License {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Protocol Buffers License";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSummaryTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.protobuf_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readFullTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.protobuf_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return "https://raw.githubusercontent.com/protocolbuffers/protobuf/master/LICENSE";
|
||||
}
|
||||
}
|
|
@ -30,20 +30,6 @@ public class MotpInfo extends TotpInfo {
|
|||
setPin(pin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOtp() {
|
||||
if (_pin == null) {
|
||||
throw new IllegalStateException("PIN must be set before generating an OTP");
|
||||
}
|
||||
|
||||
try {
|
||||
MOTP otp = MOTP.generateOTP(getSecret(), getAlgorithm(false), getDigits(), getPeriod(), getPin());
|
||||
return otp.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOtp(long time) {
|
||||
if (_pin == null) {
|
||||
|
|
|
@ -20,11 +20,11 @@ public class SteamInfo extends TotpInfo {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getOtp() throws OtpInfoException {
|
||||
public String getOtp(long time) throws OtpInfoException {
|
||||
checkSecret();
|
||||
|
||||
try {
|
||||
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
|
||||
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
|
||||
return otp.toSteamString();
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
|
@ -27,17 +27,12 @@ public class TotpInfo extends OtpInfo {
|
|||
|
||||
@Override
|
||||
public String getOtp() throws OtpInfoException {
|
||||
checkSecret();
|
||||
|
||||
try {
|
||||
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
|
||||
return otp.toString();
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return getOtp(System.currentTimeMillis() / 1000);
|
||||
}
|
||||
|
||||
public String getOtp(long time) {
|
||||
public String getOtp(long time) throws OtpInfoException {
|
||||
checkSecret();
|
||||
|
||||
try {
|
||||
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
|
||||
return otp.toString();
|
||||
|
|
|
@ -38,13 +38,13 @@ public class YandexInfo extends TotpInfo {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getOtp() {
|
||||
public String getOtp(long time) {
|
||||
if (_pin == null) {
|
||||
throw new IllegalStateException("PIN must be set before generating an OTP");
|
||||
}
|
||||
|
||||
try {
|
||||
YAOTP otp = YAOTP.generateOTP(getSecret(), getPin(), getDigits(), getAlgorithm(true), getPeriod());
|
||||
YAOTP otp = YAOTP.generateOTP(getSecret(), getPin(), getDigits(), getAlgorithm(true), getPeriod(), time);
|
||||
return otp.toString();
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.beemdevelopment.aegis.services;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.service.quicksettings.Tile;
|
||||
|
@ -16,10 +18,13 @@ public class LaunchAppTileService extends TileService {
|
|||
public void onStartListening() {
|
||||
super.onStartListening();
|
||||
Tile tile = getQsTile();
|
||||
tile.setState(Tile.STATE_INACTIVE);
|
||||
tile.updateTile();
|
||||
if (tile != null) {
|
||||
tile.setState(Tile.STATE_INACTIVE);
|
||||
tile.updateTile();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
@Override
|
||||
public void onClick() {
|
||||
super.onClick();
|
||||
|
@ -28,6 +33,12 @@ public class LaunchAppTileService extends TileService {
|
|||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
|
||||
startActivityAndCollapse(intent);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, flags);
|
||||
startActivityAndCollapse(pendingIntent);
|
||||
} else {
|
||||
startActivityAndCollapse(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.beemdevelopment.aegis.services;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.service.quicksettings.Tile;
|
||||
|
@ -16,10 +18,13 @@ public class LaunchScannerTileService extends TileService {
|
|||
public void onStartListening() {
|
||||
super.onStartListening();
|
||||
Tile tile = getQsTile();
|
||||
tile.setState(Tile.STATE_INACTIVE);
|
||||
tile.updateTile();
|
||||
if (tile != null) {
|
||||
tile.setState(Tile.STATE_INACTIVE);
|
||||
tile.updateTile();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
@Override
|
||||
public void onClick() {
|
||||
super.onClick();
|
||||
|
@ -29,6 +34,12 @@ public class LaunchScannerTileService extends TileService {
|
|||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
|
||||
startActivityAndCollapse(intent);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, flags);
|
||||
startActivityAndCollapse(pendingIntent);
|
||||
} else {
|
||||
startActivityAndCollapse(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.annotation.SuppressLint;
|
|||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -29,11 +28,7 @@ public class NotificationService extends Service {
|
|||
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
public void serviceMethod() {
|
||||
int flags = PendingIntent.FLAG_ONE_SHOT;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags |= PendingIntent.FLAG_IMMUTABLE;
|
||||
}
|
||||
|
||||
int flags = PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE;
|
||||
Intent intent = new Intent(this, VaultLockReceiver.class);
|
||||
intent.setAction(VaultLockReceiver.ACTION_LOCK_VAULT);
|
||||
intent.setPackage(BuildConfig.APPLICATION_ID);
|
||||
|
@ -47,7 +42,8 @@ public class NotificationService extends Service {
|
|||
.setOngoing(true)
|
||||
.setContentIntent(pendingIntent);
|
||||
|
||||
startForeground(NOTIFICATION_VAULT_UNLOCKED, builder.build());
|
||||
// NOTE: Disabled for now. See issue: #1047
|
||||
//startForeground(NOTIFICATION_VAULT_UNLOCKED, builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -13,20 +13,16 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.view.LayoutInflaterCompat;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.licenses.GlideLicense;
|
||||
import com.beemdevelopment.aegis.licenses.ProtobufLicense;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.ChangelogDialog;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.LicenseDialog;
|
||||
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
|
||||
|
||||
import de.psdev.licensesdialog.LicenseResolver;
|
||||
import de.psdev.licensesdialog.LicensesDialog;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
public class AboutActivity extends AegisActivity {
|
||||
|
||||
|
@ -40,7 +36,6 @@ public class AboutActivity extends AegisActivity {
|
|||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new IconicsLayoutInflater2(getDelegate()));
|
||||
super.onCreate(savedInstanceState);
|
||||
if (abortIfOrphan(savedInstanceState)) {
|
||||
return;
|
||||
|
@ -48,6 +43,7 @@ public class AboutActivity extends AegisActivity {
|
|||
|
||||
setContentView(R.layout.activity_about);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
@ -57,12 +53,15 @@ public class AboutActivity extends AegisActivity {
|
|||
View btnLicense = findViewById(R.id.btn_license);
|
||||
btnLicense.setOnClickListener(v -> {
|
||||
LicenseDialog.create()
|
||||
.setTheme(getConfiguredTheme())
|
||||
.setTheme(_themeHelper.getConfiguredTheme())
|
||||
.show(getSupportFragmentManager(), null);
|
||||
});
|
||||
|
||||
View btnThirdPartyLicenses = findViewById(R.id.btn_third_party_licenses);
|
||||
btnThirdPartyLicenses.setOnClickListener(v -> showThirdPartyLicenseDialog());
|
||||
btnThirdPartyLicenses.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(this, LicensesActivity.class);
|
||||
startActivity(intent);
|
||||
});
|
||||
|
||||
TextView appVersion = findViewById(R.id.app_version);
|
||||
appVersion.setText(getCurrentAppVersion());
|
||||
|
@ -93,9 +92,20 @@ public class AboutActivity extends AegisActivity {
|
|||
View btnChangelog = findViewById(R.id.btn_changelog);
|
||||
btnChangelog.setOnClickListener(v -> {
|
||||
ChangelogDialog.create()
|
||||
.setTheme(getConfiguredTheme())
|
||||
.setTheme(_themeHelper.getConfiguredTheme())
|
||||
.show(getSupportFragmentManager(), null);
|
||||
});
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.about_scroll_view), (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
targetView.setPadding(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
insets.bottom
|
||||
);
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
}
|
||||
|
||||
private static String getCurrentAppVersion() {
|
||||
|
@ -130,30 +140,9 @@ public class AboutActivity extends AegisActivity {
|
|||
startActivity(Intent.createChooser(mailIntent, getString(R.string.email)));
|
||||
}
|
||||
|
||||
private void showThirdPartyLicenseDialog() {
|
||||
String stylesheet = getString(R.string.custom_notices_format_style);
|
||||
int backgroundColorResource = getConfiguredTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
String backgroundColor = getThemeColorAsHex(backgroundColorResource);
|
||||
String textColor = getThemeColorAsHex(R.attr.primaryText);
|
||||
String licenseColor = getThemeColorAsHex(R.attr.cardBackgroundFocused);
|
||||
String linkColor = getThemeColorAsHex(R.attr.colorAccent);
|
||||
|
||||
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor, linkColor);
|
||||
|
||||
LicenseResolver.registerLicense(new GlideLicense());
|
||||
LicenseResolver.registerLicense(new ProtobufLicense());
|
||||
|
||||
new LicensesDialog.Builder(this)
|
||||
.setNotices(R.raw.notices)
|
||||
.setTitle(R.string.third_party_licenses)
|
||||
.setNoticesCssStyle(stylesheet)
|
||||
.setIncludeOwnLicense(true)
|
||||
.build()
|
||||
.show();
|
||||
}
|
||||
|
||||
private String getThemeColorAsHex(@AttrRes int attributeId) {
|
||||
return String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(attributeId, getTheme())));
|
||||
int color = MaterialColors.getColor(this, attributeId, getClass().getCanonicalName());
|
||||
return String.format("%06X", 0xFFFFFF & color);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -4,25 +4,34 @@ import android.annotation.SuppressLint;
|
|||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.core.view.ViewPropertyAnimatorCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.database.AuditLogRepository;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPackManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
|
@ -35,21 +44,30 @@ import dagger.hilt.components.SingletonComponent;
|
|||
@AndroidEntryPoint
|
||||
public abstract class AegisActivity extends AppCompatActivity implements VaultManager.LockListener {
|
||||
protected Preferences _prefs;
|
||||
protected ThemeHelper _themeHelper;
|
||||
|
||||
@Inject
|
||||
protected VaultManager _vaultManager;
|
||||
|
||||
@Inject
|
||||
protected AuditLogRepository _auditLogRepository;
|
||||
|
||||
@Inject
|
||||
protected IconPackManager _iconPackManager;
|
||||
|
||||
private ActionModeStatusGuardHack _statusGuardHack;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// set the theme and locale before creating the activity
|
||||
_prefs = EarlyEntryPoints.get(getApplicationContext(), PrefEntryPoint.class).getPreferences();
|
||||
_themeHelper = new ThemeHelper(this, _prefs);
|
||||
onSetTheme();
|
||||
setLocale(_prefs.getLocale());
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
_statusGuardHack = new ActionModeStatusGuardHack();
|
||||
|
||||
// set FLAG_SECURE on the window of every AegisActivity
|
||||
if (_prefs.isSecureScreenEnabled()) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
|
@ -96,31 +114,7 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
|
|||
* Called when the activity is expected to set its theme.
|
||||
*/
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme of the activity. The actual style that is set is picked from the
|
||||
* given map, based on the theme configured by the user.
|
||||
*/
|
||||
protected void setTheme(Map<Theme, Integer> themeMap) {
|
||||
int theme = themeMap.get(getConfiguredTheme());
|
||||
setTheme(theme);
|
||||
}
|
||||
|
||||
protected Theme getConfiguredTheme() {
|
||||
Theme theme = _prefs.getCurrentTheme();
|
||||
|
||||
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
|
||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
|
||||
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
|
||||
} else {
|
||||
theme = Theme.LIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
return theme;
|
||||
_themeHelper.setTheme(ThemeMap.DEFAULT);
|
||||
}
|
||||
|
||||
protected void setLocale(Locale locale) {
|
||||
|
@ -169,6 +163,79 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSupportActionModeStarted(@NonNull ActionMode mode) {
|
||||
super.onSupportActionModeStarted(mode);
|
||||
_statusGuardHack.apply(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSupportActionModeFinished(@NonNull ActionMode mode) {
|
||||
super.onSupportActionModeFinished(mode);
|
||||
_statusGuardHack.apply(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* When starting/finishing an action mode, forcefully cancel the fade in/out animation and
|
||||
* set the status bar color. This requires the abc_decor_view_status_guard colors to be set
|
||||
* to transparent.
|
||||
*
|
||||
* This should fix any inconsistencies between the color of the action bar and the status bar
|
||||
* when an action mode is active.
|
||||
*/
|
||||
private class ActionModeStatusGuardHack {
|
||||
private Field _fadeAnimField;
|
||||
private Field _actionModeViewField;
|
||||
private Drawable _appBarBackground;
|
||||
|
||||
private ActionModeStatusGuardHack() {
|
||||
try {
|
||||
_fadeAnimField = getDelegate().getClass().getDeclaredField("mFadeAnim");
|
||||
_fadeAnimField.setAccessible(true);
|
||||
_actionModeViewField = getDelegate().getClass().getDeclaredField("mActionModeView");
|
||||
_actionModeViewField.setAccessible(true);
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void apply(int visibility) {
|
||||
if (_fadeAnimField == null || _actionModeViewField == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ViewPropertyAnimatorCompat fadeAnim;
|
||||
ViewGroup actionModeView;
|
||||
try {
|
||||
fadeAnim = (ViewPropertyAnimatorCompat) _fadeAnimField.get(getDelegate());
|
||||
actionModeView = (ViewGroup) _actionModeViewField.get(getDelegate());
|
||||
} catch (IllegalAccessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppBarLayout appBarLayout = findViewById(R.id.app_bar_layout);
|
||||
if (appBarLayout != null && _appBarBackground == null) {
|
||||
_appBarBackground = appBarLayout.getBackground();
|
||||
}
|
||||
|
||||
if (fadeAnim == null || actionModeView == null || appBarLayout == null || _appBarBackground == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fadeAnim.cancel();
|
||||
|
||||
if (visibility == View.VISIBLE) {
|
||||
actionModeView.setVisibility(visibility);
|
||||
actionModeView.setAlpha(1f);
|
||||
int color = MaterialColors.getColor(appBarLayout, com.google.android.material.R.attr.colorSurfaceContainer);
|
||||
appBarLayout.setBackgroundColor(color);
|
||||
} else {
|
||||
actionModeView.setVisibility(visibility);
|
||||
actionModeView.setAlpha(0f);
|
||||
appBarLayout.setBackground(_appBarBackground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this Activity instance has become an orphan. This can happen if
|
||||
* the vault was killed/locked by an external trigger while the Activity was still open.
|
||||
|
|
|
@ -0,0 +1,281 @@
|
|||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.MetricsHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
|
||||
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
|
||||
import com.beemdevelopment.aegis.ui.views.AssignIconAdapter;
|
||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.ListPreloader;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AssignIconsActivity extends AegisActivity implements AssignIconAdapter.Listener {
|
||||
private AssignIconAdapter _adapter;
|
||||
private ArrayList<AssignIconEntry> _entries = new ArrayList<>();
|
||||
private RecyclerView _entriesView;
|
||||
private AssignIconsActivity.BackPressHandler _backPressHandler;
|
||||
private ViewPreloadSizeProvider<AssignIconEntry> _preloadSizeProvider;
|
||||
private IconPack _favoriteIconPack;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (abortIfOrphan(savedInstanceState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_assign_icons);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
ArrayList<UUID> assignIconEntriesIds = (ArrayList<UUID>) getIntent().getSerializableExtra("entries");
|
||||
for (UUID entryId: assignIconEntriesIds) {
|
||||
VaultEntry vaultEntry = _vaultManager.getVault().getEntryByUUID(entryId);
|
||||
_entries.add(new AssignIconEntry(vaultEntry));
|
||||
}
|
||||
|
||||
_backPressHandler = new AssignIconsActivity.BackPressHandler();
|
||||
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
|
||||
|
||||
IconPreloadProvider modelProvider1 = new IconPreloadProvider();
|
||||
EntryIconPreloadProvider modelProvider2 = new EntryIconPreloadProvider();
|
||||
_preloadSizeProvider = new ViewPreloadSizeProvider<>();
|
||||
RecyclerViewPreloader<IconPack.Icon> preloader1 = new RecyclerViewPreloader(this, modelProvider1, _preloadSizeProvider, 10);
|
||||
RecyclerViewPreloader<VaultEntry> preloader2 = new RecyclerViewPreloader(this, modelProvider2, _preloadSizeProvider, 10);
|
||||
|
||||
_adapter = new AssignIconAdapter(this);
|
||||
_entriesView = findViewById(R.id.list_assign_icons);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
_entriesView.setLayoutManager(layoutManager);
|
||||
_entriesView.setAdapter(_adapter);
|
||||
_entriesView.setNestedScrollingEnabled(false);
|
||||
_entriesView.addItemDecoration(new SpacesItemDecoration(8));
|
||||
_entriesView.addOnScrollListener(preloader1);
|
||||
_entriesView.addOnScrollListener(preloader2);
|
||||
|
||||
Optional<IconPack> favoriteIconPack = _iconPackManager.getIconPacks().stream()
|
||||
.sorted(Comparator.comparing(IconPack::getName))
|
||||
.findFirst();
|
||||
|
||||
if (!favoriteIconPack.isPresent()) {
|
||||
throw new RuntimeException(String.format("Started %s without any icon packs present", AssignIconsActivity.class.getName()));
|
||||
}
|
||||
|
||||
_favoriteIconPack = favoriteIconPack.get();
|
||||
|
||||
for (AssignIconEntry entry : _entries) {
|
||||
IconPack.Icon suggestedIcon = findSuggestedIcon(entry);
|
||||
if (suggestedIcon != null) {
|
||||
entry.setNewIcon(suggestedIcon);
|
||||
}
|
||||
}
|
||||
|
||||
_adapter.addEntries(_entries);
|
||||
}
|
||||
|
||||
private IconPack.Icon findSuggestedIcon(AssignIconEntry entry) {
|
||||
List<IconPack.Icon> suggestedIcons = _favoriteIconPack.getSuggestedIcons(entry.getEntry().getIssuer());
|
||||
if (suggestedIcons.size() > 0) {
|
||||
return suggestedIcons.get(0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void saveAndFinish() throws IOException {
|
||||
ArrayList<UUID> uuids = new ArrayList<>();
|
||||
for (AssignIconEntry selectedEntry : _entries) {
|
||||
VaultEntry entry = selectedEntry.getEntry();
|
||||
if (selectedEntry.getNewIcon() != null) {
|
||||
byte[] iconBytes;
|
||||
try (FileInputStream inStream = new FileInputStream(selectedEntry.getNewIcon().getFile())){
|
||||
iconBytes = IOUtils.readFile(inStream);
|
||||
}
|
||||
|
||||
VaultEntryIcon icon = new VaultEntryIcon(iconBytes, selectedEntry.getNewIcon().getIconType());
|
||||
entry.setIcon(icon);
|
||||
uuids.add(entry.getUUID());
|
||||
|
||||
_vaultManager.getVault().replaceEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entryUUIDs", uuids);
|
||||
|
||||
if (saveAndBackupVault()) {
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void discardAndFinish() {
|
||||
Dialogs.showDiscardDialog(this,
|
||||
(dialog, which) -> {
|
||||
try {
|
||||
saveAndFinish();
|
||||
} catch (IOException e) {
|
||||
Toast.makeText(this, R.string.saving_assign_icons_error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
},
|
||||
(dialog, which) -> finish());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_assign_icons, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
discardAndFinish();
|
||||
} else if (itemId == R.id.action_save) {
|
||||
try {
|
||||
saveAndFinish();
|
||||
} catch (IOException e) {
|
||||
Toast.makeText(this, R.string.saving_assign_icons_error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAssignIconEntryClick(AssignIconEntry entry) {
|
||||
List<IconPack> iconPacks = _iconPackManager.getIconPacks().stream()
|
||||
.sorted(Comparator.comparing(IconPack::getName))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, entry.getEntry().getIssuer(), false, new IconAdapter.Listener() {
|
||||
@Override
|
||||
public void onIconSelected(IconPack.Icon icon) {
|
||||
entry.setNewIcon(icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomSelected() { }
|
||||
});
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetPreloadView(View view) {
|
||||
_preloadSizeProvider.setView(view);
|
||||
}
|
||||
|
||||
private class BackPressHandler extends OnBackPressedCallback {
|
||||
public BackPressHandler() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
discardAndFinish();
|
||||
}
|
||||
}
|
||||
|
||||
private class EntryIconPreloadProvider implements ListPreloader.PreloadModelProvider<VaultEntry> {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<VaultEntry> getPreloadItems(int position) {
|
||||
VaultEntry entry = _entries.get(position).getEntry();
|
||||
if (entry.hasIcon()) {
|
||||
return Collections.singletonList(entry);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) {
|
||||
RequestBuilder<Drawable> rb = Glide.with(AssignIconsActivity.this)
|
||||
.load(entry.getIcon());
|
||||
return GlideHelper.setCommonOptions(rb, entry.getIcon().getType());
|
||||
}
|
||||
}
|
||||
|
||||
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<IconPack.Icon> {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<IconPack.Icon> getPreloadItems(int position) {
|
||||
AssignIconEntry entry = _entries.get(position);
|
||||
if (entry.getNewIcon() != null) {
|
||||
return Collections.singletonList(entry.getNewIcon());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
|
||||
RequestBuilder<Drawable> rb = Glide.with(AssignIconsActivity.this)
|
||||
.load(icon.getFile());
|
||||
return GlideHelper.setCommonOptions(rb, icon.getIconType());
|
||||
}
|
||||
}
|
||||
|
||||
private class SpacesItemDecoration extends RecyclerView.ItemDecoration {
|
||||
private final int _space;
|
||||
|
||||
public SpacesItemDecoration(int dpSpace) {
|
||||
|
||||
this._space = MetricsHelper.convertDpToPixels(AssignIconsActivity.this, dpSpace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
|
||||
outRect.left = _space;
|
||||
outRect.right = _space;
|
||||
outRect.bottom = _space;
|
||||
|
||||
if (parent.getChildLayoutPosition(view) == 0) {
|
||||
outRect.top = _space;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package com.beemdevelopment.aegis.ui;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.KeyEvent;
|
||||
|
@ -20,11 +19,9 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
|
@ -36,6 +33,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
|||
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
@ -43,6 +41,7 @@ import com.beemdevelopment.aegis.vault.slots.Slot;
|
|||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -55,10 +54,13 @@ public class AuthActivity extends AegisActivity {
|
|||
|
||||
private EditText _textPassword;
|
||||
|
||||
private VaultFile _vaultFile;
|
||||
private SlotList _slots;
|
||||
|
||||
private SecretKey _bioKey;
|
||||
private BiometricSlot _bioSlot;
|
||||
private BiometricPrompt _bioPrompt;
|
||||
private Button _decryptButton;
|
||||
|
||||
private int _failedUnlockAttempts;
|
||||
|
||||
|
@ -72,14 +74,14 @@ public class AuthActivity extends AegisActivity {
|
|||
setContentView(R.layout.activity_auth);
|
||||
_textPassword = findViewById(R.id.text_password);
|
||||
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
|
||||
Button decryptButton = findViewById(R.id.button_decrypt);
|
||||
_decryptButton = findViewById(R.id.button_decrypt);
|
||||
TextView biometricsButton = findViewById(R.id.button_biometrics);
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, new BackPressHandler());
|
||||
|
||||
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
decryptButton.performClick();
|
||||
_decryptButton.performClick();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
@ -104,17 +106,17 @@ public class AuthActivity extends AegisActivity {
|
|||
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
|
||||
}
|
||||
|
||||
if (_vaultManager.getVaultFileError() != null) {
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, _vaultManager.getVaultFileError(), (dialog, which) -> {
|
||||
try {
|
||||
_vaultFile = VaultRepository.readVaultFile(this);
|
||||
} catch (VaultRepositoryException e) {
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
VaultFile vaultFile = _vaultManager.getVaultFile();
|
||||
_slots = vaultFile.getHeader().getSlots();
|
||||
|
||||
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
|
||||
_slots = _vaultFile.getHeader().getSlots();
|
||||
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
|
||||
boolean invalidated = false;
|
||||
|
||||
|
@ -150,7 +152,7 @@ public class AuthActivity extends AegisActivity {
|
|||
}
|
||||
}
|
||||
|
||||
decryptButton.setOnClickListener(v -> {
|
||||
_decryptButton.setOnClickListener(v -> {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
|
||||
|
||||
|
@ -159,18 +161,23 @@ public class AuthActivity extends AegisActivity {
|
|||
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
|
||||
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener());
|
||||
task.execute(getLifecycle(), params);
|
||||
|
||||
_decryptButton.setEnabled(false);
|
||||
});
|
||||
|
||||
biometricsButton.setOnClickListener(v -> {
|
||||
if (_prefs.isPasswordReminderNeeded()) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(getString(R.string.password_reminder_dialog_title))
|
||||
.setMessage(getString(R.string.password_reminder_dialog_message))
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
showBiometricPrompt();
|
||||
})
|
||||
.create());
|
||||
} else {
|
||||
showBiometricPrompt();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -181,11 +188,6 @@ public class AuthActivity extends AegisActivity {
|
|||
outState.putBoolean("inhibitBioPrompt", _inhibitBioPrompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.NO_ACTION_BAR);
|
||||
}
|
||||
|
||||
private void selectPassword() {
|
||||
_textPassword.selectAll();
|
||||
|
||||
|
@ -238,11 +240,8 @@ public class AuthActivity extends AegisActivity {
|
|||
PopupWindow popup = new PopupWindow(popupLayout, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
popup.setFocusable(false);
|
||||
popup.setOutsideTouchable(true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
|
||||
popup.setElevation(5.0f);
|
||||
}
|
||||
_textPassword.post(() -> {
|
||||
if (isFinishing()) {
|
||||
if (isFinishing() || !_textPassword.isAttachedToWindow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -285,7 +284,7 @@ public class AuthActivity extends AegisActivity {
|
|||
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
|
||||
|
||||
try {
|
||||
_vaultManager.unlock(creds);
|
||||
_vaultManager.loadFrom(_vaultFile, creds);
|
||||
if (isSlotRepaired) {
|
||||
saveAndBackupVault();
|
||||
}
|
||||
|
@ -300,10 +299,11 @@ public class AuthActivity extends AegisActivity {
|
|||
}
|
||||
|
||||
private void onInvalidPassword() {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(AuthActivity.this, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(getString(R.string.unlock_vault_error))
|
||||
.setMessage(getString(R.string.unlock_vault_error_description))
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
|
||||
.create());
|
||||
|
||||
|
@ -343,6 +343,9 @@ public class AuthActivity extends AegisActivity {
|
|||
|
||||
finish(result.getKey(), result.isSlotRepaired());
|
||||
} else {
|
||||
_decryptButton.setEnabled(true);
|
||||
|
||||
_auditLogRepository.addVaultUnlockFailedPasswordEvent();
|
||||
onInvalidPassword();
|
||||
}
|
||||
}
|
||||
|
@ -355,6 +358,7 @@ public class AuthActivity extends AegisActivity {
|
|||
_bioPrompt = null;
|
||||
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
_auditLogRepository.addVaultUnlockFailedBiometricsEvent();
|
||||
Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,13 +15,16 @@ import android.view.ViewGroup;
|
|||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
@ -34,13 +37,14 @@ import com.beemdevelopment.aegis.encoding.Base32;
|
|||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.BitmapHelper;
|
||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SafHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleTextWatcher;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
|
@ -53,53 +57,63 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
|
|||
import com.beemdevelopment.aegis.otp.YandexInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
|
||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.imageview.ShapeableImageView;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TreeSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
public class EditEntryActivity extends AegisActivity {
|
||||
private static final int PICK_IMAGE_REQUEST = 0;
|
||||
|
||||
private boolean _isNew = false;
|
||||
private boolean _isManual = false;
|
||||
private VaultEntry _origEntry;
|
||||
private TreeSet<String> _groups;
|
||||
private Collection<VaultGroup> _groups;
|
||||
private boolean _hasCustomIcon = false;
|
||||
// keep track of icon changes separately as the generated jpeg's are not deterministic
|
||||
private boolean _hasChangedIcon = false;
|
||||
private IconPack.Icon _selectedIcon;
|
||||
private CircleImageView _iconView;
|
||||
private String _pickedMimeType;
|
||||
private ShapeableImageView _iconView;
|
||||
private ImageView _saveImageButton;
|
||||
|
||||
private TextInputEditText _textName;
|
||||
private TextInputEditText _textIssuer;
|
||||
private TextInputLayout _textGroupLayout;
|
||||
private TextInputEditText _textGroup;
|
||||
private TextInputEditText _textPeriodCounter;
|
||||
private TextInputLayout _textPeriodCounterLayout;
|
||||
private TextInputEditText _textDigits;
|
||||
|
@ -109,21 +123,44 @@ public class EditEntryActivity extends AegisActivity {
|
|||
private LinearLayout _textPinLayout;
|
||||
private TextInputEditText _textUsageCount;
|
||||
private TextInputEditText _textNote;
|
||||
private TextView _textLastUsed;
|
||||
|
||||
private AutoCompleteTextView _dropdownType;
|
||||
private AutoCompleteTextView _dropdownAlgo;
|
||||
private TextInputLayout _dropdownAlgoLayout;
|
||||
private AutoCompleteTextView _dropdownGroup;
|
||||
private List<String> _dropdownGroupList = new ArrayList<>();
|
||||
private List<UUID> _selectedGroups = new ArrayList<>();
|
||||
|
||||
private KropView _kropView;
|
||||
|
||||
private RelativeLayout _advancedSettingsHeader;
|
||||
private RelativeLayout _advancedSettings;
|
||||
private LinearLayout _advancedSettingsLayout;
|
||||
|
||||
private BackPressHandler _backPressHandler;
|
||||
private IconBackPressHandler _iconBackPressHandler;
|
||||
|
||||
private final ActivityResultLauncher<Intent> pickImageResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
Intent data = activityResult.getData();
|
||||
if (activityResult.getResultCode() != RESULT_OK || data == null || data.getData() == null) {
|
||||
return;
|
||||
}
|
||||
_pickedMimeType = SafHelper.getMimeType(this, data.getData());
|
||||
if (_pickedMimeType != null && _pickedMimeType.equals(IconType.SVG.toMimeType())) {
|
||||
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
|
||||
ImportFileTask task = new ImportFileTask(this, result -> {
|
||||
if (result.getError() == null) {
|
||||
CustomSvgIcon icon = new CustomSvgIcon(result.getFile());
|
||||
selectIcon(icon);
|
||||
} else {
|
||||
Dialogs.showErrorDialog(this, R.string.reading_file_error, result.getError());
|
||||
}
|
||||
});
|
||||
task.execute(getLifecycle(), params);
|
||||
} else {
|
||||
startEditingIcon(data.getData());
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -132,12 +169,13 @@ public class EditEntryActivity extends AegisActivity {
|
|||
}
|
||||
setContentView(R.layout.activity_edit_entry);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
|
||||
_groups = _vaultManager.getVault().getGroups();
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
if (bar != null) {
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_outline_close_24);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
|
@ -164,6 +202,8 @@ public class EditEntryActivity extends AegisActivity {
|
|||
_saveImageButton = findViewById(R.id.iv_saveImage);
|
||||
_textName = findViewById(R.id.text_name);
|
||||
_textIssuer = findViewById(R.id.text_issuer);
|
||||
_textGroup = findViewById(R.id.text_group);
|
||||
_textGroupLayout = findViewById(R.id.text_group_layout);
|
||||
_textPeriodCounter = findViewById(R.id.text_period_counter);
|
||||
_textPeriodCounterLayout = findViewById(R.id.text_period_counter_layout);
|
||||
_textDigits = findViewById(R.id.text_digits);
|
||||
|
@ -173,14 +213,12 @@ public class EditEntryActivity extends AegisActivity {
|
|||
_textPinLayout = findViewById(R.id.layout_pin);
|
||||
_textUsageCount = findViewById(R.id.text_usage_count);
|
||||
_textNote = findViewById(R.id.text_note);
|
||||
_textLastUsed = findViewById(R.id.text_last_used);
|
||||
_dropdownType = findViewById(R.id.dropdown_type);
|
||||
DropdownHelper.fillDropdown(this, _dropdownType, R.array.otp_types_array);
|
||||
_dropdownAlgoLayout = findViewById(R.id.dropdown_algo_layout);
|
||||
_dropdownAlgo = findViewById(R.id.dropdown_algo);
|
||||
DropdownHelper.fillDropdown(this, _dropdownAlgo, R.array.otp_algo_array);
|
||||
_dropdownGroup = findViewById(R.id.dropdown_group);
|
||||
updateGroupDropdownList();
|
||||
DropdownHelper.fillDropdown(this, _dropdownGroup, _dropdownGroupList);
|
||||
|
||||
// if this is NOT a manually entered entry, move the "Secret" field from basic to advanced settings
|
||||
if (!_isNew || !_isManual) {
|
||||
|
@ -209,22 +247,12 @@ public class EditEntryActivity extends AegisActivity {
|
|||
|
||||
_advancedSettingsHeader = findViewById(R.id.accordian_header);
|
||||
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
|
||||
_advancedSettings = findViewById(R.id.expandableLayout);
|
||||
_advancedSettingsLayout = findViewById(R.id.layout_advanced);
|
||||
|
||||
// fill the fields with values if possible
|
||||
GlideHelper.loadEntryIcon(Glide.with(this), _origEntry, _iconView);
|
||||
if (_origEntry.hasIcon()) {
|
||||
IconViewHelper.setLayerType(_iconView, _origEntry.getIconType());
|
||||
Glide.with(this)
|
||||
.asDrawable()
|
||||
.load(_origEntry)
|
||||
.set(IconLoader.ICON_TYPE, _origEntry.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_iconView);
|
||||
_hasCustomIcon = true;
|
||||
} else {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
_textName.setText(_origEntry.getName());
|
||||
|
@ -262,8 +290,18 @@ public class EditEntryActivity extends AegisActivity {
|
|||
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
|
||||
updatePinFieldVisibility(_origEntry.getInfo().getTypeId());
|
||||
|
||||
String group = _origEntry.getGroup();
|
||||
setGroup(group);
|
||||
Set<UUID> groups = _origEntry.getGroups();
|
||||
if (groups.isEmpty()) {
|
||||
_textGroup.setText(getString(R.string.no_group));
|
||||
} else {
|
||||
String text = groups.stream().map(uuid -> {
|
||||
VaultGroup group = _vaultManager.getVault().getGroupByUUID(uuid);
|
||||
return group.getName();
|
||||
})
|
||||
.collect(Collectors.joining(", "));
|
||||
_selectedGroups.addAll(groups);
|
||||
_textGroup.setText(text);
|
||||
}
|
||||
|
||||
// Update the icon if the issuer or name has changed
|
||||
_textIssuer.addTextChangedListener(_nameChangeListener);
|
||||
|
@ -271,11 +309,11 @@ public class EditEntryActivity extends AegisActivity {
|
|||
|
||||
// Register listeners to trigger validation
|
||||
_textIssuer.addTextChangedListener(_validationListener);
|
||||
_textGroup.addTextChangedListener(_validationListener);
|
||||
_textName.addTextChangedListener(_validationListener);
|
||||
_textNote.addTextChangedListener(_validationListener);
|
||||
_textSecret.addTextChangedListener(_validationListener);
|
||||
_dropdownType.addTextChangedListener(_validationListener);
|
||||
_dropdownGroup.addTextChangedListener(_validationListener);
|
||||
_dropdownAlgo.addTextChangedListener(_validationListener);
|
||||
_textPeriodCounter.addTextChangedListener(_validationListener);
|
||||
_textDigits.addTextChangedListener(_validationListener);
|
||||
|
@ -327,28 +365,107 @@ public class EditEntryActivity extends AegisActivity {
|
|||
startIconSelection();
|
||||
});
|
||||
|
||||
_dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
private int prevPosition = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (position == _dropdownGroupList.size() - 1) {
|
||||
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, text -> {
|
||||
String groupName = new String(text);
|
||||
if (!groupName.isEmpty()) {
|
||||
_groups.add(groupName);
|
||||
updateGroupDropdownList();
|
||||
_dropdownGroup.setText(groupName, false);
|
||||
}
|
||||
});
|
||||
_dropdownGroup.setText(_dropdownGroupList.get(prevPosition), false);
|
||||
} else {
|
||||
prevPosition = position;
|
||||
}
|
||||
_textGroup.setShowSoftInputOnFocus(false);
|
||||
_textGroup.setOnClickListener(v -> showGroupSelectionDialog());
|
||||
_textGroup.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) {
|
||||
showGroupSelectionDialog();
|
||||
}
|
||||
});
|
||||
|
||||
_textGroupLayout.setOnClickListener(v -> {
|
||||
showGroupSelectionDialog();
|
||||
});
|
||||
|
||||
_textUsageCount.setText(_prefs.getUsageCount(entryUUID).toString());
|
||||
setLastUsedTimestamp(_prefs.getLastUsedTimestamp(entryUUID));
|
||||
}
|
||||
|
||||
private void showGroupSelectionDialog() {
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(this);
|
||||
View view = getLayoutInflater().inflate(R.layout.dialog_select_groups, null);
|
||||
dialog.setContentView(view);
|
||||
|
||||
ChipGroup chipGroup = view.findViewById(R.id.groupChipGroup);
|
||||
TextView addGroupInfo = view.findViewById(R.id.addGroupInfo);
|
||||
LinearLayout addGroup = view.findViewById(R.id.addGroup);
|
||||
Button clearButton = view.findViewById(R.id.btnClear);
|
||||
Button saveButton = view.findViewById(R.id.btnSave);
|
||||
|
||||
chipGroup.removeAllViews();
|
||||
addGroupInfo.setVisibility(View.VISIBLE);
|
||||
addGroup.setVisibility(View.VISIBLE);
|
||||
|
||||
for (VaultGroup group : _groups) {
|
||||
addChipTo(chipGroup, new VaultGroupModel(group), false);
|
||||
}
|
||||
|
||||
addGroup.setOnClickListener(v1 -> {
|
||||
Dialogs.TextInputListener onAddGroup = text -> {
|
||||
String groupName = new String(text).trim();
|
||||
if (!groupName.isEmpty()) {
|
||||
VaultGroup group = _vaultManager.getVault().findGroupByName(groupName);
|
||||
if (group == null) {
|
||||
group = new VaultGroup(groupName);
|
||||
_vaultManager.getVault().addGroup(group);
|
||||
}
|
||||
|
||||
_selectedGroups.add(group.getUUID());
|
||||
addChipTo(chipGroup, new VaultGroupModel(group), true);
|
||||
}
|
||||
};
|
||||
|
||||
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, onAddGroup);
|
||||
});
|
||||
|
||||
saveButton.setOnClickListener(v1 -> {
|
||||
if(getCheckedUUID(chipGroup).isEmpty()) {
|
||||
_selectedGroups.clear();
|
||||
_textGroup.setText(getString(R.string.no_group));
|
||||
} else {
|
||||
_selectedGroups.clear();
|
||||
_selectedGroups.addAll(getCheckedUUID(chipGroup));
|
||||
_textGroup.setText(getCheckedNames(chipGroup));
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
clearButton.setOnClickListener(v1 -> {
|
||||
chipGroup.clearCheck();
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group, Boolean isNew) {
|
||||
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
|
||||
chip.setText(group.getName());
|
||||
chip.setCheckable(true);
|
||||
|
||||
chip.setChecked((!_selectedGroups.isEmpty() && _selectedGroups.contains(group.getUUID())) || isNew);
|
||||
chip.setCheckedIconVisible(true);
|
||||
chip.setTag(group);
|
||||
chipGroup.addView(chip);
|
||||
}
|
||||
|
||||
private static Set<UUID> getCheckedUUID(ChipGroup chipGroup) {
|
||||
return chipGroup.getCheckedChipIds().stream()
|
||||
.map(i -> {
|
||||
Chip chip = chipGroup.findViewById(i);
|
||||
VaultGroupModel group = (VaultGroupModel) chip.getTag();
|
||||
return group.getUUID();
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static String getCheckedNames(ChipGroup chipGroup) {
|
||||
return chipGroup.getCheckedChipIds().stream()
|
||||
.map(i -> {
|
||||
Chip chip = chipGroup.findViewById(i);
|
||||
VaultGroupModel group = (VaultGroupModel) chip.getTag();
|
||||
return group.getName();
|
||||
})
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
private void updateAdvancedFieldStatus(String otpType) {
|
||||
|
@ -365,41 +482,20 @@ public class EditEntryActivity extends AegisActivity {
|
|||
_textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin);
|
||||
}
|
||||
|
||||
private void setGroup(String groupName) {
|
||||
int pos = 0;
|
||||
if (groupName != null) {
|
||||
pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() + 1 : 0;
|
||||
}
|
||||
|
||||
_dropdownGroup.setText(_dropdownGroupList.get(pos), false);
|
||||
}
|
||||
|
||||
private void openAdvancedSettings() {
|
||||
Animation fadeOut = new AlphaAnimation(1, 0);
|
||||
fadeOut.setInterpolator(new AccelerateInterpolator());
|
||||
fadeOut.setDuration(220 * (long) AnimationsHelper.Scale.ANIMATOR.getValue(this));
|
||||
fadeOut.setDuration((long) (220 * AnimationsHelper.Scale.ANIMATOR.getValue(this)));
|
||||
_advancedSettingsHeader.startAnimation(fadeOut);
|
||||
|
||||
Animation fadeIn = new AlphaAnimation(0, 1);
|
||||
fadeIn.setInterpolator(new AccelerateInterpolator());
|
||||
fadeIn.setDuration(250 * (long) AnimationsHelper.Scale.ANIMATOR.getValue(this));
|
||||
|
||||
fadeOut.setAnimationListener(new SimpleAnimationEndListener((a) -> {
|
||||
_advancedSettingsHeader.setVisibility(View.GONE);
|
||||
_advancedSettings.startAnimation(fadeIn);
|
||||
_advancedSettingsLayout.setVisibility(View.VISIBLE);
|
||||
_advancedSettingsLayout.animate()
|
||||
.setInterpolator(new AccelerateInterpolator())
|
||||
.setDuration((long) (250 * AnimationsHelper.Scale.ANIMATOR.getValue(this)))
|
||||
.alpha(1);
|
||||
}));
|
||||
|
||||
fadeIn.setAnimationListener(new SimpleAnimationEndListener((a) -> {
|
||||
_advancedSettings.setVisibility(View.VISIBLE);
|
||||
}));
|
||||
}
|
||||
|
||||
private void updateGroupDropdownList() {
|
||||
Resources res = getResources();
|
||||
_dropdownGroupList.clear();
|
||||
_dropdownGroupList.add(res.getString(R.string.no_group));
|
||||
_dropdownGroupList.addAll(_groups);
|
||||
_dropdownGroupList.add(res.getString(R.string.new_group));
|
||||
}
|
||||
|
||||
private boolean hasUnsavedChanges(VaultEntry newEntry) {
|
||||
|
@ -437,38 +533,33 @@ public class EditEntryActivity extends AegisActivity {
|
|||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
discardAndFinish();
|
||||
break;
|
||||
case R.id.action_save:
|
||||
onSave();
|
||||
break;
|
||||
case R.id.action_delete:
|
||||
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry), (dialog, which) -> {
|
||||
deleteAndFinish(_origEntry);
|
||||
});
|
||||
break;
|
||||
case R.id.action_edit_icon:
|
||||
startIconSelection();
|
||||
break;
|
||||
case R.id.action_reset_usage_count:
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.action_reset_usage_count)
|
||||
.setMessage(R.string.action_reset_usage_count_dialog)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> resetUsageCount())
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
break;
|
||||
case R.id.action_default_icon:
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
discardAndFinish();
|
||||
} else if (itemId == R.id.action_save) {
|
||||
onSave();
|
||||
} else if (itemId == R.id.action_delete) {
|
||||
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry), (dialog, which) -> {
|
||||
deleteAndFinish(_origEntry);
|
||||
});
|
||||
} else if (itemId == R.id.action_edit_icon) {
|
||||
startIconSelection();
|
||||
} else if (itemId == R.id.action_reset_usage_count) {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.action_reset_usage_count)
|
||||
.setMessage(R.string.action_reset_usage_count_dialog)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> resetUsageCount())
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
} else if (itemId == R.id.action_default_icon) {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
|
||||
_selectedIcon = null;
|
||||
_hasCustomIcon = false;
|
||||
_hasChangedIcon = true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
_selectedIcon = null;
|
||||
_hasCustomIcon = false;
|
||||
_hasChangedIcon = true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -483,7 +574,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
|
||||
_vaultManager.startActivityForResult(this, chooserIntent, PICK_IMAGE_REQUEST);
|
||||
_vaultManager.fireIntentLauncher(this, chooserIntent, pickImageResultLauncher);
|
||||
}
|
||||
|
||||
private void resetUsageCount() {
|
||||
|
@ -500,7 +591,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), new IconAdapter.Listener() {
|
||||
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), true, new IconAdapter.Listener() {
|
||||
@Override
|
||||
public void onIconSelected(IconPack.Icon icon) {
|
||||
selectIcon(icon);
|
||||
|
@ -519,14 +610,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
_hasCustomIcon = true;
|
||||
_hasChangedIcon = true;
|
||||
|
||||
IconViewHelper.setLayerType(_iconView, icon.getIconType());
|
||||
Glide.with(EditEntryActivity.this)
|
||||
.asDrawable()
|
||||
.load(icon.getFile())
|
||||
.set(IconLoader.ICON_TYPE, icon.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_iconView);
|
||||
GlideHelper.loadIcon(Glide.with(EditEntryActivity.this), icon, _iconView);
|
||||
}
|
||||
|
||||
private void startEditingIcon(Uri data) {
|
||||
|
@ -596,6 +680,16 @@ public class EditEntryActivity extends AegisActivity {
|
|||
saveAndFinish(entry, false);
|
||||
}
|
||||
|
||||
private void setLastUsedTimestamp(long timestamp) {
|
||||
String readableDate = getString(R.string.last_used_never);
|
||||
if (timestamp != 0) {
|
||||
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.getDefault());
|
||||
readableDate = dateFormat.format(new Date(timestamp));
|
||||
}
|
||||
|
||||
_textLastUsed.setText(String.format("%s: %s", getString(R.string.last_used), readableDate));
|
||||
}
|
||||
|
||||
private void deleteAndFinish(VaultEntry entry) {
|
||||
_vaultManager.getVault().removeEntry(entry);
|
||||
saveAndFinish(entry, true);
|
||||
|
@ -612,29 +706,6 @@ public class EditEntryActivity extends AegisActivity {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
|
||||
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
|
||||
String fileType = SafHelper.getMimeType(this, data.getData());
|
||||
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
|
||||
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
|
||||
ImportFileTask task = new ImportFileTask(this, result -> {
|
||||
if (result.getError() == null) {
|
||||
CustomSvgIcon icon = new CustomSvgIcon(result.getFile());
|
||||
selectIcon(icon);
|
||||
} else {
|
||||
Dialogs.showErrorDialog(this, R.string.reading_file_error, result.getError());
|
||||
}
|
||||
});
|
||||
task.execute(getLifecycle(), params);
|
||||
} else {
|
||||
startEditingIcon(data.getData());
|
||||
}
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
private int parsePeriod() throws ParseException {
|
||||
try {
|
||||
return Integer.parseInt(_textPeriodCounter.getText().toString());
|
||||
|
@ -726,23 +797,23 @@ public class EditEntryActivity extends AegisActivity {
|
|||
entry.setName(_textName.getText().toString());
|
||||
entry.setNote(_textNote.getText().toString());
|
||||
|
||||
int groupPos = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
|
||||
if (groupPos != 0) {
|
||||
String group = _dropdownGroupList.get(groupPos);
|
||||
entry.setGroup(group);
|
||||
if (_selectedGroups.isEmpty()) {
|
||||
entry.setGroups(new HashSet<>());
|
||||
} else {
|
||||
entry.setGroup(null);
|
||||
entry.setGroups(new HashSet<>(_selectedGroups));
|
||||
}
|
||||
|
||||
if (_hasChangedIcon) {
|
||||
if (_hasCustomIcon) {
|
||||
VaultEntryIcon icon;
|
||||
if (_selectedIcon == null) {
|
||||
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
// the quality parameter is ignored for PNG
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
byte[] data = stream.toByteArray();
|
||||
entry.setIcon(data, IconType.PNG);
|
||||
IconType iconType = _pickedMimeType == null
|
||||
? IconType.INVALID : IconType.fromMimeType(_pickedMimeType);
|
||||
if (iconType == IconType.INVALID) {
|
||||
iconType = bitmap.hasAlpha() ? IconType.PNG : IconType.JPEG;
|
||||
}
|
||||
icon = BitmapHelper.toVaultEntryIcon(bitmap, iconType);
|
||||
} else {
|
||||
byte[] iconBytes;
|
||||
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
|
||||
|
@ -750,11 +821,12 @@ public class EditEntryActivity extends AegisActivity {
|
|||
} catch (IOException e) {
|
||||
throw new ParseException(e.getMessage());
|
||||
}
|
||||
|
||||
entry.setIcon(iconBytes, _selectedIcon.getIconType());
|
||||
icon = new VaultEntryIcon(iconBytes, _selectedIcon.getIconType());
|
||||
}
|
||||
|
||||
entry.setIcon(icon);
|
||||
} else {
|
||||
entry.setIcon(null, IconType.INVALID);
|
||||
entry.setIcon(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -762,9 +834,10 @@ public class EditEntryActivity extends AegisActivity {
|
|||
}
|
||||
|
||||
private void onSaveError(String msg) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(getString(R.string.saving_profile_error))
|
||||
.setMessage(msg)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create());
|
||||
}
|
||||
|
@ -782,10 +855,92 @@ public class EditEntryActivity extends AegisActivity {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (_isNew) {
|
||||
for (VaultEntry existing : _vaultManager.getVault().getEntries()) {
|
||||
if (entry.hasSameNameAndIssuer(existing)) {
|
||||
showDuplicateBottomSheet(entry);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addAndFinish(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showDuplicateBottomSheet(VaultEntry newEntry) {
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(this);
|
||||
View view = getLayoutInflater().inflate(R.layout.dialog_duplicate_entry, null);
|
||||
dialog.setContentView(view);
|
||||
|
||||
dialog.setCancelable(false);
|
||||
|
||||
View overwrite = view.findViewById(R.id.overwrite_entry);
|
||||
View addSuffix = view.findViewById(R.id.create_new_entry);
|
||||
View cancel = view.findViewById(R.id.cancel_save);
|
||||
|
||||
TextView suffixSubtext = view.findViewById(R.id.duplicate_suffix_subtitle);
|
||||
|
||||
String baseName = newEntry.getName();
|
||||
Set<String> existingNames = new HashSet<>();
|
||||
for (VaultEntry e : _vaultManager.getVault().getEntries()) {
|
||||
if (e.getIssuer().equals(newEntry.getIssuer())) {
|
||||
existingNames.add(e.getName());
|
||||
}
|
||||
}
|
||||
|
||||
int counter = 2;
|
||||
String newName;
|
||||
do {
|
||||
newName = baseName + " #" + counter++;
|
||||
} while (existingNames.contains(newName));
|
||||
|
||||
suffixSubtext.setText(getString(R.string.dialog_duplicate_entry_suffix_subtitle, newName));
|
||||
|
||||
overwrite.setOnClickListener(v -> {
|
||||
List<VaultEntry> duplicates = new ArrayList<>();
|
||||
for (VaultEntry existing : _vaultManager.getVault().getEntries()) {
|
||||
if (existing.hasSameNameAndIssuer(newEntry)) {
|
||||
duplicates.add(existing);
|
||||
}
|
||||
}
|
||||
|
||||
Resources res = getResources();
|
||||
String message = res.getQuantityString(
|
||||
R.plurals.dialog_duplicate_entry_overwrite_dialog_message,
|
||||
duplicates.size(),
|
||||
duplicates.size(),
|
||||
newEntry.getIssuer(),
|
||||
newEntry.getName()
|
||||
);
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_duplicate_entry_overwrite_dialog_title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.action_delete, (d, which) -> {
|
||||
for (VaultEntry dup : duplicates) {
|
||||
_vaultManager.getVault().removeEntry(dup);
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
addAndFinish(newEntry);
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
});
|
||||
|
||||
String finalNewName = newName;
|
||||
addSuffix.setOnClickListener(v -> {
|
||||
newEntry.setName(finalNewName);
|
||||
dialog.dismiss();
|
||||
addAndFinish(newEntry);
|
||||
});
|
||||
|
||||
cancel.setOnClickListener(v -> dialog.dismiss());
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private static void setViewEnabled(View view, boolean enabled) {
|
||||
view.setEnabled(enabled);
|
||||
|
||||
|
@ -852,11 +1007,12 @@ public class EditEntryActivity extends AegisActivity {
|
|||
private final File _file;
|
||||
|
||||
protected CustomSvgIcon(File file) {
|
||||
super(file.getAbsolutePath(), null, null);
|
||||
super(file.getAbsolutePath(), null, null, null);
|
||||
_file = file;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public File getFile() {
|
||||
return _file;
|
||||
}
|
||||
|
|
|
@ -7,24 +7,28 @@ import android.view.View;
|
|||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
|
||||
private GroupAdapter _adapter;
|
||||
private HashSet<String> _removedGroups;
|
||||
private RecyclerView _slotsView;
|
||||
private HashSet<UUID> _removedGroups;
|
||||
private RecyclerView _groupsView;
|
||||
private View _emptyStateView;
|
||||
private BackPressHandler _backPressHandler;
|
||||
|
||||
|
@ -36,6 +40,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
}
|
||||
setContentView(R.layout.activity_groups);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
@ -43,22 +48,51 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
_backPressHandler = new BackPressHandler();
|
||||
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
|
||||
|
||||
_removedGroups = new HashSet<>();
|
||||
if (savedInstanceState != null) {
|
||||
List<String> groups = savedInstanceState.getStringArrayList("removedGroups");
|
||||
_removedGroups = new HashSet<>(Objects.requireNonNull(groups));
|
||||
} else {
|
||||
_removedGroups = new HashSet<>();
|
||||
List<String> removedGroups = savedInstanceState.getStringArrayList("removedGroups");
|
||||
if (removedGroups != null) {
|
||||
for (String uuid : removedGroups) {
|
||||
_removedGroups.add(UUID.fromString(uuid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_adapter = new GroupAdapter(this);
|
||||
_slotsView= findViewById(R.id.list_slots);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
_slotsView.setLayoutManager(layoutManager);
|
||||
_slotsView.setAdapter(_adapter);
|
||||
_slotsView.setNestedScrollingEnabled(false);
|
||||
ItemTouchHelper touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||
@Override
|
||||
public int getMovementFlags(
|
||||
@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
|
||||
for (String group : _vaultManager.getVault().getGroups()) {
|
||||
_adapter.addGroup(group);
|
||||
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
|
||||
int draggedItemIndex = viewHolder.getBindingAdapterPosition();
|
||||
int targetIndex = target.getBindingAdapterPosition();
|
||||
|
||||
_adapter.onItemMove(draggedItemIndex, targetIndex);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { }
|
||||
});
|
||||
|
||||
_adapter = new GroupAdapter(this);
|
||||
_groupsView = findViewById(R.id.list_groups);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
_groupsView.setLayoutManager(layoutManager);
|
||||
_groupsView.setAdapter(_adapter);
|
||||
_groupsView.setNestedScrollingEnabled(false);
|
||||
touchHelper.attachToRecyclerView(_groupsView);
|
||||
|
||||
for (VaultGroup group : _vaultManager.getVault().getGroups()) {
|
||||
if (!_removedGroups.contains(group.getUUID())) {
|
||||
_adapter.addGroup(group);
|
||||
}
|
||||
}
|
||||
|
||||
_emptyStateView = findViewById(R.id.vEmptyList);
|
||||
|
@ -68,16 +102,37 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putStringArrayList("removedGroups", new ArrayList<>(_removedGroups));
|
||||
ArrayList<String> removed = new ArrayList<>();
|
||||
for (UUID uuid : _removedGroups) {
|
||||
removed.add(uuid.toString());
|
||||
}
|
||||
|
||||
outState.putStringArrayList("removedGroups", removed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveGroup(String group) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
public void onEditGroup(VaultGroup group) {
|
||||
Dialogs.TextInputListener onEditGroup = text -> {
|
||||
String newGroupName = new String(text).trim();
|
||||
if (!newGroupName.isEmpty()) {
|
||||
VaultGroup newGroup = Cloner.clone(group);
|
||||
newGroup.setName(newGroupName);
|
||||
_adapter.replaceGroup(group.getUUID(), newGroup);
|
||||
_backPressHandler.setEnabled(true);
|
||||
}
|
||||
};
|
||||
|
||||
Dialogs.showTextInputDialog(GroupManagerActivity.this, R.string.rename_group, R.string.group_name_hint, onEditGroup, group.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveGroup(VaultGroup group) {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.remove_group)
|
||||
.setMessage(R.string.remove_group_description)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
_removedGroups.add(group);
|
||||
_removedGroups.add(group.getUUID());
|
||||
_adapter.removeGroup(group);
|
||||
_backPressHandler.setEnabled(true);
|
||||
updateEmptyState();
|
||||
|
@ -86,17 +141,36 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
.create());
|
||||
}
|
||||
|
||||
public void onRemoveUnusedGroups() {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.remove_unused_groups)
|
||||
.setMessage(R.string.remove_unused_groups_description)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
Set<VaultGroup> unusedGroups = new HashSet<>(_vaultManager.getVault().getGroups());
|
||||
unusedGroups.removeAll(_vaultManager.getVault().getUsedGroups());
|
||||
|
||||
for (VaultGroup group : unusedGroups) {
|
||||
_removedGroups.add(group.getUUID());
|
||||
_adapter.removeGroup(group);
|
||||
}
|
||||
_backPressHandler.setEnabled(true);
|
||||
updateEmptyState();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
}
|
||||
|
||||
private void saveAndFinish() {
|
||||
if (!_removedGroups.isEmpty()) {
|
||||
for (VaultEntry entry : _vaultManager.getVault().getEntries()) {
|
||||
if (_removedGroups.contains(entry.getGroup())) {
|
||||
entry.setGroup(null);
|
||||
}
|
||||
for (UUID uuid : _removedGroups) {
|
||||
_vaultManager.getVault().removeGroup(uuid);
|
||||
}
|
||||
|
||||
saveAndBackupVault();
|
||||
}
|
||||
|
||||
_vaultManager.getVault().replaceGroups(_adapter.getGroups());
|
||||
saveAndBackupVault();
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
|
@ -119,15 +193,15 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
discardAndFinish();
|
||||
break;
|
||||
case R.id.action_save:
|
||||
saveAndFinish();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
discardAndFinish();
|
||||
} else if (itemId == R.id.action_save) {
|
||||
saveAndFinish();
|
||||
} else if (itemId == R.id.action_delete_unused_groups) {
|
||||
onRemoveUnusedGroups();
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -135,10 +209,10 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
|
||||
private void updateEmptyState() {
|
||||
if (_adapter.getItemCount() > 0) {
|
||||
_slotsView.setVisibility(View.VISIBLE);
|
||||
_groupsView.setVisibility(View.VISIBLE);
|
||||
_emptyStateView.setVisibility(View.GONE);
|
||||
} else {
|
||||
_slotsView.setVisibility(View.GONE);
|
||||
_groupsView.setVisibility(View.GONE);
|
||||
_emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,44 +1,63 @@
|
|||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.BitmapHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
import com.beemdevelopment.aegis.ui.tasks.IconOptimizationTask;
|
||||
import com.beemdevelopment.aegis.ui.tasks.RootShellTask;
|
||||
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ImportEntriesActivity extends AegisActivity {
|
||||
private View _view;
|
||||
private Menu _menu;
|
||||
private RecyclerView _entriesView;
|
||||
private ImportEntriesAdapter _adapter;
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
private UUIDMap<VaultGroup> _importedGroups;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -47,14 +66,17 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
}
|
||||
setContentView(R.layout.activity_import_entries);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
|
||||
_view = findViewById(R.id.importEntriesRootView);
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_outline_close_24);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
_adapter = new ImportEntriesAdapter();
|
||||
RecyclerView entriesView = findViewById(R.id.list_entries);
|
||||
entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
_entriesView = findViewById(R.id.list_entries);
|
||||
_entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
|
@ -63,9 +85,9 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
});
|
||||
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
entriesView.setLayoutManager(layoutManager);
|
||||
entriesView.setAdapter(_adapter);
|
||||
entriesView.setNestedScrollingEnabled(false);
|
||||
_entriesView.setLayoutManager(layoutManager);
|
||||
_entriesView.setAdapter(_adapter);
|
||||
_entriesView.setNestedScrollingEnabled(false);
|
||||
|
||||
FloatingActionButton fab = findViewById(R.id.fab);
|
||||
fab.setOnClickListener(v -> {
|
||||
|
@ -88,10 +110,11 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
if (importer.isInstalledAppVersionSupported()) {
|
||||
startImportApp(importer);
|
||||
} else {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(getString(R.string.app_version_error, importerDef.getName()))
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(R.string.yes, (dialog1, which) -> {
|
||||
startImportApp(importer);
|
||||
})
|
||||
|
@ -156,7 +179,7 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
state.decrypt(this, new DatabaseImporter.DecryptListener() {
|
||||
@Override
|
||||
public void onStateDecrypted(DatabaseImporter.State state) {
|
||||
importDatabase(state);
|
||||
processDecryptedImporterState(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -171,7 +194,7 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
importDatabase(state);
|
||||
processDecryptedImporterState(state);
|
||||
}
|
||||
} catch (DatabaseImporterException e) {
|
||||
e.printStackTrace();
|
||||
|
@ -179,7 +202,7 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private void importDatabase(DatabaseImporter.State state) {
|
||||
private void processDecryptedImporterState(DatabaseImporter.State state) {
|
||||
DatabaseImporter.Result result;
|
||||
try {
|
||||
result = state.convert();
|
||||
|
@ -189,16 +212,43 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
UUIDMap<VaultEntry> entries = result.getEntries();
|
||||
for (VaultEntry entry : entries.getValues()) {
|
||||
_adapter.addEntry(new ImportEntry(entry));
|
||||
Map<UUID, VaultEntryIcon> icons = result.getEntries().getValues().stream()
|
||||
.filter(e -> e.getIcon() != null
|
||||
&& !e.getIcon().getType().equals(IconType.SVG)
|
||||
&& !BitmapHelper.isVaultEntryIconOptimized(e.getIcon()))
|
||||
.collect(Collectors.toMap(VaultEntry::getUUID, VaultEntry::getIcon));
|
||||
if (!icons.isEmpty()) {
|
||||
IconOptimizationTask task = new IconOptimizationTask(this, newIcons -> {
|
||||
for (Map.Entry<UUID, VaultEntryIcon> mapEntry : newIcons.entrySet()) {
|
||||
VaultEntry entry = result.getEntries().getByUUID(mapEntry.getKey());
|
||||
entry.setIcon(mapEntry.getValue());
|
||||
}
|
||||
|
||||
processImporterResult(result);
|
||||
});
|
||||
task.execute(getLifecycle(), icons);
|
||||
} else {
|
||||
processImporterResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
private void processImporterResult(DatabaseImporter.Result result) {
|
||||
List<ImportEntry> importEntries = new ArrayList<>();
|
||||
for (VaultEntry entry : result.getEntries().getValues()) {
|
||||
ImportEntry importEntry = new ImportEntry(entry);
|
||||
_adapter.addEntry(importEntry);
|
||||
importEntries.add(importEntry);
|
||||
}
|
||||
|
||||
_importedGroups = result.getGroups();
|
||||
|
||||
List<DatabaseImporterEntryException> errors = result.getErrors();
|
||||
if (errors.size() > 0) {
|
||||
String message = getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size());
|
||||
Dialogs.showMultiErrorDialog(this, R.string.import_error_title, message, errors, null);
|
||||
Dialogs.showMultiExceptionDialog(this, R.string.import_error_title, message, errors, null);
|
||||
}
|
||||
|
||||
findDuplicates(importEntries);
|
||||
}
|
||||
|
||||
private void showWipeEntriesDialog() {
|
||||
|
@ -212,10 +262,43 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
private void saveAndFinish(boolean wipeEntries) {
|
||||
VaultRepository vault = _vaultManager.getVault();
|
||||
if (wipeEntries) {
|
||||
vault.wipeEntries();
|
||||
vault.wipeContents();
|
||||
}
|
||||
|
||||
// Given the list of selected entries, collect the UUID's of all groups
|
||||
// that we're actually going to import
|
||||
List<ImportEntry> selectedEntries = _adapter.getCheckedEntries();
|
||||
List<UUID> selectedGroupUuids = new ArrayList<>();
|
||||
for (ImportEntry entry : selectedEntries) {
|
||||
selectedGroupUuids.addAll(entry.getEntry().getGroups());
|
||||
}
|
||||
|
||||
// Add all of the new groups to the vault. If a group with the same name already
|
||||
// exists in the vault, rewrite all entries in that group to reference the existing group.
|
||||
for (VaultGroup importedGroup : _importedGroups) {
|
||||
if (!selectedGroupUuids.contains(importedGroup.getUUID())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
VaultGroup existingGroup = vault.findGroupByUUID(importedGroup.getUUID());
|
||||
if (existingGroup != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
existingGroup = vault.findGroupByName(importedGroup.getName());
|
||||
if (existingGroup == null) {
|
||||
vault.addGroup(importedGroup);
|
||||
} else {
|
||||
for (ImportEntry entry : selectedEntries) {
|
||||
Set<UUID> entryGroups = entry.getEntry().getGroups();
|
||||
if (entryGroups.contains(importedGroup.getUUID())) {
|
||||
entryGroups.remove(importedGroup.getUUID());
|
||||
entryGroups.add(existingGroup.getUUID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ImportEntry selectedEntry : selectedEntries) {
|
||||
VaultEntry entry = selectedEntry.getEntry();
|
||||
|
||||
|
@ -231,11 +314,85 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size());
|
||||
Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show();
|
||||
|
||||
|
||||
setResult(RESULT_OK, null);
|
||||
finish();
|
||||
|
||||
if (_iconPackManager.hasIconPack()) {
|
||||
ArrayList<UUID> assignIconEntriesIds = new ArrayList<>();
|
||||
Intent assignIconIntent = new Intent(getBaseContext(), AssignIconsActivity.class);
|
||||
for (ImportEntry entry : selectedEntries) {
|
||||
assignIconEntriesIds.add(entry.getEntry().getUUID());
|
||||
}
|
||||
|
||||
assignIconIntent.putExtra("entries", assignIconEntriesIds);
|
||||
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.import_assign_icons_dialog_title)
|
||||
.setMessage(R.string.import_assign_icons_dialog_text)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
startActivity(assignIconIntent);
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, ((dialogInterface, i) -> finish()))
|
||||
.create());
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void findDuplicates(List<ImportEntry> importEntries) {
|
||||
List<UUID> duplicateEntries = new ArrayList<>();
|
||||
for (ImportEntry importEntry: importEntries) {
|
||||
boolean exists = _vaultManager.getVault().getEntries().stream().anyMatch(item ->
|
||||
item.getIssuer().equals(importEntry.getEntry().getIssuer()) &&
|
||||
Arrays.equals(item.getInfo().getSecret(), importEntry.getEntry().getInfo().getSecret()));
|
||||
|
||||
if (exists) {
|
||||
duplicateEntries.add(importEntry.getEntry().getUUID());
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicateEntries.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
_adapter.setCheckboxStates(duplicateEntries, false);
|
||||
Snackbar snackbar = Snackbar.make(_view, getResources().getQuantityString(R.plurals.import_duplicate_toast, duplicateEntries.size(), duplicateEntries.size()), Snackbar.LENGTH_INDEFINITE);
|
||||
snackbar.addCallback(new Snackbar.Callback() {
|
||||
@Override
|
||||
public void onShown(Snackbar sb) {
|
||||
int snackbarHeight = sb.getView().getHeight();
|
||||
|
||||
_entriesView.setPadding(
|
||||
_entriesView.getPaddingLeft(),
|
||||
_entriesView.getPaddingTop(),
|
||||
_entriesView.getPaddingRight(),
|
||||
_entriesView.getPaddingBottom() + snackbarHeight * 2
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissed(Snackbar sb, int event) {
|
||||
int snackbarHeight = sb.getView().getHeight();
|
||||
|
||||
_entriesView.setPadding(
|
||||
_entriesView.getPaddingLeft(),
|
||||
_entriesView.getPaddingTop(),
|
||||
_entriesView.getPaddingRight(),
|
||||
_entriesView.getPaddingBottom() - snackbarHeight * 2
|
||||
);
|
||||
}
|
||||
});
|
||||
snackbar.setAction(R.string.undo, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
_adapter.setCheckboxStates(duplicateEntries, true);
|
||||
}
|
||||
});
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
_menu = menu;
|
||||
|
@ -245,18 +402,15 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
break;
|
||||
case R.id.toggle_checkboxes:
|
||||
_adapter.toggleCheckboxes();
|
||||
break;
|
||||
case R.id.toggle_wipe_vault:
|
||||
item.setChecked(!item.isChecked());
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
finish();
|
||||
} else if (itemId == R.id.toggle_checkboxes) {
|
||||
_adapter.toggleCheckboxes();
|
||||
} else if (itemId == R.id.toggle_wipe_vault) {
|
||||
item.setChecked(!item.isChecked());
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -13,7 +13,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.intro.IntroBaseActivity;
|
||||
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
|
||||
|
@ -21,7 +20,9 @@ import com.beemdevelopment.aegis.ui.slides.DoneSlide;
|
|||
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.WelcomeSlide;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
@ -40,11 +41,6 @@ public class IntroActivity extends IntroBaseActivity {
|
|||
addSlide(DoneSlide.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.NO_ACTION_BAR);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, @NonNull Class<? extends SlideFragment> newSlide) {
|
||||
// hide the keyboard before every slide change
|
||||
|
@ -108,8 +104,17 @@ public class IntroActivity extends IntroBaseActivity {
|
|||
return;
|
||||
}
|
||||
} else {
|
||||
VaultFile vaultFile;
|
||||
try {
|
||||
_vaultManager.load(creds);
|
||||
vaultFile = VaultRepository.readVaultFile(this);
|
||||
} catch (VaultRepositoryException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_vaultManager.loadFrom(vaultFile, creds);
|
||||
} catch (VaultRepositoryException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e);
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder;
|
||||
import com.mikepenz.aboutlibraries.ui.LibsActivity;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import dagger.hilt.InstallIn;
|
||||
import dagger.hilt.android.EarlyEntryPoint;
|
||||
import dagger.hilt.android.EarlyEntryPoints;
|
||||
import dagger.hilt.components.SingletonComponent;
|
||||
|
||||
public class LicensesActivity extends LibsActivity {
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
LibsBuilder builder = new LibsBuilder()
|
||||
.withSearchEnabled(true)
|
||||
.withAboutMinimalDesign(true)
|
||||
.withActivityTitle(getString(R.string.title_activity_licenses));
|
||||
setIntent(builder.intent(this));
|
||||
|
||||
Preferences _prefs = EarlyEntryPoints.get(getApplicationContext(), PrefEntryPoint.class).getPreferences();
|
||||
ThemeHelper themeHelper = new ThemeHelper(this, _prefs);
|
||||
themeHelper.setTheme(ThemeMap.DEFAULT);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@EarlyEntryPoint
|
||||
@InstallIn(SingletonComponent.class)
|
||||
public interface PrefEntryPoint {
|
||||
Preferences getPreferences();
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -10,12 +10,15 @@ import androidx.preference.Preference;
|
|||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.fragments.preferences.AppearancePreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.fragments.preferences.MainPreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
|
||||
public class PreferencesActivity extends AegisActivity implements
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
private Fragment _fragment;
|
||||
private CharSequence _prefTitle;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -25,6 +28,7 @@ public class PreferencesActivity extends AegisActivity implements
|
|||
}
|
||||
setContentView(R.layout.activity_preferences);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
getSupportFragmentManager()
|
||||
.registerFragmentLifecycleCallbacks(new FragmentResumeListener(), true);
|
||||
|
||||
|
@ -48,27 +52,16 @@ public class PreferencesActivity extends AegisActivity implements
|
|||
}
|
||||
} else {
|
||||
_fragment = getSupportFragmentManager().findFragmentById(R.id.content);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle inState) {
|
||||
if (_fragment instanceof PreferencesFragment) {
|
||||
// pass the stored result intent back to the fragment
|
||||
if (inState.containsKey("result")) {
|
||||
((PreferencesFragment) _fragment).setResult(inState.getParcelable("result"));
|
||||
_prefTitle = savedInstanceState.getCharSequence("prefTitle");
|
||||
if (_prefTitle != null) {
|
||||
setTitle(_prefTitle);
|
||||
}
|
||||
}
|
||||
super.onRestoreInstanceState(inState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
if (_fragment instanceof PreferencesFragment) {
|
||||
// save the result intent of the fragment
|
||||
// this is done so we don't lose anything if the fragment calls recreate on this activity
|
||||
outState.putParcelable("result", ((PreferencesFragment) _fragment).getResult());
|
||||
}
|
||||
outState.putCharSequence("prefTitle", _prefTitle);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
|
@ -90,7 +83,8 @@ public class PreferencesActivity extends AegisActivity implements
|
|||
_fragment.setTargetFragment(caller, 0);
|
||||
showFragment(_fragment);
|
||||
|
||||
setTitle(pref.getTitle());
|
||||
_prefTitle = pref.getTitle();
|
||||
setTitle(_prefTitle);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -121,6 +115,9 @@ public class PreferencesActivity extends AegisActivity implements
|
|||
public void onFragmentStarted(@NonNull FragmentManager fm, @NonNull Fragment f) {
|
||||
if (f instanceof MainPreferencesFragment) {
|
||||
setTitle(R.string.action_settings);
|
||||
} else if (f instanceof AppearancePreferencesFragment) {
|
||||
_prefTitle = getString(R.string.pref_section_appearance_title);
|
||||
setTitle(_prefTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,11 @@ import androidx.camera.view.PreviewView;
|
|||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.zxing.Result;
|
||||
|
@ -57,6 +57,7 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
|||
}
|
||||
setContentView(R.layout.activity_scanner);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
|
||||
_entries = new ArrayList<>();
|
||||
_lenses = new ArrayList<>();
|
||||
|
@ -95,11 +96,6 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
|||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
_menu = menu;
|
||||
|
@ -142,10 +138,10 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
|||
if (dual) {
|
||||
switch (_currentLens) {
|
||||
case CameraSelector.LENS_FACING_BACK:
|
||||
item.setIcon(R.drawable.ic_camera_front_24dp);
|
||||
item.setIcon(R.drawable.ic_outline_camera_front_24);
|
||||
break;
|
||||
case CameraSelector.LENS_FACING_FRONT:
|
||||
item.setIcon(R.drawable.ic_camera_rear_24dp);
|
||||
item.setIcon(R.drawable.ic_outline_camera_rear_24);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,15 +10,17 @@ import android.graphics.Color;
|
|||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.PersistableBundle;
|
||||
import android.util.TypedValue;
|
||||
import android.provider.Settings;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
|
@ -27,6 +29,9 @@ import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
|||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.otp.Transferable;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.imageview.ShapeableImageView;
|
||||
import com.google.zxing.WriterException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -34,7 +39,7 @@ import java.util.List;
|
|||
|
||||
public class TransferEntriesActivity extends AegisActivity {
|
||||
private List<Transferable> _authInfos;
|
||||
private ImageView _qrImage;
|
||||
private ShapeableImageView _qrImage;
|
||||
private TextView _description;
|
||||
private TextView _issuer;
|
||||
private TextView _accountName;
|
||||
|
@ -43,6 +48,8 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
private Button _previousButton;
|
||||
private Button _copyButton;
|
||||
private int _currentEntryCount = 1;
|
||||
private float _deviceBrightness;
|
||||
private boolean _isMaxBrightnessSet = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -52,6 +59,7 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
}
|
||||
setContentView(R.layout.activity_share_entry);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
|
||||
_qrImage = findViewById(R.id.ivQrCode);
|
||||
_description = findViewById(R.id.tvDescription);
|
||||
|
@ -62,7 +70,7 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
_previousButton = findViewById(R.id.btnPrevious);
|
||||
_copyButton = findViewById(R.id.btnCopyClipboard);
|
||||
|
||||
if (getSupportActionBar() != null){
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
@ -88,7 +96,7 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
});
|
||||
|
||||
_previousButton.setOnClickListener(v -> {
|
||||
if (_currentEntryCount > 1 ) {
|
||||
if (_currentEntryCount > 1) {
|
||||
_nextButton.setText(R.string.next);
|
||||
_currentEntryCount--;
|
||||
generateQR();
|
||||
|
@ -116,14 +124,59 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
if (clipboard != null) {
|
||||
clipboard.setPrimaryClip(clip);
|
||||
}
|
||||
Toast.makeText(this,R.string.uri_copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, R.string.uri_copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
||||
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_copy_uri_to_clipboard, e);
|
||||
}
|
||||
});
|
||||
|
||||
generateQR();
|
||||
// Calculate sensible dimensions for the QR code depending on whether we're in landscape
|
||||
_qrImage.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
ConstraintLayout layout = findViewById(R.id.layoutShareEntry);
|
||||
if (layout.getWidth() > layout.getHeight()) {
|
||||
int squareSize = (int) (0.5 * layout.getHeight());
|
||||
ViewGroup.LayoutParams params = _qrImage.getLayoutParams();
|
||||
params.width = squareSize;
|
||||
params.height = squareSize;
|
||||
_qrImage.setLayoutParams(params);
|
||||
}
|
||||
|
||||
generateQR();
|
||||
|
||||
_qrImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});
|
||||
|
||||
_deviceBrightness = getSystemBrightness();
|
||||
_qrImage.setOnClickListener(v -> {
|
||||
if (!_isMaxBrightnessSet) {
|
||||
setBrightness(1f);
|
||||
_isMaxBrightnessSet = true;
|
||||
} else {
|
||||
setBrightness(_deviceBrightness);
|
||||
_isMaxBrightnessSet = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private float getSystemBrightness() {
|
||||
int brightness = 0;
|
||||
try {
|
||||
brightness = Settings.System.getInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS);
|
||||
} catch (Settings.SettingNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return brightness / 255f;
|
||||
}
|
||||
|
||||
private void setBrightness(float brightnessAmount) {
|
||||
WindowManager.LayoutParams attrs = getWindow().getAttributes();
|
||||
attrs.screenBrightness = brightnessAmount;
|
||||
getWindow().setAttributes(attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -151,16 +204,13 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
|
||||
_entriesCount.setText(getResources().getQuantityString(R.plurals.qr_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
|
||||
|
||||
@ColorInt int backgroundColor = Color.WHITE;
|
||||
if (getConfiguredTheme() == Theme.LIGHT) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
getTheme().resolveAttribute(R.attr.background, typedValue, true);
|
||||
backgroundColor = typedValue.data;
|
||||
}
|
||||
int backgroundColor = _themeHelper.getConfiguredTheme() == Theme.LIGHT
|
||||
? MaterialColors.getColor(_qrImage, com.google.android.material.R.attr.colorSurfaceContainer)
|
||||
: Color.WHITE;
|
||||
|
||||
Bitmap bitmap;
|
||||
try {
|
||||
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), 512, 512, backgroundColor);
|
||||
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), _qrImage.getWidth(), _qrImage.getWidth(), backgroundColor);
|
||||
} catch (WriterException | GoogleAuthInfoException e) {
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
|
||||
return;
|
||||
|
|
|
@ -18,19 +18,19 @@ import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
|
|||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||
public class DropdownCheckBoxes<T> extends AppCompatAutoCompleteTextView {
|
||||
private @PluralsRes int _selectedCountPlural = R.plurals.dropdown_checkboxes_default_count;
|
||||
|
||||
private boolean _allowFiltering = false;
|
||||
|
||||
private final List<String> _items = new ArrayList<>();
|
||||
private List<String> _visibleItems = new ArrayList<>();
|
||||
private final Set<String> _checkedItems = new TreeSet<>();
|
||||
private final List<T> _items = new ArrayList<>();
|
||||
private List<T> _visibleItems = new ArrayList<>();
|
||||
private final Set<T> _checkedItems = new HashSet<>();
|
||||
|
||||
private CheckboxAdapter _adapter;
|
||||
|
||||
|
@ -70,7 +70,15 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
|||
}
|
||||
}
|
||||
|
||||
public void addItems(List<String> items, boolean startChecked) {
|
||||
/**
|
||||
* Add parameterized items to be displayed as a checkbox in the dropdown view
|
||||
* the label for the checkbox is determined by the toString() method of the items
|
||||
* you add.
|
||||
*
|
||||
* @param items a list of the items you want to show in the dropdown
|
||||
* @param startChecked whether the checkbox should be checked initially
|
||||
*/
|
||||
public void addItems(List<T> items, boolean startChecked) {
|
||||
_items.addAll(items);
|
||||
_visibleItems.addAll(items);
|
||||
|
||||
|
@ -97,7 +105,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
|||
_selectedCountPlural = resId;
|
||||
}
|
||||
|
||||
public Set<String> getCheckedItems() {
|
||||
public Set<T> getCheckedItems() {
|
||||
return _checkedItems;
|
||||
}
|
||||
|
||||
|
@ -109,7 +117,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getItem(int i) {
|
||||
public T getItem(int i) {
|
||||
return _visibleItems.get(i);
|
||||
}
|
||||
|
||||
|
@ -124,19 +132,18 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
|||
convertView = LayoutInflater.from(getContext()).inflate(R.layout.dropdown_checkbox, viewGroup, false);
|
||||
}
|
||||
|
||||
String item = _visibleItems.get(i);
|
||||
T item = _visibleItems.get(i);
|
||||
|
||||
CheckBox checkBox = convertView.findViewById(R.id.checkbox_in_dropdown);
|
||||
checkBox.setText(item);
|
||||
checkBox.setText(item.toString());
|
||||
checkBox.setTag(item);
|
||||
checkBox.setChecked(_checkedItems.contains(item));
|
||||
|
||||
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
String label = buttonView.getText().toString();
|
||||
|
||||
if (isChecked) {
|
||||
_checkedItems.add(label);
|
||||
_checkedItems.add((T) buttonView.getTag());
|
||||
} else {
|
||||
_checkedItems.remove(label);
|
||||
_checkedItems.remove((T) buttonView.getTag());
|
||||
}
|
||||
|
||||
updateCheckedItemsCountText();
|
||||
|
@ -153,9 +160,9 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
|||
FilterResults results = new FilterResults();
|
||||
results.values = (query == null || query.toString().isEmpty())
|
||||
? _items
|
||||
: _items.stream().filter(str -> {
|
||||
: _items.stream().filter(item -> {
|
||||
String q = query.toString().toLowerCase();
|
||||
String strLower = str.toLowerCase();
|
||||
String strLower = item.toString().toLowerCase();
|
||||
|
||||
return strLower.contains(q);
|
||||
})
|
||||
|
@ -166,7 +173,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
|||
|
||||
@Override
|
||||
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
|
||||
_visibleItems = (List<String>) filterResults.values;
|
||||
_visibleItems = (List<T>) filterResults.values;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.content.Context;
|
|||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class ChangelogDialog extends SimpleWebViewDialog {
|
||||
private ChangelogDialog() {
|
||||
public ChangelogDialog() {
|
||||
super(R.string.changelog);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@ import android.content.ClipData;
|
|||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextWatcher;
|
||||
|
@ -21,13 +19,17 @@ import android.widget.EditText;
|
|||
import android.widget.ListView;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.ComponentActivity;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
|
@ -39,10 +41,9 @@ import com.beemdevelopment.aegis.vault.VaultEntry;
|
|||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.nulabinc.zxcvbn.Strength;
|
||||
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -86,9 +87,10 @@ public class Dialogs {
|
|||
}
|
||||
textMessage.setText(message);
|
||||
|
||||
showSecureDialog(new AlertDialog.Builder(context)
|
||||
showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(title)
|
||||
.setView(view)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.yes, onDelete)
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
|
@ -107,16 +109,16 @@ public class Dialogs {
|
|||
}
|
||||
|
||||
public static void showDiscardDialog(Context context, DialogInterface.OnClickListener onSave, DialogInterface.OnClickListener onDiscard) {
|
||||
showSecureDialog(new AlertDialog.Builder(context)
|
||||
showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(context.getString(R.string.discard_changes))
|
||||
.setMessage(context.getString(R.string.discard_changes_description))
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(R.string.save, onSave)
|
||||
.setNegativeButton(R.string.discard, onDiscard)
|
||||
.create());
|
||||
}
|
||||
|
||||
public static void showSetPasswordDialog(ComponentActivity activity, PasswordSlotListener listener) {
|
||||
Zxcvbn zxcvbn = new Zxcvbn();
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
|
||||
EditText textPassword = view.findViewById(R.id.text_password);
|
||||
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
|
||||
|
@ -124,6 +126,8 @@ public class Dialogs {
|
|||
TextView textPasswordStrength = view.findViewById(R.id.text_password_strength);
|
||||
TextInputLayout textPasswordWrapper = view.findViewById(R.id.text_password_wrapper);
|
||||
CheckBox switchToggleVisibility = view.findViewById(R.id.check_toggle_visibility);
|
||||
PasswordStrengthHelper passStrength = new PasswordStrengthHelper(
|
||||
textPassword, barPasswordStrength, textPasswordStrength, textPasswordWrapper);
|
||||
|
||||
switchToggleVisibility.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
|
@ -137,7 +141,7 @@ public class Dialogs {
|
|||
}
|
||||
});
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(activity)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.set_password)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
|
@ -179,13 +183,7 @@ public class Dialogs {
|
|||
TextWatcher watcher = new SimpleTextWatcher(text -> {
|
||||
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
|
||||
buttonOK.get().setEnabled(equal);
|
||||
|
||||
Strength strength = zxcvbn.measure(textPassword.getText());
|
||||
barPasswordStrength.setProgress(strength.getScore());
|
||||
barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(PasswordStrengthHelper.getColor(strength.getScore()))));
|
||||
textPasswordStrength.setText((textPassword.getText().length() != 0) ? PasswordStrengthHelper.getString(strength.getScore(), activity) : "");
|
||||
textPasswordWrapper.setError(strength.getFeedback().getWarning());
|
||||
strength.wipe();
|
||||
passStrength.measure(activity);
|
||||
});
|
||||
textPassword.addTextChangedListener(watcher);
|
||||
textPasswordConfirm.addTextChangedListener(watcher);
|
||||
|
@ -193,10 +191,13 @@ public class Dialogs {
|
|||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener, boolean isSecret) {
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener, boolean isSecret,@Nullable String hint) {
|
||||
final AtomicReference<Button> buttonOK = new AtomicReference<>();
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
|
||||
TextInputEditText input = view.findViewById(R.id.text_input);
|
||||
if(hint != null) {
|
||||
input.setText(hint);
|
||||
}
|
||||
input.addTextChangedListener(new SimpleTextWatcher(text -> {
|
||||
if (buttonOK.get() != null) {
|
||||
buttonOK.get().setEnabled(!text.toString().isEmpty());
|
||||
|
@ -208,7 +209,7 @@ public class Dialogs {
|
|||
}
|
||||
inputLayout.setHint(hintId);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(titleId)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null);
|
||||
|
@ -238,12 +239,16 @@ public class Dialogs {
|
|||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, String text) {
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, null, false, text);
|
||||
}
|
||||
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret);
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret, null);
|
||||
}
|
||||
|
||||
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
|
||||
showTextInputDialog(context, titleId, hintId, listener, false);
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, null, false, null);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, TextInputListener listener) {
|
||||
|
@ -251,19 +256,19 @@ public class Dialogs {
|
|||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
|
||||
showTextInputDialog(context, R.string.set_password, 0, R.string.password, listener, cancelListener, true);
|
||||
showTextInputDialog(context, R.string.set_password, 0, R.string.password, listener, cancelListener, true, null);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
|
||||
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, null, true);
|
||||
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, null, true, null);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
|
||||
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, cancelListener, true);
|
||||
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, cancelListener, true, null);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
|
||||
showTextInputDialog(context, titleId, messageId, R.string.password, listener, cancelListener, true);
|
||||
showTextInputDialog(context, titleId, messageId, R.string.password, listener, cancelListener, true, null);
|
||||
}
|
||||
|
||||
public static void showCheckboxDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int checkboxMessageId, CheckboxInputListener listener) {
|
||||
|
@ -271,7 +276,7 @@ public class Dialogs {
|
|||
CheckBox checkBox = view.findViewById(R.id.checkbox);
|
||||
checkBox.setText(checkboxMessageId);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(titleId)
|
||||
.setView(view)
|
||||
.setNegativeButton(R.string.no, (dialog1, which) ->
|
||||
|
@ -305,7 +310,7 @@ public class Dialogs {
|
|||
numberPicker.setValue(currentValue);
|
||||
numberPicker.setWrapSelectorWheel(true);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.set_number)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
|
||||
|
@ -316,25 +321,32 @@ public class Dialogs {
|
|||
}
|
||||
|
||||
public static void showBackupVersionsPickerDialog(Context context, int currentVersionCount, NumberInputListener listener) {
|
||||
final int max = 30;
|
||||
String[] numbers = new String[max / 5];
|
||||
for (int i = 0; i < numbers.length; i++) {
|
||||
numbers[i] = Integer.toString(i * 5 + 5);
|
||||
String infinite = context.getString(R.string.pref_backups_versions_infinite);
|
||||
String[] values = {"5", "10", "15", "20", "25", "30", infinite};
|
||||
int[] numbers = {5, 10, 15, 20, 25, 30, Preferences.BACKUPS_VERSIONS_INFINITE};
|
||||
int selectedIndex;
|
||||
if (currentVersionCount == Preferences.BACKUPS_VERSIONS_INFINITE) {
|
||||
selectedIndex = numbers.length - 1;
|
||||
} else {
|
||||
selectedIndex = currentVersionCount / 5 - 1;
|
||||
}
|
||||
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_number_picker, null);
|
||||
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
|
||||
numberPicker.setDisplayedValues(numbers);
|
||||
numberPicker.setMaxValue(numbers.length - 1);
|
||||
numberPicker.setDisplayedValues(values);
|
||||
numberPicker.setMaxValue(values.length - 1);
|
||||
numberPicker.setMinValue(0);
|
||||
numberPicker.setValue(currentVersionCount / 5 - 1);
|
||||
numberPicker.setValue(selectedIndex);
|
||||
numberPicker.setWrapSelectorWheel(false);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.set_number)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
|
||||
listener.onNumberInputResult(numberPicker.getValue()))
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
int index = numberPicker.getValue();
|
||||
int number = numbers[index];
|
||||
listener.onNumberInputResult(number);
|
||||
})
|
||||
.create();
|
||||
|
||||
showSecureDialog(dialog);
|
||||
|
@ -371,10 +383,11 @@ public class Dialogs {
|
|||
TextView textMessage = view.findViewById(R.id.error_message);
|
||||
textMessage.setText(message);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog1, which);
|
||||
|
@ -406,38 +419,40 @@ public class Dialogs {
|
|||
|
||||
public static void showBackupErrorDialog(Context context, Preferences.BackupResult backupRes, DialogInterface.OnClickListener listener) {
|
||||
String system = context.getString(backupRes.isBuiltIn() ? R.string.backup_system_builtin : R.string.backup_system_android);
|
||||
String message = context.getString(R.string.backup_error_dialog_details, system, backupRes.getElapsedSince(context));
|
||||
@StringRes int details = backupRes.isPermissionError() ? R.string.backup_permission_error_dialog_details : R.string.backup_error_dialog_details;
|
||||
String message = context.getString(details, system, backupRes.getElapsedSince(context));
|
||||
Dialogs.showErrorDialog(context, message, backupRes.getError(), listener);
|
||||
}
|
||||
|
||||
public static void showMultiMessageDialog(
|
||||
public static void showMultiErrorDialog(
|
||||
Context context, @StringRes int title, String message, List<CharSequence> messages, DialogInterface.OnClickListener listener) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog, which);
|
||||
}
|
||||
})
|
||||
.setNeutralButton(context.getString(R.string.details), (dialog, which) -> {
|
||||
showDetailedMultiMessageDialog(context, title, messages, listener);
|
||||
showDetailedMultiErrorDialog(context, title, messages, listener);
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
public static <T extends Throwable> void showMultiErrorDialog(
|
||||
public static <T extends Throwable> void showMultiExceptionDialog(
|
||||
Context context, @StringRes int title, String message, List<T> errors, DialogInterface.OnClickListener listener) {
|
||||
List<CharSequence> messages = new ArrayList<>();
|
||||
for (Throwable e : errors) {
|
||||
messages.add(e.toString());
|
||||
}
|
||||
|
||||
showMultiMessageDialog(context, title, message, messages, listener);
|
||||
showMultiErrorDialog(context, title, message, messages, listener);
|
||||
}
|
||||
|
||||
private static void showDetailedMultiMessageDialog(
|
||||
private static void showDetailedMultiErrorDialog(
|
||||
Context context, @StringRes int title, List<CharSequence> messages, DialogInterface.OnClickListener listener) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
for (CharSequence message : messages) {
|
||||
|
@ -445,10 +460,11 @@ public class Dialogs {
|
|||
builder.append("\n\n");
|
||||
}
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(title)
|
||||
.setMessage(builder)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog1, which);
|
||||
|
@ -475,10 +491,11 @@ public class Dialogs {
|
|||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_time_sync, null);
|
||||
CheckBox checkBox = view.findViewById(R.id.check_warning_disable);
|
||||
|
||||
showSecureDialog(new AlertDialog.Builder(context)
|
||||
showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.time_sync_warning_title)
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
if (checkBox.isChecked()) {
|
||||
prefs.setIsTimeSyncWarningEnabled(false);
|
||||
|
@ -513,7 +530,7 @@ public class Dialogs {
|
|||
setImporterHelpText(helpText, importers.get(position), isDirect);
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.choose_application)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
|
@ -534,12 +551,13 @@ public class Dialogs {
|
|||
errorDetails.append("\n\n");
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.partial_google_auth_import)
|
||||
.setMessage(context.getString(R.string.partial_google_auth_import_warning, missingIndexesAsString))
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(context.getString(R.string.import_partial_export_anyway, entries), (dialog, which) -> {
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(context.getResources().getQuantityString(R.plurals.import_partial_export_anyway, entries, entries), (dialog, which) -> {
|
||||
dismissHandler.onClick(dialog, which);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
|
@ -562,6 +580,54 @@ public class Dialogs {
|
|||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showBackupsVersioningStrategy(Context context, BackupsVersioningStrategy currentStrategy, BackupsVersioningStrategyListener listener) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_backups_versioning_strategy, null);
|
||||
RadioGroup radioGroup = view.findViewById(R.id.radio_group);
|
||||
RadioButton keepXVersionsButton = view.findViewById(R.id.keep_x_versions_button);
|
||||
RadioButton singleBackupButton = view.findViewById(R.id.single_backup_button);
|
||||
TextView warningText = view.findViewById(R.id.warning_text);
|
||||
CheckBox riskAccept = view.findViewById(R.id.risk_accept);
|
||||
final AtomicReference<Button> positiveButtonRef = new AtomicReference<>();
|
||||
radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
Button positiveButton = positiveButtonRef.get();
|
||||
if (positiveButton != null) {
|
||||
positiveButton.setEnabled(checkedId == keepXVersionsButton.getId());
|
||||
}
|
||||
int visibility = checkedId == singleBackupButton.getId() ? View.VISIBLE : View.GONE;
|
||||
warningText.setVisibility(visibility);
|
||||
riskAccept.setVisibility(visibility);
|
||||
});
|
||||
riskAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
Button positiveButton = positiveButtonRef.get();
|
||||
if (positiveButton != null) {
|
||||
positiveButton.setEnabled(isChecked);
|
||||
}
|
||||
});
|
||||
AlertDialog alertDialog = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.pref_backups_versioning_strategy_dialog_title)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int checkedId = radioGroup.getCheckedRadioButtonId();
|
||||
if (checkedId == keepXVersionsButton.getId()) {
|
||||
listener.onStrategySelectionResult(BackupsVersioningStrategy.MULTIPLE_BACKUPS);
|
||||
} else if (checkedId == singleBackupButton.getId()) {
|
||||
listener.onStrategySelectionResult(BackupsVersioningStrategy.SINGLE_BACKUP);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
alertDialog.setOnShowListener(dialog -> {
|
||||
Button positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
positiveButtonRef.set(positiveButton);
|
||||
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
radioGroup.check(keepXVersionsButton.getId());
|
||||
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
radioGroup.check(singleBackupButton.getId());
|
||||
}
|
||||
});
|
||||
showSecureDialog(alertDialog);
|
||||
}
|
||||
|
||||
private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) {
|
||||
if (isDirect) {
|
||||
view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName()));
|
||||
|
@ -590,4 +656,8 @@ public class Dialogs {
|
|||
public interface ImporterListener {
|
||||
void onImporterSelectionResult(DatabaseImporter.Definition definition);
|
||||
}
|
||||
|
||||
public interface BackupsVersioningStrategyListener {
|
||||
void onStrategySelectionResult(BackupsVersioningStrategy strategy);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,14 +18,13 @@ import androidx.recyclerview.widget.GridLayoutManager;
|
|||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||
import com.beemdevelopment.aegis.ui.views.IconRecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.ListPreloader;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
@ -40,7 +39,7 @@ public class IconPickerDialog {
|
|||
|
||||
}
|
||||
|
||||
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, IconAdapter.Listener listener) {
|
||||
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, boolean showAddCustom, IconAdapter.Listener listener) {
|
||||
View view = LayoutInflater.from(activity).inflate(R.layout.dialog_icon_picker, null);
|
||||
TextView textIconPack = view.findViewById(R.id.text_icon_pack);
|
||||
|
||||
|
@ -76,12 +75,9 @@ public class IconPickerDialog {
|
|||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
|
||||
return Glide.with(dialog.getContext())
|
||||
.asDrawable()
|
||||
.load(icon.getFile())
|
||||
.set(IconLoader.ICON_TYPE, icon.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false);
|
||||
RequestBuilder<Drawable> rb = Glide.with(dialog.getContext())
|
||||
.load(icon.getFile());
|
||||
return GlideHelper.setCommonOptions(rb, icon.getIconType());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,7 +124,7 @@ public class IconPickerDialog {
|
|||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.addOnScrollListener(preloader);
|
||||
adapter.loadIcons(iconPacks.get(0));
|
||||
adapter.loadIcons(iconPacks.get(0), showAddCustom);
|
||||
textIconPack.setText(iconPacks.get(0).getName());
|
||||
|
||||
view.findViewById(R.id.btn_icon_pack).setOnClickListener(v -> {
|
||||
|
@ -139,7 +135,7 @@ public class IconPickerDialog {
|
|||
PopupMenu popupMenu = new PopupMenu(activity, v);
|
||||
popupMenu.setOnMenuItemClickListener(item -> {
|
||||
IconPack pack = iconPacks.get(iconPackNames.indexOf(item.getTitle().toString()));
|
||||
adapter.loadIcons(pack);
|
||||
adapter.loadIcons(pack, showAddCustom);
|
||||
|
||||
String query = iconSearch.getText().toString();
|
||||
if (!query.isEmpty()) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.content.Context;
|
|||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class LicenseDialog extends SimpleWebViewDialog {
|
||||
private LicenseDialog() {
|
||||
public LicenseDialog() {
|
||||
super(R.string.license);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@ import androidx.fragment.app.DialogFragment;
|
|||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.common.io.CharStreams;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -44,14 +45,14 @@ public abstract class SimpleWebViewDialog extends DialogFragment {
|
|||
view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_web_view, null);
|
||||
} catch (InflateException e) {
|
||||
e.printStackTrace();
|
||||
return new AlertDialog.Builder(requireContext())
|
||||
return new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(android.R.string.dialog_alert_title)
|
||||
.setMessage(getString(R.string.webview_error))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(requireContext())
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(_title)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
|
@ -69,12 +70,21 @@ public abstract class SimpleWebViewDialog extends DialogFragment {
|
|||
}
|
||||
|
||||
protected String getBackgroundColor() {
|
||||
int backgroundColorResource = _theme == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
return colorToCSS(ThemeHelper.getThemeColor(backgroundColorResource, requireContext().getTheme()));
|
||||
int color = MaterialColors.getColor(
|
||||
requireContext(),
|
||||
com.google.android.material.R.attr.colorSurfaceContainerHigh,
|
||||
getClass().getCanonicalName()
|
||||
);
|
||||
return colorToCSS(color);
|
||||
}
|
||||
|
||||
protected String getTextColor() {
|
||||
return colorToCSS(0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, requireContext().getTheme()));
|
||||
int color = MaterialColors.getColor(
|
||||
requireContext(),
|
||||
com.google.android.material.R.attr.colorOnSurface,
|
||||
getClass().getCanonicalName()
|
||||
);
|
||||
return colorToCSS(0xFFFFFF & color);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
|
|
|
@ -8,11 +8,17 @@ import androidx.appcompat.app.AlertDialog;
|
|||
import androidx.preference.Preference;
|
||||
|
||||
import com.beemdevelopment.aegis.AccountNamePosition;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.ui.GroupManagerActivity;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.google.android.material.color.DynamicColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
private Preference _groupsPreference;
|
||||
|
@ -21,7 +27,6 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
|||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
addPreferencesFromResource(R.xml.preferences_appearance);
|
||||
|
||||
_groupsPreference = requirePreference("pref_groups");
|
||||
|
@ -33,7 +38,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
|||
|
||||
_resetUsageCountPreference = requirePreference("pref_reset_usage_count");
|
||||
_resetUsageCountPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.preference_reset_usage_count)
|
||||
.setMessage(R.string.preference_reset_usage_count_dialog)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> _prefs.clearUsageCount())
|
||||
|
@ -48,7 +53,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
|||
darkModePreference.setOnPreferenceClickListener(preference -> {
|
||||
int currentTheme1 = _prefs.getCurrentTheme().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.choose_theme)
|
||||
.setSingleChoiceItems(R.array.theme_titles, currentTheme1, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
|
@ -56,7 +61,6 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
|||
|
||||
dialog.dismiss();
|
||||
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
requireActivity().recreate();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
@ -65,11 +69,34 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
|||
return true;
|
||||
});
|
||||
|
||||
Preference dynamicColorsPreference = requirePreference("pref_dynamic_colors");
|
||||
dynamicColorsPreference.setEnabled(DynamicColors.isDynamicColorAvailable());
|
||||
dynamicColorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
requireActivity().recreate();
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference langPreference = requirePreference("pref_lang");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
requireActivity().recreate();
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
||||
String[] langs = getResources().getStringArray(R.array.pref_lang_values);
|
||||
String[] langNames = getResources().getStringArray(R.array.pref_lang_entries);
|
||||
List<String> langList = Arrays.asList(langs);
|
||||
int curLangIndex = langList.contains(_prefs.getLanguage()) ? langList.indexOf(_prefs.getLanguage()) : 0;
|
||||
langPreference.setSummary(langNames[curLangIndex]);
|
||||
langPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.pref_lang_title)
|
||||
.setSingleChoiceItems(langNames, curLangIndex, (dialog, which) -> {
|
||||
int newLangIndex = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
_prefs.setLanguage(langs[newLangIndex]);
|
||||
langPreference.setSummary(langNames[newLangIndex]);
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
requireActivity().recreate();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create());
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
|
@ -83,14 +110,13 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
|||
viewModePreference.setOnPreferenceClickListener(preference -> {
|
||||
int currentViewMode1 = _prefs.getCurrentViewMode().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.choose_view_mode)
|
||||
.setSingleChoiceItems(R.array.view_mode_titles, currentViewMode1, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
_prefs.setCurrentViewMode(ViewMode.fromInteger(i));
|
||||
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i]));
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
overrideAccountNamePosition(ViewMode.fromInteger(i) == ViewMode.TILES);
|
||||
refreshAccountNamePositionText();
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
@ -99,15 +125,27 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
|||
return true;
|
||||
});
|
||||
|
||||
Preference codeDigitGroupingPreference = requirePreference("pref_code_group_size_string");
|
||||
codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
Preference showExpirationStatePreference = requirePreference("pref_expiration_state");
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
showExpirationStatePreference.setSummary(getString(R.string.pref_expiration_state_fallback));
|
||||
}
|
||||
|
||||
Preference onlyShowNecessaryAccountNames = requirePreference("pref_shared_issuer_account_name");
|
||||
onlyShowNecessaryAccountNames.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
String[] codeGroupings = getResources().getStringArray(R.array.pref_code_groupings_values);
|
||||
String[] codeGroupingNames = getResources().getStringArray(R.array.pref_code_groupings);
|
||||
Preference codeDigitGroupingPreference = requirePreference("pref_code_group_size_string");
|
||||
codeDigitGroupingPreference.setOnPreferenceClickListener(preference -> {
|
||||
int currentCodeGroupingIndex = Arrays.asList(codeGroupings).indexOf(_prefs.getCodeGroupSize().name());
|
||||
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.pref_code_group_size_title)
|
||||
.setSingleChoiceItems(codeGroupingNames, currentCodeGroupingIndex, (dialog, which) -> {
|
||||
int newCodeGroupingIndex = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
_prefs.setCodeGroupSize(Preferences.CodeGrouping.valueOf(codeGroupings[newCodeGroupingIndex]));
|
||||
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create());
|
||||
return true;
|
||||
});
|
||||
|
||||
|
@ -117,13 +155,13 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
|||
_currentAccountNamePositionPreference.setOnPreferenceClickListener(preference -> {
|
||||
int currentAccountNamePosition1 = _prefs.getAccountNamePosition().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.choose_account_name_position))
|
||||
.setSingleChoiceItems(R.array.account_name_position_titles, currentAccountNamePosition1, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
_prefs.setAccountNamePosition(AccountNamePosition.fromInteger(i));
|
||||
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[i]));
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
refreshAccountNamePositionText();
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
@ -132,21 +170,15 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
|||
return true;
|
||||
});
|
||||
|
||||
Preference showIconsPreference = requirePreference("pref_show_icons");
|
||||
showIconsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
overrideAccountNamePosition(_prefs.getCurrentViewMode() == ViewMode.TILES);
|
||||
refreshAccountNamePositionText();
|
||||
}
|
||||
|
||||
private void overrideAccountNamePosition(boolean override) {
|
||||
private void refreshAccountNamePositionText() {
|
||||
boolean override = (_prefs.getCurrentViewMode() == ViewMode.TILES && _prefs.getAccountNamePosition() == AccountNamePosition.END);
|
||||
|
||||
if (override) {
|
||||
_currentAccountNamePositionPreference.setEnabled(false);
|
||||
_currentAccountNamePositionPreference.setSummary(getString(R.string.pref_account_name_position_summary_override));
|
||||
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s. %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[_prefs.getAccountNamePosition().ordinal()], getString(R.string.pref_account_name_position_summary_override)));
|
||||
} else {
|
||||
_currentAccountNamePositionPreference.setEnabled(true);
|
||||
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[_prefs.getAccountNamePosition().ordinal()]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
package com.beemdevelopment.aegis.ui.fragments.preferences;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.database.AuditLogEntry;
|
||||
import com.beemdevelopment.aegis.database.AuditLogRepository;
|
||||
import com.beemdevelopment.aegis.helpers.MetricsHelper;
|
||||
import com.beemdevelopment.aegis.ui.models.AuditLogEntryModel;
|
||||
import com.beemdevelopment.aegis.ui.views.AuditLogAdapter;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class AuditLogPreferencesFragment extends Fragment {
|
||||
|
||||
@Inject
|
||||
AuditLogRepository _auditLogRepository;
|
||||
|
||||
private AuditLogAdapter _adapter;
|
||||
|
||||
private RecyclerView _auditLogRecyclerView;
|
||||
private LinearLayout _noAuditLogsView;
|
||||
|
||||
@Inject
|
||||
VaultManager _vaultManager;
|
||||
|
||||
public AuditLogPreferencesFragment() {
|
||||
super(R.layout.fragment_audit_log);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
|
||||
LiveData<List<AuditLogEntry>> entries = _auditLogRepository.getAllAuditLogEntries();
|
||||
|
||||
_adapter = new AuditLogAdapter();
|
||||
_noAuditLogsView = view.findViewById(R.id.vEmptyList);
|
||||
_auditLogRecyclerView = view.findViewById(R.id.list_audit_log);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
|
||||
_auditLogRecyclerView.addItemDecoration(new SpacesItemDecoration(8));
|
||||
_auditLogRecyclerView.setLayoutManager(layoutManager);
|
||||
_auditLogRecyclerView.setAdapter(_adapter);
|
||||
_auditLogRecyclerView.setNestedScrollingEnabled(false);
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(_auditLogRecyclerView, (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
targetView.setPadding(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
insets.bottom
|
||||
);
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
entries.observe(getViewLifecycleOwner(), entries1 -> {
|
||||
_noAuditLogsView.setVisibility(entries1.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
|
||||
for (AuditLogEntry entry : entries1) {
|
||||
VaultEntry referencedEntry = null;
|
||||
if (entry.getReference() != null) {
|
||||
UUID referencedEntryUUID = UUID.fromString(entry.getReference());
|
||||
if (_vaultManager.getVault().hasEntryByUUID(referencedEntryUUID)) {
|
||||
referencedEntry = _vaultManager.getVault().getEntryByUUID(referencedEntryUUID);
|
||||
}
|
||||
}
|
||||
|
||||
AuditLogEntryModel auditLogEntryModel = new AuditLogEntryModel(entry, referencedEntry);
|
||||
_adapter.addAuditLogEntryModel(auditLogEntryModel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class SpacesItemDecoration extends RecyclerView.ItemDecoration {
|
||||
private final int _space;
|
||||
|
||||
public SpacesItemDecoration(int dpSpace) {
|
||||
_space = MetricsHelper.convertDpToPixels(getContext(), dpSpace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
|
||||
outRect.left = _space;
|
||||
outRect.right = _space;
|
||||
outRect.bottom = _space;
|
||||
|
||||
if (parent.getChildLayoutPosition(view) == 0) {
|
||||
outRect.top = _space;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,19 +11,25 @@ import android.text.style.ForegroundColorSpan;
|
|||
import android.text.style.StyleSpan;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.vault.VaultBackupManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
public class BackupsPreferencesFragment extends PreferencesFragment {
|
||||
private SwitchPreferenceCompat _androidBackupsPreference;
|
||||
private SwitchPreferenceCompat _backupsPreference;
|
||||
private SwitchPreferenceCompat _backupReminderPreference;
|
||||
private Preference _versioningStrategyPreference;
|
||||
private Preference _backupsLocationPreference;
|
||||
private Preference _backupsTriggerPreference;
|
||||
private Preference _backupsVersionsPreference;
|
||||
|
@ -32,6 +38,15 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
private Preference _builtinBackupStatusPreference;
|
||||
private Preference _androidBackupStatusPreference;
|
||||
|
||||
private final ActivityResultLauncher<Intent> backupsResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
Intent data = activityResult.getData();
|
||||
int resultCode = activityResult.getResultCode();
|
||||
if (data != null) {
|
||||
onSelectBackupsLocationResult(resultCode, data);
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
@ -40,7 +55,6 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
addPreferencesFromResource(R.xml.preferences_backups);
|
||||
|
||||
_backupsPasswordWarningPreference = requirePreference("pref_backups_warning_password");
|
||||
|
@ -64,7 +78,13 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
_backupsPreference = requirePreference("pref_backups");
|
||||
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if ((boolean) newValue) {
|
||||
selectBackupsLocation();
|
||||
Dialogs.showBackupsVersioningStrategy(requireContext(), BackupsVersioningStrategy.MULTIPLE_BACKUPS, strategy -> {
|
||||
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
selectBackupsLocation();
|
||||
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
createBackupFile();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_prefs.setIsBackupsEnabled(false);
|
||||
updateBackupPreference();
|
||||
|
@ -89,6 +109,24 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
return false;
|
||||
});
|
||||
|
||||
_versioningStrategyPreference = requirePreference("pref_versioning_strategy");
|
||||
updateBackupsVersioningStrategySummary();
|
||||
_versioningStrategyPreference.setOnPreferenceClickListener(preference -> {
|
||||
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
|
||||
Dialogs.showBackupsVersioningStrategy(requireContext(), currentStrategy, strategy -> {
|
||||
if (strategy == currentStrategy) {
|
||||
return;
|
||||
}
|
||||
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
selectBackupsLocation();
|
||||
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
createBackupFile();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
_androidBackupsPreference = requirePreference("pref_android_backups");
|
||||
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_prefs.setIsAndroidBackupsEnabled((boolean) newValue);
|
||||
|
@ -99,13 +137,15 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
return false;
|
||||
});
|
||||
|
||||
Uri backupLocation = _prefs.getBackupsLocation();
|
||||
_backupsLocationPreference = requirePreference("pref_backups_location");
|
||||
if (backupLocation != null) {
|
||||
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
|
||||
}
|
||||
updateBackupsLocationSummary();
|
||||
_backupsLocationPreference.setOnPreferenceClickListener(preference -> {
|
||||
selectBackupsLocation();
|
||||
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
|
||||
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
selectBackupsLocation();
|
||||
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
createBackupFile();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
|
@ -119,12 +159,11 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
});
|
||||
|
||||
_backupsVersionsPreference = requirePreference("pref_backups_versions");
|
||||
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, _prefs.getBackupsVersionCount(), _prefs.getBackupsVersionCount()));
|
||||
updateBackupsVersionsSummary();
|
||||
_backupsVersionsPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showBackupVersionsPickerDialog(requireContext(), _prefs.getBackupsVersionCount(), number -> {
|
||||
number = number * 5 + 5;
|
||||
_prefs.setBackupsVersionCount(number);
|
||||
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, _prefs.getBackupsVersionCount(), _prefs.getBackupsVersionCount()));
|
||||
updateBackupsVersionsSummary();
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
@ -137,13 +176,6 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (data != null && requestCode == CODE_BACKUPS) {
|
||||
onSelectBackupsLocationResult(resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void onSelectBackupsLocationResult(int resultCode, Intent data) {
|
||||
Uri uri = data.getData();
|
||||
if (resultCode != Activity.RESULT_OK || uri == null) {
|
||||
|
@ -155,9 +187,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
|
||||
_prefs.setBackupsLocation(uri);
|
||||
_prefs.setIsBackupsEnabled(true);
|
||||
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
|
||||
updateBackupPreference();
|
||||
scheduleBackup();
|
||||
updateBackupsVersioningStrategySummary();
|
||||
updateBackupsLocationSummary();
|
||||
}
|
||||
|
||||
private void updateBackupPreference() {
|
||||
|
@ -171,9 +204,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
_backupsPreference.setChecked(backupEnabled);
|
||||
_backupsPreference.setEnabled(encrypted);
|
||||
_backupReminderPreference.setChecked(backupReminderEnabled);
|
||||
_versioningStrategyPreference.setVisible(backupEnabled);
|
||||
_backupsLocationPreference.setVisible(backupEnabled);
|
||||
_backupsTriggerPreference.setVisible(backupEnabled);
|
||||
_backupsVersionsPreference.setVisible(backupEnabled);
|
||||
_backupsVersionsPreference.setVisible(backupEnabled && _prefs.getBackupVersioningStrategy() != BackupsVersioningStrategy.SINGLE_BACKUP);
|
||||
if (backupEnabled) {
|
||||
updateBackupStatus(_builtinBackupStatusPreference, _prefs.getBuiltInBackupResult());
|
||||
}
|
||||
|
@ -191,9 +225,9 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
|
||||
// TODO: Find out why setting the tint of the icon doesn't work
|
||||
if (backupFailed) {
|
||||
pref.setIcon(R.drawable.ic_info_outline_black_24dp);
|
||||
pref.setIcon(R.drawable.ic_outline_error_24);
|
||||
} else if (res != null) {
|
||||
pref.setIcon(R.drawable.ic_check_black_24dp);
|
||||
pref.setIcon(R.drawable.ic_outline_check_24);
|
||||
} else {
|
||||
pref.setIcon(null);
|
||||
}
|
||||
|
@ -201,24 +235,31 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
|
||||
private CharSequence getBackupStatusMessage(@Nullable Preferences.BackupResult res) {
|
||||
String message;
|
||||
int color = R.color.warning_color;
|
||||
int colorAttr = com.google.android.material.R.attr.colorError;
|
||||
if (res == null) {
|
||||
message = getString(R.string.backup_status_none);
|
||||
} else if (res.isSuccessful()) {
|
||||
color = R.color.success_color;
|
||||
colorAttr = R.attr.colorSuccess;
|
||||
message = getString(R.string.backup_status_success, res.getElapsedSince(requireContext()));
|
||||
} else {
|
||||
message = getString(R.string.backup_status_failed, res.getElapsedSince(requireContext()));
|
||||
}
|
||||
|
||||
int color = MaterialColors.getColor(requireContext(), colorAttr, getClass().getCanonicalName());
|
||||
Spannable spannable = new SpannableString(message);
|
||||
spannable.setSpan(new ForegroundColorSpan(getResources().getColor(color)), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
if (color == R.color.warning_color) {
|
||||
spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
spannable.setSpan(new ForegroundColorSpan(color), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
}
|
||||
|
||||
private void createBackupFile() {
|
||||
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType("application/json")
|
||||
.putExtra(Intent.EXTRA_TITLE, VaultBackupManager.FILENAME_SINGLE);
|
||||
_vaultManager.fireIntentLauncher(this, intent, backupsResultLauncher);
|
||||
}
|
||||
|
||||
private void selectBackupsLocation() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
@ -226,7 +267,7 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||
|
||||
_vaultManager.startActivityForResult(this, intent, CODE_BACKUPS);
|
||||
_vaultManager.fireIntentLauncher(this, intent, backupsResultLauncher);
|
||||
}
|
||||
|
||||
private void scheduleBackup() {
|
||||
|
@ -238,4 +279,38 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
Dialogs.showErrorDialog(requireContext(), R.string.backup_error, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBackupsVersioningStrategySummary() {
|
||||
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
|
||||
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_keep_x_versions);
|
||||
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_single_backup);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBackupsLocationSummary() {
|
||||
Uri backupsLocation = _prefs.getBackupsLocation();
|
||||
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
|
||||
String text;
|
||||
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
text = getString(R.string.pref_backups_location_summary);
|
||||
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
text = getString(R.string.pref_backup_location_summary);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
String summary = String.format("%s: %s", text, Uri.decode(backupsLocation.toString()));
|
||||
_backupsLocationPreference.setSummary(summary);
|
||||
}
|
||||
|
||||
private void updateBackupsVersionsSummary() {
|
||||
int count = _prefs.getBackupsVersionCount();
|
||||
if (count == Preferences.BACKUPS_VERSIONS_INFINITE) {
|
||||
_backupsVersionsPreference.setSummary(R.string.pref_backups_versions_infinite_summary);
|
||||
} else {
|
||||
String summary = getResources().getQuantityString(R.plurals.pref_backups_versions_summary, count, count);
|
||||
_backupsVersionsPreference.setSummary(summary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,80 @@
|
|||
package com.beemdevelopment.aegis.ui.fragments.preferences;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.beemdevelopment.aegis.CopyBehavior;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
public class BehaviorPreferencesFragment extends PreferencesFragment {
|
||||
private Preference _entryPausePreference;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
addPreferencesFromResource(R.xml.preferences_behavior);
|
||||
|
||||
Preference currentSearchBehaviorPreference = requirePreference("pref_search_behavior");
|
||||
currentSearchBehaviorPreference.setSummary(getSearchBehaviorSummary());
|
||||
currentSearchBehaviorPreference.setOnPreferenceClickListener((preference) -> {
|
||||
final int[] items = Preferences.SEARCH_BEHAVIOR_SETTINGS;
|
||||
final String[] textItems = getResources().getStringArray(R.array.pref_search_behavior_types);
|
||||
final boolean[] checkedItems = new boolean[items.length];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
checkedItems[i] = _prefs.isSearchBehaviorTypeEnabled(items[i]);
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.pref_search_behavior_prompt)
|
||||
.setMultiChoiceItems(textItems, checkedItems, (dialog, index, isChecked) -> {
|
||||
checkedItems[index] = isChecked;
|
||||
|
||||
boolean containsAtLeastOneCheckedItem = false;
|
||||
for(boolean b: checkedItems) {
|
||||
if (b) {
|
||||
containsAtLeastOneCheckedItem = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog alertDialog = (AlertDialog) dialog;
|
||||
Button positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
|
||||
positiveButton.setEnabled(containsAtLeastOneCheckedItem);
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int searchBehavior = 0;
|
||||
for (int i = 0; i < checkedItems.length; i++) {
|
||||
if (checkedItems[i]) {
|
||||
searchBehavior |= items[i];
|
||||
}
|
||||
}
|
||||
|
||||
_prefs.setSearchBehaviorMask(searchBehavior);
|
||||
currentSearchBehaviorPreference.setSummary(getSearchBehaviorSummary());
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
Dialogs.showSecureDialog(builder.create());
|
||||
return true;
|
||||
});
|
||||
|
||||
int currentCopyBehavior = _prefs.getCopyBehavior().ordinal();
|
||||
Preference copyBehaviorPreference = requirePreference("pref_copy_behavior");
|
||||
copyBehaviorPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.copy_behavior_titles)[currentCopyBehavior]));
|
||||
copyBehaviorPreference.setOnPreferenceClickListener(preference -> {
|
||||
int currentCopyBehavior1 = _prefs.getCopyBehavior().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.choose_copy_behavior))
|
||||
.setSingleChoiceItems(R.array.copy_behavior_titles, currentCopyBehavior1, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
_prefs.setCopyBehavior(CopyBehavior.fromInteger(i));
|
||||
copyBehaviorPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.copy_behavior_titles)[i]));
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
@ -38,18 +83,31 @@ public class BehaviorPreferencesFragment extends PreferencesFragment {
|
|||
return true;
|
||||
});
|
||||
|
||||
Preference entryPausePreference = requirePreference("pref_pause_entry");
|
||||
entryPausePreference.setEnabled(_prefs.isTapToRevealEnabled() || _prefs.isEntryHighlightEnabled());
|
||||
|
||||
Preference entryHighlightPreference = requirePreference("pref_highlight_entry");
|
||||
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
_entryPausePreference.setEnabled(_prefs.isTapToRevealEnabled() || (boolean) newValue);
|
||||
entryPausePreference.setEnabled(_prefs.isTapToRevealEnabled() || (boolean) newValue);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
_entryPausePreference = requirePreference("pref_pause_entry");
|
||||
_entryPausePreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
_entryPausePreference.setEnabled(_prefs.isTapToRevealEnabled() || _prefs.isEntryHighlightEnabled());
|
||||
private String getSearchBehaviorSummary() {
|
||||
final int[] settings = Preferences.SEARCH_BEHAVIOR_SETTINGS;
|
||||
final String[] descriptions = getResources().getStringArray(R.array.pref_search_behavior_types);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < settings.length; i++) {
|
||||
if (_prefs.isSearchBehaviorTypeEnabled(settings[i])) {
|
||||
if (builder.length() != 0) {
|
||||
builder.append(", ");
|
||||
}
|
||||
|
||||
builder.append(descriptions[i].toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
return getString(R.string.pref_search_behavior_summary, builder.toString());
|
||||
}
|
||||
}
|
||||
|
|
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