Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9d45b3ade | ||
|
|
4ad0a14e41 | ||
|
|
59d5c640d6 | ||
|
|
6ca5e94ae5 | ||
|
|
d2ebda791b | ||
|
|
7071b5e687 | ||
|
|
7cbdecae16 | ||
|
|
170cd930e9 | ||
|
|
b5e21a58b1 | ||
|
|
2edd388869 | ||
|
|
1607132dbf | ||
|
|
41fad611db | ||
|
|
c7eef8da08 | ||
|
|
f31ca4f1da | ||
|
|
7897a0941b | ||
|
|
276cdb584a | ||
|
|
491a81584d | ||
|
|
7038816afd | ||
|
|
bc600de7ab | ||
|
|
da9adf6490 | ||
|
|
aa4877607d | ||
|
|
90315996f4 | ||
|
|
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 |
@@ -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%"
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
- name: Tests
|
||||
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2
|
||||
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d
|
||||
with:
|
||||
api-level: 31
|
||||
arch: x86_64
|
||||
|
||||
@@ -14,6 +14,7 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
if: github.event_name != 'schedule' || github.repository == 'beemdevelopment/Aegis'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -13,19 +13,13 @@ jobs:
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -123,24 +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)
|
||||
|
||||
- [delta-aegis-icons](https://github.com/Delta-Icons/aegis-icons)
|
||||
|
||||
Delta version of the unofficial monochrome-styled 2FA icon pack aegis-icons.
|
||||
|
||||
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-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
|
||||
|
||||
@@ -155,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)
|
||||
|
||||
+63
-48
@@ -20,21 +20,27 @@ def fileProviderAuthority = "${packageName}.fileprovider"
|
||||
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
compileSdk 35
|
||||
|
||||
namespace packageName
|
||||
|
||||
defaultConfig {
|
||||
applicationId "${packageName}"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode 67
|
||||
versionName "3.1"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 35
|
||||
versionCode 81
|
||||
versionName "3.4.2"
|
||||
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'
|
||||
}
|
||||
@@ -87,22 +93,35 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,36 +152,34 @@ aboutLibraries {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def cameraxVersion = '1.3.1'
|
||||
def cameraxVersion = '1.4.2'
|
||||
def glideVersion = '4.16.0'
|
||||
def guavaVersion = '33.0.0'
|
||||
def hiltVersion = '2.50'
|
||||
def guavaVersion = '33.4.8'
|
||||
def hiltVersion = '2.56.2'
|
||||
def junitVersion = '4.13.2'
|
||||
def libsuVersion = '5.2.2'
|
||||
def roomVersion = "2.6.1"
|
||||
def libsuVersion = '6.0.0'
|
||||
def roomVersion = '2.7.1'
|
||||
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.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}"
|
||||
annotationProcessor "androidx.room:room-compiler:$roomVersion"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.activity:activity:1.8.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:$cameraxVersion"
|
||||
implementation "androidx.core:core:1.12.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.6.2"
|
||||
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.2'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.4.0'
|
||||
implementation "androidx.room:room-runtime:$roomVersion"
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
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'
|
||||
@@ -174,37 +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.11.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
|
||||
implementation 'com.google.zxing:core:3.5.2'
|
||||
implementation("com.mikepenz:aboutlibraries:11.1.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.1.0"
|
||||
implementation 'com.nulab-inc:zxcvbn:1.8.2'
|
||||
implementation 'de.hdodenhof:circleimageview:3.1.0'
|
||||
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.77'
|
||||
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:20231013'
|
||||
testImplementation 'org.robolectric:robolectric:4.11.1'
|
||||
testImplementation 'org.json:json:20250517'
|
||||
testImplementation 'org.robolectric:robolectric:4.14.1'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
+6
-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>
|
||||
|
||||
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -118,10 +119,11 @@ public class OverallTest extends AegisTest {
|
||||
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());
|
||||
|
||||
@@ -175,20 +177,18 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
private void addEntry(VaultEntry entry) {
|
||||
onView(withId(R.id.fab)).perform(click());
|
||||
onView(withId(R.id.fab_enter)).perform(click());
|
||||
onView(withId(R.id.fab_menu_item_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());
|
||||
|
||||
@@ -208,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());
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- NOTE: Disabled for now. See issue: #1047
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
@@ -27,6 +29,7 @@
|
||||
android:icon="@mipmap/${iconName}"
|
||||
android:label="Aegis"
|
||||
android:supportsRtl="true"
|
||||
android:largeHeap="true"
|
||||
android:theme="@style/Theme.Aegis.Launch"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity android:name=".ui.TransferEntriesActivity"
|
||||
@@ -134,6 +137,15 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.QsTileRefreshReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.USER_UNLOCKED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${fileProviderAuthority}"
|
||||
@@ -149,7 +161,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" />
|
||||
|
||||
@@ -31,6 +31,134 @@
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 3.4.2</h3>
|
||||
<p>
|
||||
This version fixes the quick settings tile staying inactive permanently after upgrading to Android 16.
|
||||
</p>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Redesigned FAB menu</li>
|
||||
<li>Ability to import otpauth uri from clipboard</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Fix quick settings tile state</li>
|
||||
<li>Disable autofill services in 'Edit Entry' screen to avoid accidental overwriting master password</li>
|
||||
<li>Inverted positions of buttons in 'Select Group' dialog</li>
|
||||
<li>Remove redundant padding in tiles view</li>
|
||||
</ul>
|
||||
<h3>Version 3.4.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for importing from Proton Authenticator</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The autofill service would show a prompt to save the PIN as a password</li>
|
||||
</ul>
|
||||
<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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -17,23 +17,14 @@ import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleEventObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import androidx.room.Room;
|
||||
|
||||
import com.beemdevelopment.aegis.database.AppDatabase;
|
||||
import com.beemdevelopment.aegis.database.AuditLogEntry;
|
||||
import com.beemdevelopment.aegis.database.AuditLogRepository;
|
||||
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.topjohnwu.superuser.Shell;
|
||||
|
||||
import org.checkerframework.checker.units.qual.A;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import dagger.hilt.InstallIn;
|
||||
import dagger.hilt.android.EarlyEntryPoint;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum BackupsVersioningStrategy {
|
||||
UNDEFINED,
|
||||
MULTIPLE_BACKUPS,
|
||||
SINGLE_BACKUP
|
||||
}
|
||||
@@ -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,9 +5,11 @@ 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;
|
||||
@@ -34,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) {
|
||||
@@ -70,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;
|
||||
@@ -139,6 +163,14 @@ 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");
|
||||
|
||||
@@ -162,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;
|
||||
}
|
||||
@@ -561,6 +607,19 @@ public class Preferences {
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
public static class BackupResult {
|
||||
private final Date _time;
|
||||
private boolean _isBuiltIn;
|
||||
|
||||
@@ -14,10 +14,4 @@ public class ThemeMap {
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_Amoled
|
||||
);
|
||||
|
||||
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_Amoled_Fullscreen
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.widget.ImageView;
|
||||
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class FabMenuHelper {
|
||||
private final static long ANIMATION_DURATION = 300L;
|
||||
private final static long ANIMATION_ACTION_DELAY = 50L;
|
||||
private final View _scrim;
|
||||
private final View _menuItemsContainer;
|
||||
private final FloatingActionButton _mainFab;
|
||||
private final List<View> _actions;
|
||||
private Consumer<Boolean> _stateListener;
|
||||
private boolean _isOpen = false;
|
||||
|
||||
public FabMenuHelper(
|
||||
View scrim,
|
||||
ViewGroup menuItemsContainer,
|
||||
FloatingActionButton fab,
|
||||
Map<View, Runnable> actions
|
||||
) {
|
||||
_scrim = scrim;
|
||||
_menuItemsContainer = menuItemsContainer;
|
||||
_mainFab = fab;
|
||||
_actions = new ArrayList<>(actions.keySet());
|
||||
|
||||
for (View action : _actions) {
|
||||
action.setVisibility(View.GONE);
|
||||
action.setAlpha(0f);
|
||||
action.setScaleX(0f);
|
||||
action.setScaleY(0f);
|
||||
}
|
||||
|
||||
setupClickListeners(actions);
|
||||
}
|
||||
|
||||
public void setOnFabMenuStateChangeListener(Consumer<Boolean> listener) {
|
||||
_stateListener = listener;
|
||||
}
|
||||
|
||||
private void setupClickListeners(Map<View, Runnable> actions) {
|
||||
_mainFab.setOnClickListener(v -> toggle());
|
||||
_scrim.setOnClickListener(v -> close());
|
||||
|
||||
actions.forEach((action, onClick) -> {
|
||||
action.setOnClickListener(v -> {
|
||||
if (onClick != null) {
|
||||
onClick.run();
|
||||
}
|
||||
close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void toggle() {
|
||||
if (_isOpen) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
public void open() {
|
||||
if (_isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isOpen = true;
|
||||
|
||||
_scrim.animate()
|
||||
.alpha(0.5f)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.withStartAction(() -> _scrim.setVisibility(View.VISIBLE))
|
||||
.start();
|
||||
|
||||
_menuItemsContainer.setVisibility(View.VISIBLE);
|
||||
|
||||
long delay = 0L;
|
||||
for (int i = _actions.size() - 1; i >= 0; i--) {
|
||||
animateActionIn(_actions.get(i), delay);
|
||||
delay += ANIMATION_ACTION_DELAY;
|
||||
}
|
||||
|
||||
animateFabIconForward(_mainFab);
|
||||
|
||||
if (_stateListener != null) {
|
||||
_stateListener.accept(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (!_isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isOpen = false;
|
||||
|
||||
_scrim.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.withEndAction(() -> _scrim.setVisibility(View.GONE))
|
||||
.start();
|
||||
|
||||
long delay = 0L;
|
||||
for (View action : _actions) {
|
||||
animateActionOut(action, delay);
|
||||
delay += ANIMATION_ACTION_DELAY;
|
||||
}
|
||||
|
||||
animateFabIconBackward(_mainFab);
|
||||
|
||||
_mainFab.postDelayed(() -> {
|
||||
if (!_isOpen) {
|
||||
_menuItemsContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}, ANIMATION_DURATION);
|
||||
|
||||
if (_stateListener != null) {
|
||||
_stateListener.accept(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void animateFabIconForward(FloatingActionButton fab) {
|
||||
animateFabIcon(fab, 0f, 45f);
|
||||
}
|
||||
|
||||
private void animateFabIconBackward(FloatingActionButton fab) {
|
||||
animateFabIcon(fab, 45f, 0f);
|
||||
}
|
||||
|
||||
private void animateFabIcon(FloatingActionButton fab, float from, float to) {
|
||||
Drawable drawable = _mainFab.getDrawable();
|
||||
int width = drawable.getIntrinsicWidth();
|
||||
int height = drawable.getIntrinsicHeight();
|
||||
fab.setScaleType(ImageView.ScaleType.MATRIX);
|
||||
Matrix matrix = new Matrix();
|
||||
ValueAnimator anim = ValueAnimator.ofFloat(from, to);
|
||||
anim.setDuration(100L);
|
||||
|
||||
anim.addUpdateListener(valueAnimator -> {
|
||||
Float angle = (Float) valueAnimator.getAnimatedValue();
|
||||
matrix.reset();
|
||||
matrix.postRotate(angle, width / 2f, height / 2f);
|
||||
fab.setImageMatrix(matrix);
|
||||
});
|
||||
|
||||
anim.start();
|
||||
}
|
||||
|
||||
private void animateActionIn(View action, long delay) {
|
||||
action.setVisibility(View.VISIBLE);
|
||||
action.setAlpha(0f);
|
||||
action.setScaleX(0.4f);
|
||||
action.setScaleY(0.4f);
|
||||
|
||||
action.animate()
|
||||
.alpha(1f)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.setStartDelay(delay)
|
||||
.setInterpolator(new OvershootInterpolator(1.2f))
|
||||
.start();
|
||||
}
|
||||
|
||||
private void animateActionOut(View action, long delay) {
|
||||
action.animate()
|
||||
.alpha(0f)
|
||||
.scaleX(0f)
|
||||
.scaleY(0f)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.setStartDelay(delay)
|
||||
.withEndAction(() -> action.setVisibility(View.GONE))
|
||||
.start();
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return _isOpen;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -69,7 +69,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
int swipeFlags = 0;
|
||||
if (adapter.isPositionFooter(position)
|
||||
|| adapter.isPositionErrorCard(position)
|
||||
|| adapter.getEntryAtPos(position) != _selectedEntry
|
||||
|| adapter.getEntryAtPosition(position) != _selectedEntry
|
||||
|| !isLongPressDragEnabled()) {
|
||||
return makeMovementFlags(0, swipeFlags);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ public class ThemeHelper {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -34,17 +34,19 @@ 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("Proton Authenticator", ProtonAuthenticatorImporter.class, R.string.importer_help_proton_authenticator, 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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.helpers.ContextHelper;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.Argon2Task;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.common.base.Objects;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.bouncycastle.crypto.params.Argon2Parameters;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
|
||||
public class ProtonAuthenticatorImporter extends DatabaseImporter {
|
||||
|
||||
public ProtonAuthenticatorImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull State read(@NonNull InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
String contents = new String(IOUtils.readAll(stream), UTF_8);
|
||||
JSONObject json = new JSONObject(contents);
|
||||
|
||||
if (json.has("salt") && json.has("content")) {
|
||||
int version = json.getInt("version");
|
||||
if (version != 1) {
|
||||
throw new DatabaseImporterException(String.format("Unsupported version: %d", version));
|
||||
}
|
||||
byte[] salt = Base64.decode(json.getString("salt"));
|
||||
byte[] content = Base64.decode(json.getString("content"));
|
||||
return new EncryptedState(salt, content);
|
||||
}
|
||||
|
||||
return new DecryptedState(json);
|
||||
} catch (JSONException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private static final int KEY_SIZE = 32;
|
||||
private static final int MEMORY_KB = 19 * 1024;
|
||||
private static final int ITERATIONS = 2;
|
||||
private static final int PARALLELISM = 1;
|
||||
private static final int NONCE_SIZE = 12;
|
||||
private static final int TAG_SIZE = 16;
|
||||
private static final byte[] AAD = "proton.authenticator.export.v1".getBytes(UTF_8);
|
||||
|
||||
private final byte[] _salt;
|
||||
private final byte[] _content;
|
||||
|
||||
private EncryptedState(byte[] salt, byte[] content) {
|
||||
super(true);
|
||||
_salt = salt;
|
||||
_content = content;
|
||||
}
|
||||
|
||||
public DecryptedState decrypt(char[] password) throws DatabaseImporterException {
|
||||
Argon2Task.Params params = getKeyDerivationParams(password);
|
||||
SecretKey key = Argon2Task.deriveKey(params);
|
||||
return decrypt(key);
|
||||
}
|
||||
|
||||
private DecryptedState decrypt(SecretKey key) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] nonce = Arrays.copyOfRange(_content, 0, NONCE_SIZE);
|
||||
byte[] ct = Arrays.copyOfRange(_content, NONCE_SIZE, _content.length);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_SIZE * 8, nonce));
|
||||
cipher.updateAAD(AAD);
|
||||
byte[] plaintext = cipher.doFinal(ct);
|
||||
|
||||
return new DecryptedState(new JSONObject(new String(plaintext, UTF_8)));
|
||||
} catch (BadPaddingException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
|
||||
| InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, R.string.enter_password_proton_message, password -> {
|
||||
Argon2Task.Params params = getKeyDerivationParams(password);
|
||||
Argon2Task task = new Argon2Task(context, key -> {
|
||||
try {
|
||||
DecryptedState 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)
|
||||
.withVersion(Argon2Parameters.ARGON2_VERSION_13)
|
||||
.withIterations(ITERATIONS)
|
||||
.withParallelism(PARALLELISM)
|
||||
.withMemoryAsKB(MEMORY_KB)
|
||||
.withSalt(_salt)
|
||||
.build();
|
||||
return new Argon2Task.Params(password, argon2Params, KEY_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedState extends DatabaseImporter.State {
|
||||
private final JSONObject _json;
|
||||
|
||||
public DecryptedState(@NonNull JSONObject json) {
|
||||
super(false);
|
||||
_json = json;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Result convert() throws DatabaseImporterException {
|
||||
Result result = new Result();
|
||||
|
||||
try {
|
||||
JSONArray entries = _json.getJSONArray("entries");
|
||||
for (int i = 0; i < entries.length(); i++) {
|
||||
JSONObject entry = entries.getJSONObject(i);
|
||||
try {
|
||||
result.addEntry(convertEntry(entry));
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static @NonNull VaultEntry convertEntry(@NonNull JSONObject entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
JSONObject content = entry.getJSONObject("content");
|
||||
String name = JsonUtils.optString(content, "name");
|
||||
if (name == null) {
|
||||
name = "";
|
||||
}
|
||||
String uriString = content.getString("uri");
|
||||
|
||||
Uri uri = Uri.parse(uriString);
|
||||
try {
|
||||
if (Objects.equal(uri.getScheme(), "steam") && uri.getHost() != null) {
|
||||
SteamInfo otp = new SteamInfo(Base32.decode(uri.getHost()));
|
||||
return new VaultEntry(otp, name, "Steam");
|
||||
}
|
||||
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
|
||||
return new VaultEntry(info.getOtpInfo(), name, info.getIssuer());
|
||||
} catch (GoogleAuthInfoException | OtpInfoException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, uriString);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -45,11 +45,11 @@ import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
public class AuthenticatorProImporter extends DatabaseImporter {
|
||||
public class StratumImporter extends DatabaseImporter {
|
||||
private static final String HEADER = "AUTHENTICATORPRO";
|
||||
private static final String HEADER_LEGACY = "AuthenticatorPro";
|
||||
private static final String PKG_NAME = "me.jmh.authenticatorpro";
|
||||
private static final String PKG_DB_PATH = "files/proauth.db3";
|
||||
private static final String PKG_NAME = "com.stratumauth.app";
|
||||
private static final String PKG_DB_PATH = "databases/authenticator.db3";
|
||||
|
||||
private enum Algorithm {
|
||||
SHA1,
|
||||
@@ -57,7 +57,7 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
||||
SHA512
|
||||
}
|
||||
|
||||
public AuthenticatorProImporter(Context context) {
|
||||
public StratumImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
||||
Argon2Task.Params params = getKeyDerivationParams(password);
|
||||
Argon2Task task = new Argon2Task(context, key -> {
|
||||
try {
|
||||
AuthenticatorProImporter.JsonState state = decrypt(key);
|
||||
StratumImporter.JsonState state = decrypt(key);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
@@ -244,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);
|
||||
@@ -4,9 +4,9 @@ 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;
|
||||
@@ -16,6 +16,7 @@ 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;
|
||||
@@ -173,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.beemdevelopment.aegis.receivers;
|
||||
|
||||
import static android.content.Intent.ACTION_BOOT_COMPLETED;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.service.quicksettings.TileService;
|
||||
|
||||
import com.beemdevelopment.aegis.services.LaunchAppTileService;
|
||||
import com.beemdevelopment.aegis.services.LaunchScannerTileService;
|
||||
|
||||
public class QsTileRefreshReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction() == null
|
||||
|| (!intent.getAction().equals(ACTION_BOOT_COMPLETED)
|
||||
&& !intent.getAction().equals(Intent.ACTION_USER_UNLOCKED))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
TileService.requestListeningState(context,
|
||||
new ComponentName(context, LaunchAppTileService.class));
|
||||
TileService.requestListeningState(context,
|
||||
new ComponentName(context, LaunchScannerTileService.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -13,11 +13,15 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.StringRes;
|
||||
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.ui.dialogs.ChangelogDialog;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.LicenseDialog;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
public class AboutActivity extends AegisActivity {
|
||||
@@ -39,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);
|
||||
@@ -90,6 +95,17 @@ public class AboutActivity extends AegisActivity {
|
||||
.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() {
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -12,7 +12,6 @@ import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
@@ -26,6 +25,7 @@ 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;
|
||||
@@ -186,13 +186,9 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
|
||||
private class ActionModeStatusGuardHack {
|
||||
private Field _fadeAnimField;
|
||||
private Field _actionModeViewField;
|
||||
|
||||
@ColorInt
|
||||
private final int _statusBarColor;
|
||||
private Drawable _appBarBackground;
|
||||
|
||||
private ActionModeStatusGuardHack() {
|
||||
_statusBarColor = getWindow().getStatusBarColor();
|
||||
|
||||
try {
|
||||
_fadeAnimField = getDelegate().getClass().getDeclaredField("mFadeAnim");
|
||||
_fadeAnimField.setAccessible(true);
|
||||
@@ -216,20 +212,26 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
|
||||
return;
|
||||
}
|
||||
|
||||
if (fadeAnim == null || actionModeView == null) {
|
||||
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();
|
||||
|
||||
actionModeView.setVisibility(visibility);
|
||||
actionModeView.setAlpha(visibility == View.VISIBLE ? 1f : 0f);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
int statusBarColor = visibility == View.VISIBLE
|
||||
? MaterialColors.getColor(actionModeView, com.google.android.material.R.attr.colorSurfaceContainer)
|
||||
: _statusBarColor;
|
||||
getWindow().setStatusBarColor(statusBarColor);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ 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;
|
||||
@@ -61,6 +62,7 @@ public class AssignIconsActivity extends AegisActivity implements AssignIconAdap
|
||||
|
||||
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);
|
||||
|
||||
@@ -42,6 +42,7 @@ 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 com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -72,7 +73,22 @@ public class AuthActivity extends AegisActivity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_auth);
|
||||
_textPassword = findViewById(R.id.text_password);
|
||||
|
||||
TextInputLayout layoutStandard = findViewById(R.id.layout_standard);
|
||||
TextInputLayout layoutNoAutofill = findViewById(R.id.layout_no_autofill);
|
||||
EditText editStandard = findViewById(R.id.text_password);
|
||||
EditText editNoAutofill = findViewById(R.id.text_password_no_autofill);
|
||||
|
||||
if (_prefs.isPinKeyboardEnabled()) {
|
||||
layoutStandard.setVisibility(View.GONE);
|
||||
layoutNoAutofill.setVisibility(View.VISIBLE);
|
||||
_textPassword = editNoAutofill;
|
||||
} else {
|
||||
layoutStandard.setVisibility(View.VISIBLE);
|
||||
layoutNoAutofill.setVisibility(View.GONE);
|
||||
_textPassword = editStandard;
|
||||
}
|
||||
|
||||
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
|
||||
_decryptButton = findViewById(R.id.button_decrypt);
|
||||
TextView biometricsButton = findViewById(R.id.button_biometrics);
|
||||
@@ -86,10 +102,6 @@ public class AuthActivity extends AegisActivity {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (_prefs.isPinKeyboardEnabled()) {
|
||||
_textPassword.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (savedInstanceState == null) {
|
||||
_inhibitBioPrompt = intent.getBooleanExtra("inhibitBioPrompt", false);
|
||||
@@ -241,7 +253,7 @@ public class AuthActivity extends AegisActivity {
|
||||
popup.setFocusable(false);
|
||||
popup.setOutsideTouchable(true);
|
||||
_textPassword.post(() -> {
|
||||
if (isFinishing()) {
|
||||
if (isFinishing() || !_textPassword.isAttachedToWindow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
@@ -16,6 +16,7 @@ import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
@@ -27,6 +28,7 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.avito.android.krop.KropView;
|
||||
@@ -35,12 +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.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;
|
||||
@@ -68,11 +72,13 @@ 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;
|
||||
@@ -83,16 +89,14 @@ 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.Objects;
|
||||
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 boolean _isNew = false;
|
||||
private boolean _isManual = false;
|
||||
@@ -102,11 +106,14 @@ public class EditEntryActivity extends AegisActivity {
|
||||
// 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;
|
||||
@@ -121,13 +128,12 @@ public class EditEntryActivity extends AegisActivity {
|
||||
private AutoCompleteTextView _dropdownType;
|
||||
private AutoCompleteTextView _dropdownAlgo;
|
||||
private TextInputLayout _dropdownAlgoLayout;
|
||||
private AutoCompleteTextView _dropdownGroup;
|
||||
private List<VaultGroupModel> _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;
|
||||
@@ -138,8 +144,8 @@ public class EditEntryActivity extends AegisActivity {
|
||||
if (activityResult.getResultCode() != RESULT_OK || data == null || data.getData() == null) {
|
||||
return;
|
||||
}
|
||||
String fileType = SafHelper.getMimeType(this, data.getData());
|
||||
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
|
||||
_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) {
|
||||
@@ -163,6 +169,7 @@ 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();
|
||||
|
||||
@@ -195,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);
|
||||
@@ -210,9 +219,6 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_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) {
|
||||
@@ -241,7 +247,7 @@ 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);
|
||||
@@ -286,10 +292,15 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
Set<UUID> groups = _origEntry.getGroups();
|
||||
if (groups.isEmpty()) {
|
||||
setGroup(new VaultGroupModel(getString(R.string.no_group)));
|
||||
_textGroup.setText(getString(R.string.no_group));
|
||||
} else {
|
||||
VaultGroup group = _vaultManager.getVault().getGroupByUUID(groups.iterator().next());
|
||||
setGroup(new VaultGroupModel(group));
|
||||
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
|
||||
@@ -298,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);
|
||||
@@ -354,38 +365,109 @@ public class EditEntryActivity extends AegisActivity {
|
||||
startIconSelection();
|
||||
});
|
||||
|
||||
_dropdownGroup.setOnItemClickListener((parent, view, position, id) -> {
|
||||
VaultGroupModel selectedGroup = _dropdownGroupList.get(position);
|
||||
if (selectedGroup.isPlaceholder() && Objects.equals(selectedGroup.getName(), getString(R.string.new_group))) {
|
||||
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);
|
||||
}
|
||||
|
||||
updateGroupDropdownList();
|
||||
setGroup(new VaultGroupModel(group));
|
||||
}
|
||||
};
|
||||
|
||||
DialogInterface.OnCancelListener onCancel = dialogInterface -> {
|
||||
VaultGroupModel previous = (VaultGroupModel) _dropdownGroup.getTag();
|
||||
_dropdownGroup.setText(previous.getName(), false);
|
||||
};
|
||||
|
||||
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, onAddGroup, onCancel);
|
||||
} else {
|
||||
setGroup(_dropdownGroupList.get(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) {
|
||||
boolean enabled = !otpType.equals(SteamInfo.ID) && !otpType.equals(YandexInfo.ID)
|
||||
&& !otpType.equals(MotpInfo.ID) && (!_isNew || _isManual);
|
||||
@@ -400,36 +482,20 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin);
|
||||
}
|
||||
|
||||
private void setGroup(VaultGroupModel group) {
|
||||
_dropdownGroup.setText(group.getName(), false);
|
||||
_dropdownGroup.setTag(group);
|
||||
}
|
||||
|
||||
private void openAdvancedSettings() {
|
||||
Animation fadeOut = new AlphaAnimation(1, 0);
|
||||
fadeOut.setInterpolator(new AccelerateInterpolator());
|
||||
fadeOut.setDuration((long) (220 * AnimationsHelper.Scale.ANIMATOR.getValue(this)));
|
||||
_advancedSettingsHeader.startAnimation(fadeOut);
|
||||
|
||||
Animation fadeIn = new AlphaAnimation(0, 1);
|
||||
fadeIn.setInterpolator(new AccelerateInterpolator());
|
||||
fadeIn.setDuration((long) (250 * 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() {
|
||||
_dropdownGroupList.clear();
|
||||
_dropdownGroupList.add(new VaultGroupModel(getString(R.string.new_group)));
|
||||
_dropdownGroupList.addAll(_groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
|
||||
_dropdownGroupList.add(new VaultGroupModel(getString(R.string.no_group)));
|
||||
}
|
||||
|
||||
private boolean hasUnsavedChanges(VaultEntry newEntry) {
|
||||
@@ -731,13 +797,10 @@ public class EditEntryActivity extends AegisActivity {
|
||||
entry.setName(_textName.getText().toString());
|
||||
entry.setNote(_textNote.getText().toString());
|
||||
|
||||
VaultGroupModel group = (VaultGroupModel) _dropdownGroup.getTag();
|
||||
if (group.isPlaceholder()) {
|
||||
if (_selectedGroups.isEmpty()) {
|
||||
entry.setGroups(new HashSet<>());
|
||||
} else {
|
||||
Set<UUID> groups = new HashSet<>();
|
||||
groups.add(group.getUUID());
|
||||
entry.setGroups(groups);
|
||||
entry.setGroups(new HashSet<>(_selectedGroups));
|
||||
}
|
||||
|
||||
if (_hasChangedIcon) {
|
||||
@@ -745,11 +808,12 @@ public class EditEntryActivity extends AegisActivity {
|
||||
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();
|
||||
icon = new VaultEntryIcon(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())){
|
||||
@@ -791,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);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.View;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -14,13 +15,10 @@ import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryException;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -30,7 +28,6 @@ import java.util.UUID;
|
||||
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
|
||||
private GroupAdapter _adapter;
|
||||
private HashSet<UUID> _removedGroups;
|
||||
private HashSet<VaultGroup> _renamedGroups;
|
||||
private RecyclerView _groupsView;
|
||||
private View _emptyStateView;
|
||||
private BackPressHandler _backPressHandler;
|
||||
@@ -43,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);
|
||||
@@ -51,32 +49,45 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
|
||||
|
||||
_removedGroups = new HashSet<>();
|
||||
_renamedGroups = new HashSet<>();
|
||||
if (savedInstanceState != null) {
|
||||
List<String> removedGroups = savedInstanceState.getStringArrayList("removedGroups");
|
||||
List<String> renamedGroups = savedInstanceState.getStringArrayList("renamedGroups");
|
||||
if (removedGroups != null) {
|
||||
for (String uuid : removedGroups) {
|
||||
_removedGroups.add(UUID.fromString(uuid));
|
||||
}
|
||||
}
|
||||
if (renamedGroups != null) {
|
||||
for (String groupObject : renamedGroups) {
|
||||
try {
|
||||
_renamedGroups.add(VaultGroup.fromJson(new JSONObject(groupObject)));
|
||||
} catch (VaultEntryException | JSONException ignored) {
|
||||
// This is intentionally ignored since the json object is valid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ItemTouchHelper touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||
@Override
|
||||
public int getMovementFlags(
|
||||
@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
|
||||
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())) {
|
||||
@@ -84,11 +95,6 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
}
|
||||
}
|
||||
|
||||
for(VaultGroup group: _renamedGroups) {
|
||||
_adapter.replaceGroup(group.getUUID(), group);
|
||||
}
|
||||
|
||||
|
||||
_emptyStateView = findViewById(R.id.vEmptyList);
|
||||
updateEmptyState();
|
||||
}
|
||||
@@ -100,12 +106,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
for (UUID uuid : _removedGroups) {
|
||||
removed.add(uuid.toString());
|
||||
}
|
||||
ArrayList<String> renamed = new ArrayList<>();
|
||||
for (VaultGroup group : _renamedGroups) {
|
||||
renamed.add(group.toJson().toString());
|
||||
}
|
||||
|
||||
outState.putStringArrayList("renamedGroups", renamed);
|
||||
outState.putStringArrayList("removedGroups", removed);
|
||||
}
|
||||
|
||||
@@ -116,7 +117,6 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
if (!newGroupName.isEmpty()) {
|
||||
VaultGroup newGroup = Cloner.clone(group);
|
||||
newGroup.setName(newGroupName);
|
||||
_renamedGroups.add(newGroup);
|
||||
_adapter.replaceGroup(group.getUUID(), newGroup);
|
||||
_backPressHandler.setEnabled(true);
|
||||
}
|
||||
@@ -166,23 +166,16 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
for (UUID uuid : _removedGroups) {
|
||||
_vaultManager.getVault().removeGroup(uuid);
|
||||
}
|
||||
|
||||
saveAndBackupVault();
|
||||
}
|
||||
if (!_renamedGroups.isEmpty()) {
|
||||
_renamedGroups.removeIf(group -> _removedGroups.contains(group.getUUID()));
|
||||
for (VaultGroup renamedGroup : _renamedGroups) {
|
||||
_vaultManager.getVault().renameGroup(renamedGroup);
|
||||
}
|
||||
|
||||
saveAndBackupVault();
|
||||
}
|
||||
_vaultManager.getVault().replaceGroups(_adapter.getGroups());
|
||||
saveAndBackupVault();
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private void discardAndFinish() {
|
||||
if (_removedGroups.isEmpty() && _renamedGroups.isEmpty()) {
|
||||
if (_removedGroups.isEmpty()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,16 +15,21 @@ 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;
|
||||
@@ -39,12 +44,15 @@ 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;
|
||||
|
||||
@@ -58,6 +66,7 @@ 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);
|
||||
|
||||
@@ -66,8 +75,8 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
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);
|
||||
@@ -76,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 -> {
|
||||
@@ -170,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
|
||||
@@ -185,7 +194,7 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
importDatabase(state);
|
||||
processDecryptedImporterState(state);
|
||||
}
|
||||
} catch (DatabaseImporterException e) {
|
||||
e.printStackTrace();
|
||||
@@ -193,8 +202,7 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void importDatabase(DatabaseImporter.State state) {
|
||||
List<ImportEntry> importEntries = new ArrayList<>();
|
||||
private void processDecryptedImporterState(DatabaseImporter.State state) {
|
||||
DatabaseImporter.Result result;
|
||||
try {
|
||||
result = state.convert();
|
||||
@@ -204,8 +212,29 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
UUIDMap<VaultEntry> entries = result.getEntries();
|
||||
for (VaultEntry entry : entries.getValues()) {
|
||||
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);
|
||||
@@ -330,6 +359,31 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
|
||||
_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) {
|
||||
|
||||
@@ -17,12 +17,16 @@ import android.provider.Settings;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Toast;
|
||||
@@ -31,18 +35,24 @@ 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.AlertDialog;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.AccountNamePosition;
|
||||
import com.beemdevelopment.aegis.CopyBehavior;
|
||||
import com.beemdevelopment.aegis.GroupPlaceholderType;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.helpers.BitmapHelper;
|
||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FabMenuHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.helpers.PermissionHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
@@ -50,26 +60,40 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
|
||||
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
|
||||
import com.beemdevelopment.aegis.ui.tasks.IconOptimizationTask;
|
||||
import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryListView;
|
||||
import com.beemdevelopment.aegis.util.ClipboardUtils;
|
||||
import com.beemdevelopment.aegis.util.TimeUtils;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
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.color.MaterialColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MainActivity extends AegisActivity implements EntryListView.Listener {
|
||||
@@ -91,7 +115,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
private SearchView _searchView;
|
||||
private EntryListView _entryListView;
|
||||
|
||||
private Collection<VaultGroup> _groups;
|
||||
private ChipGroup _groupChip;
|
||||
private Set<UUID> _groupFilter;
|
||||
private Set<UUID> _prefGroupFilter;
|
||||
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
private FabMenuHelper _fabMenuHelper;
|
||||
|
||||
private ActionMode _actionMode;
|
||||
private ActionMode.Callback _actionModeCallbacks = new ActionModeCallbacks();
|
||||
@@ -99,6 +129,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
private LockBackPressHandler _lockBackPressHandler;
|
||||
private SearchViewBackPressHandler _searchViewBackPressHandler;
|
||||
private ActionModeBackPressHandler _actionModeBackPressHandler;
|
||||
private FabMenuBackPressHandler _fabMenuBackPressHandler;
|
||||
|
||||
private final ActivityResultLauncher<Intent> authResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
@@ -129,23 +160,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||
return;
|
||||
}
|
||||
onAssignEntriesResult(activityResult.getData());
|
||||
onAssignIconsResult();
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> preferenceResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||
return;
|
||||
}
|
||||
onPreferencesResult(activityResult.getData());
|
||||
});
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> onPreferencesResult());
|
||||
|
||||
private final ActivityResultLauncher<Intent> editEntryResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||
return;
|
||||
}
|
||||
onEditEntryResult(activityResult.getData());
|
||||
onEditEntryResult();
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> addEntryResultLauncher =
|
||||
@@ -168,6 +194,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
_loaded = false;
|
||||
_isDPadPressed = false;
|
||||
_isDoingIntro = false;
|
||||
@@ -186,48 +213,185 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
getOnBackPressedDispatcher().addCallback(this, _searchViewBackPressHandler);
|
||||
_actionModeBackPressHandler = new ActionModeBackPressHandler();
|
||||
getOnBackPressedDispatcher().addCallback(this, _actionModeBackPressHandler);
|
||||
_fabMenuBackPressHandler = new FabMenuBackPressHandler();
|
||||
getOnBackPressedDispatcher().addCallback(this, _fabMenuBackPressHandler);
|
||||
|
||||
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
|
||||
_entryListView.setListener(this);
|
||||
_entryListView.setCodeGroupSize(_prefs.getCodeGroupSize());
|
||||
_entryListView.setAccountNamePosition(_prefs.getAccountNamePosition());
|
||||
_entryListView.setShowIcon(_prefs.isIconVisible());
|
||||
_entryListView.setShowExpirationState(_prefs.getShowExpirationState());
|
||||
_entryListView.setShowNextCode(_prefs.getShowNextCode());
|
||||
_entryListView.setOnlyShowNecessaryAccountNames(_prefs.onlyShowNecessaryAccountNames());
|
||||
_entryListView.setHighlightEntry(_prefs.isEntryHighlightEnabled());
|
||||
_entryListView.setPauseFocused(_prefs.isPauseFocusedEnabled());
|
||||
_entryListView.setTapToReveal(_prefs.isTapToRevealEnabled());
|
||||
_entryListView.setTapToRevealTime(_prefs.getTapToRevealTime());
|
||||
_entryListView.setSortCategory(_prefs.getCurrentSortCategory(), false);
|
||||
_entryListView.setViewMode(_prefs.getCurrentViewMode());
|
||||
_entryListView.setSortCategory(_prefs.getCurrentSortCategory(), false);
|
||||
_entryListView.setCopyBehavior(_prefs.getCopyBehavior());
|
||||
_entryListView.setPrefGroupFilter(_prefs.getGroupFilter());
|
||||
_entryListView.setSearchBehaviorMask(_prefs.getSearchBehaviorMask());
|
||||
_prefGroupFilter = _prefs.getGroupFilter();
|
||||
|
||||
FloatingActionButton fab = findViewById(R.id.fab);
|
||||
fab.setOnClickListener(v -> {
|
||||
View view = getLayoutInflater().inflate(R.layout.dialog_add_entry, null);
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(this);
|
||||
dialog.setContentView(view);
|
||||
View scrimOverlayLayout = LayoutInflater.from(this).inflate(R.layout.scrim_layout, null);
|
||||
View scrimOverlay = scrimOverlayLayout.findViewById(R.id.scrim);
|
||||
addContentView(scrimOverlayLayout, new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
));
|
||||
|
||||
view.findViewById(R.id.fab_enter).setOnClickListener(v1 -> {
|
||||
dialog.dismiss();
|
||||
startEditEntryActivityForManual();
|
||||
});
|
||||
view.findViewById(R.id.fab_scan_image).setOnClickListener(v2 -> {
|
||||
dialog.dismiss();
|
||||
startScanImageActivity();
|
||||
});
|
||||
view.findViewById(R.id.fab_scan).setOnClickListener(v3 -> {
|
||||
dialog.dismiss();
|
||||
startScanActivity();
|
||||
});
|
||||
View fabMenuLayout = LayoutInflater.from(this).inflate(R.layout.fab_menu, null);
|
||||
addContentView(fabMenuLayout, new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
));
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
});
|
||||
ViewGroup menuItemsContainer = fabMenuLayout.findViewById(R.id.fab_menu_items_container);
|
||||
FloatingActionButton fab = fabMenuLayout.findViewById(R.id.fab);
|
||||
|
||||
LinkedHashMap<View, Runnable> actions = new LinkedHashMap<>();
|
||||
actions.put(fabMenuLayout.findViewById(R.id.fab_menu_item_scan), this::startScanActivity);
|
||||
actions.put(fabMenuLayout.findViewById(R.id.fab_menu_item_scan_image), this::startScanImageActivity);
|
||||
actions.put(fabMenuLayout.findViewById(R.id.fab_menu_item_enter), this::startEditEntryActivity);
|
||||
|
||||
_fabMenuHelper = new FabMenuHelper(scrimOverlay, menuItemsContainer, fab, actions);
|
||||
_fabMenuHelper.setOnFabMenuStateChangeListener(_fabMenuBackPressHandler::setEnabled);
|
||||
|
||||
_groupChip = findViewById(R.id.groupChipGroup);
|
||||
_fabScrollHelper = new FabScrollHelper(fab);
|
||||
_selectedEntries = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void setGroups(Collection<VaultGroup> groups) {
|
||||
_groups = groups;
|
||||
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
|
||||
if (_prefGroupFilter != null) {
|
||||
Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter);
|
||||
_prefGroupFilter = null;
|
||||
if (!groupFilter.isEmpty()) {
|
||||
_groupFilter = groupFilter;
|
||||
_entryListView.setGroupFilter(groupFilter);
|
||||
}
|
||||
} else if (_groupFilter != null) {
|
||||
Set<UUID> groupFilter = cleanGroupFilter(_groupFilter);
|
||||
if (!_groupFilter.equals(groupFilter)) {
|
||||
_groupFilter = groupFilter;
|
||||
_entryListView.setGroupFilter(groupFilter);
|
||||
}
|
||||
}
|
||||
|
||||
_entryListView.setGroups(groups);
|
||||
initializeGroups();
|
||||
}
|
||||
|
||||
private void initializeGroups() {
|
||||
_groupChip.removeAllViews();
|
||||
_groupChip.setSingleSelection(!_prefs.isGroupMultiselectEnabled());
|
||||
|
||||
for (VaultGroup group : _groups) {
|
||||
addChipTo(_groupChip, new VaultGroupModel(group));
|
||||
}
|
||||
|
||||
GroupPlaceholderType placeholderType = GroupPlaceholderType.NO_GROUP;
|
||||
addChipTo(_groupChip, new VaultGroupModel(this, placeholderType));
|
||||
addSaveChip(_groupChip);
|
||||
}
|
||||
|
||||
private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) {
|
||||
Set<UUID> groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet());
|
||||
|
||||
return groupFilter.stream()
|
||||
.filter(g -> g == null || groupUuids.contains(g))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) {
|
||||
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
|
||||
chip.setText(group.getName());
|
||||
chip.setCheckable(true);
|
||||
chip.setCheckedIconVisible(false);
|
||||
chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID()));
|
||||
|
||||
if (group.isPlaceholder()) {
|
||||
GroupPlaceholderType groupPlaceholderType = group.getPlaceholderType();
|
||||
chip.setTag(groupPlaceholderType);
|
||||
|
||||
if (groupPlaceholderType == GroupPlaceholderType.ALL) {
|
||||
chip.setChecked(_groupFilter == null);
|
||||
} else if (groupPlaceholderType == GroupPlaceholderType.NO_GROUP) {
|
||||
chip.setChecked(_groupFilter != null && _groupFilter.contains(null));
|
||||
}
|
||||
} else {
|
||||
chip.setTag(group);
|
||||
}
|
||||
|
||||
chip.setOnCheckedChangeListener((group1, isChecked) -> {
|
||||
if (_actionMode != null) {
|
||||
_actionMode.finish();
|
||||
}
|
||||
|
||||
setSaveChipVisibility(true);
|
||||
|
||||
// Reset group filter if last checked group gets unchecked
|
||||
if (!isChecked && _groupFilter.size() == 1) {
|
||||
Set<UUID> groupFilter = new HashSet<>();
|
||||
|
||||
chipGroup.clearCheck();
|
||||
_groupFilter = groupFilter;
|
||||
_entryListView.setGroupFilter(groupFilter);
|
||||
return;
|
||||
}
|
||||
|
||||
_groupFilter = getGroupFilter(chipGroup);
|
||||
_entryListView.setGroupFilter(_groupFilter);
|
||||
});
|
||||
|
||||
chipGroup.addView(chip);
|
||||
}
|
||||
|
||||
private void addSaveChip(ChipGroup chipGroup) {
|
||||
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
|
||||
|
||||
chip.setText(getString(R.string.save));
|
||||
chip.setVisibility(View.GONE);
|
||||
chip.setChipStrokeWidth(0);
|
||||
chip.setCheckable(false);
|
||||
chip.setChipBackgroundColorResource(android.R.color.transparent);
|
||||
chip.setTextColor(MaterialColors.getColor(chip.getRootView(), com.google.android.material.R.attr.colorSecondary));
|
||||
chip.setClickable(true);
|
||||
chip.setCheckedIconVisible(false);
|
||||
chip.setOnClickListener(v -> {
|
||||
onSaveGroupFilter(_groupFilter);
|
||||
setSaveChipVisibility(false);
|
||||
});
|
||||
|
||||
chipGroup.addView(chip);
|
||||
}
|
||||
|
||||
private void setSaveChipVisibility(boolean visible) {
|
||||
Chip saveChip = (Chip) _groupChip.getChildAt(_groupChip.getChildCount() - 1);
|
||||
saveChip.setChecked(false);
|
||||
saveChip.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private static Set<UUID> getGroupFilter(ChipGroup chipGroup) {
|
||||
return chipGroup.getCheckedChipIds().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(i -> {
|
||||
Chip chip = chipGroup.findViewById(i);
|
||||
|
||||
if (chip.getTag() instanceof VaultGroupModel) {
|
||||
VaultGroupModel group = (VaultGroupModel) chip.getTag();
|
||||
return group.getUUID();
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
_entryListView.setListener(null);
|
||||
@@ -256,6 +420,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
instance.putString("submittedSearchQuery", _submittedSearchQuery);
|
||||
instance.putBoolean("isDoingIntro", _isDoingIntro);
|
||||
instance.putBoolean("isAuthenticating", _isAuthenticating);
|
||||
|
||||
if (_groupFilter != null) {
|
||||
instance.putSerializable("prefGroupFilter", new HashSet<>(_groupFilter));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -285,39 +453,52 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
@Override
|
||||
public void onEntryListTouch() {
|
||||
_isDPadPressed = false;
|
||||
}
|
||||
|
||||
private void onPreferencesResult(Intent data) {
|
||||
// refresh the entire entry list if needed
|
||||
if (_loaded) {
|
||||
if (data.getBooleanExtra("needsRecreate", false)) {
|
||||
recreate();
|
||||
} else if (data.getBooleanExtra("needsRefresh", false)) {
|
||||
AccountNamePosition accountNamePosition = _prefs.getAccountNamePosition();
|
||||
boolean showIcons = _prefs.isIconVisible();
|
||||
boolean onlyShowNecessaryAccountNames = _prefs.onlyShowNecessaryAccountNames();
|
||||
Preferences.CodeGrouping codeGroupSize = _prefs.getCodeGroupSize();
|
||||
boolean highlightEntry = _prefs.isEntryHighlightEnabled();
|
||||
boolean pauseFocused = _prefs.isPauseFocusedEnabled();
|
||||
boolean tapToReveal = _prefs.isTapToRevealEnabled();
|
||||
int tapToRevealTime = _prefs.getTapToRevealTime();
|
||||
ViewMode viewMode = _prefs.getCurrentViewMode();
|
||||
CopyBehavior copyBehavior = _prefs.getCopyBehavior();
|
||||
_entryListView.setAccountNamePosition(accountNamePosition);
|
||||
_entryListView.setOnlyShowNecessaryAccountNames(onlyShowNecessaryAccountNames);
|
||||
_entryListView.setShowIcon(showIcons);
|
||||
_entryListView.setCodeGroupSize(codeGroupSize);
|
||||
_entryListView.setHighlightEntry(highlightEntry);
|
||||
_entryListView.setPauseFocused(pauseFocused);
|
||||
_entryListView.setTapToReveal(tapToReveal);
|
||||
_entryListView.setTapToRevealTime(tapToRevealTime);
|
||||
_entryListView.setViewMode(viewMode);
|
||||
_entryListView.setCopyBehavior(copyBehavior);
|
||||
_entryListView.refresh(true);
|
||||
if (_searchView != null && !_searchView.isIconified()) {
|
||||
if (ViewCompat.getRootWindowInsets(findViewById(android.R.id.content).getRootView()).isVisible(WindowInsetsCompat.Type.ime())) {
|
||||
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (inputMethodManager != null && getCurrentFocus() != null) {
|
||||
inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onPreferencesResult() {
|
||||
// refresh the entire entry list if needed
|
||||
if (_loaded) {
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private void startEditEntryActivity() {
|
||||
String clip = ClipboardUtils.readText(this);
|
||||
if (clip != null) {
|
||||
GoogleAuthInfo parsed;
|
||||
try {
|
||||
parsed = GoogleAuthInfo.parseUri(clip.trim());
|
||||
String message = getString(
|
||||
R.string.import_from_clipboard_message,
|
||||
parsed.getAccountName(),
|
||||
parsed.getIssuer()
|
||||
);
|
||||
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.import_from_clipboard_title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> startEditEntryActivityForNew(new VaultEntry(parsed)))
|
||||
.setNegativeButton(R.string.no, (dialog, which) -> startEditEntryActivityForManual())
|
||||
.create());
|
||||
|
||||
return;
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Log.i("EntryActivity", "Clipboard did not contain a valid otpauth URI", e);
|
||||
}
|
||||
}
|
||||
|
||||
startEditEntryActivityForManual();
|
||||
}
|
||||
|
||||
private void startEditEntryActivityForNew(VaultEntry entry) {
|
||||
Intent intent = new Intent(this, EditEntryActivity.class);
|
||||
intent.putExtra("newEntry", entry);
|
||||
@@ -349,6 +530,76 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
assignIconsResultLauncher.launch(assignIconIntent);
|
||||
}
|
||||
|
||||
private void startAssignGroupsDialog() {
|
||||
View view = LayoutInflater.from(this).inflate(R.layout.dialog_select_group, null);
|
||||
TextInputLayout groupSelectionLayout = view.findViewById(R.id.group_selection_layout);
|
||||
AutoCompleteTextView groupsSelection = view.findViewById(R.id.group_selection_dropdown);
|
||||
TextInputLayout newGroupLayout = view.findViewById(R.id.text_group_name_layout);
|
||||
TextInputEditText newGroupText = view.findViewById(R.id.text_group_name);
|
||||
|
||||
Collection<VaultGroup> groups = _vaultManager.getVault().getUsedGroups();
|
||||
List<VaultGroupModel> groupModels = new ArrayList<>();
|
||||
groupModels.add(new VaultGroupModel(this, GroupPlaceholderType.NEW_GROUP));
|
||||
groupModels.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
|
||||
DropdownHelper.fillDropdown(this, groupsSelection, groupModels);
|
||||
|
||||
AtomicReference<VaultGroupModel> groupModelRef = new AtomicReference<>();
|
||||
groupsSelection.setOnItemClickListener((parent, view1, position, id) -> {
|
||||
VaultGroupModel groupModel = (VaultGroupModel) parent.getItemAtPosition(position);
|
||||
groupModelRef.set(groupModel);
|
||||
|
||||
if (groupModel.isPlaceholder()) {
|
||||
newGroupLayout.setVisibility(View.VISIBLE);
|
||||
newGroupText.requestFocus();
|
||||
} else {
|
||||
newGroupLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
groupSelectionLayout.setError(null);
|
||||
});
|
||||
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.assign_groups)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
btnPos.setOnClickListener(v -> {
|
||||
VaultGroupModel groupModel = groupModelRef.get();
|
||||
if (groupModel == null) {
|
||||
groupSelectionLayout.setError(getString(R.string.error_required_field));
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupModel.isPlaceholder()) {
|
||||
String newGroupName = newGroupText.getText().toString().trim();
|
||||
if (newGroupName.isEmpty()) {
|
||||
newGroupLayout.setError(getString(R.string.error_required_field));
|
||||
return;
|
||||
}
|
||||
|
||||
VaultGroup group = new VaultGroup(newGroupName);
|
||||
_vaultManager.getVault().addGroup(group);
|
||||
groupModel = new VaultGroupModel(group);
|
||||
}
|
||||
|
||||
for (VaultEntry selectedEntry : _selectedEntries) {
|
||||
selectedEntry.addGroup(groupModel.getUUID());
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
saveAndBackupVault();
|
||||
_actionMode.finish();
|
||||
setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
});
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private void startIntroActivity() {
|
||||
if (!_isDoingIntro) {
|
||||
Intent intro = new Intent(this, IntroActivity.class);
|
||||
@@ -368,31 +619,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
if (_loaded) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
|
||||
_entryListView.addEntry(entry, true);
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
_entryListView.onEntryAdded(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditEntryResult(Intent data) {
|
||||
private void onEditEntryResult() {
|
||||
if (_loaded) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
_entryListView.removeEntry(entryUUID);
|
||||
} else {
|
||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
|
||||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
}
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
}
|
||||
}
|
||||
|
||||
private void onAssignEntriesResult(Intent data) {
|
||||
private void onAssignIconsResult() {
|
||||
if (_loaded) {
|
||||
ArrayList<UUID> entryUUIDs = (ArrayList<UUID>) data.getSerializableExtra("entryUUIDs");
|
||||
|
||||
for (UUID entryUUID: entryUUIDs) {
|
||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
|
||||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
}
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,23 +732,25 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
} else if (entries.size() > 1) {
|
||||
for (VaultEntry entry: entries) {
|
||||
_vaultManager.getVault().addEntry(entry);
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
|
||||
if (saveAndBackupVault()) {
|
||||
Toast.makeText(this, getResources().getQuantityString(R.plurals.added_new_entries, entries.size(), entries.size()), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSortCategoryMenu() {
|
||||
SortCategory category = _prefs.getCurrentSortCategory();
|
||||
_menu.findItem(category.getMenuItem()).setChecked(true);
|
||||
if (_menu != null) {
|
||||
SortCategory category = _prefs.getCurrentSortCategory();
|
||||
_menu.findItem(category.getMenuItem()).setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onIntroResult() {
|
||||
loadEntries();
|
||||
checkTimeSyncSetting();
|
||||
}
|
||||
|
||||
private void checkTimeSyncSetting() {
|
||||
@@ -521,11 +763,41 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private void checkIconOptimization() {
|
||||
if (!_vaultManager.getVault().areIconsOptimized()) {
|
||||
Map<UUID, VaultEntryIcon> oldIcons = _vaultManager.getVault().getEntries().stream()
|
||||
.filter(e -> e.getIcon() != null
|
||||
&& !e.getIcon().getType().equals(IconType.SVG)
|
||||
&& !BitmapHelper.isVaultEntryIconOptimized(e.getIcon()))
|
||||
.collect(Collectors.toMap(VaultEntry::getUUID, VaultEntry::getIcon));
|
||||
|
||||
if (!oldIcons.isEmpty()) {
|
||||
IconOptimizationTask task = new IconOptimizationTask(this, this::onIconsOptimized);
|
||||
task.execute(getLifecycle(), oldIcons);
|
||||
} else {
|
||||
onIconsOptimized(Collections.emptyMap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onIconsOptimized(Map<UUID, VaultEntryIcon> newIcons) {
|
||||
for (Map.Entry<UUID, VaultEntryIcon> mapEntry : newIcons.entrySet()) {
|
||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(mapEntry.getKey());
|
||||
entry.setIcon(mapEntry.getValue());
|
||||
}
|
||||
|
||||
_vaultManager.getVault().setIconsOptimized(true);
|
||||
saveAndBackupVault();
|
||||
|
||||
if (!newIcons.isEmpty()) {
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
}
|
||||
}
|
||||
|
||||
private void onDecryptResult() {
|
||||
_auditLogRepository.addVaultUnlockedEvent();
|
||||
|
||||
loadEntries();
|
||||
checkTimeSyncSetting();
|
||||
}
|
||||
|
||||
private void startScanActivity() {
|
||||
@@ -657,8 +929,15 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
_entryListView.onRefreshStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
if (_vaultManager.isVaultInitNeeded()) {
|
||||
if (_prefs.isIntroDone()) {
|
||||
@@ -698,7 +977,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
startAuthActivity(false);
|
||||
} else if (_loaded) {
|
||||
// update the list of groups in the entry list view so that the chip gets updated
|
||||
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
|
||||
// update the usage counts in case they are edited outside of the EntryListView
|
||||
_entryListView.setUsageCounts(_prefs.getUsageCounts());
|
||||
@@ -707,43 +986,36 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
// refresh all codes to prevent showing old ones
|
||||
_entryListView.refresh(false);
|
||||
|
||||
_entryListView.onRefreshStart();
|
||||
} else {
|
||||
loadEntries();
|
||||
checkTimeSyncSetting();
|
||||
checkIconOptimization();
|
||||
|
||||
_entryListView.onRefreshStart();
|
||||
}
|
||||
|
||||
_lockBackPressHandler.setEnabled(
|
||||
_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)
|
||||
_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)
|
||||
);
|
||||
|
||||
handleIncomingIntent();
|
||||
updateLockIcon();
|
||||
updateSortCategoryMenu();
|
||||
doShortcutActions();
|
||||
updateErrorCard();
|
||||
}
|
||||
|
||||
private void deleteEntries(List<VaultEntry> entries) {
|
||||
for (VaultEntry entry: entries) {
|
||||
VaultEntry oldEntry = _vaultManager.getVault().removeEntry(entry);
|
||||
_entryListView.removeEntry(oldEntry);
|
||||
}
|
||||
|
||||
saveAndBackupVault();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
_menu = menu;
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
|
||||
updateLockIcon();
|
||||
if (_loaded) {
|
||||
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
updateSortCategoryMenu();
|
||||
}
|
||||
updateSortCategoryMenu();
|
||||
|
||||
MenuItem searchViewMenuItem = menu.findItem(R.id.mi_search);
|
||||
|
||||
_searchView = (SearchView) searchViewMenuItem.getActionView();
|
||||
_searchView.setMaxWidth(Integer.MAX_VALUE);
|
||||
_searchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> {
|
||||
@@ -753,6 +1025,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_searchView.setOnCloseListener(() -> {
|
||||
boolean enabled = _submittedSearchQuery != null;
|
||||
_searchViewBackPressHandler.setEnabled(enabled);
|
||||
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -786,6 +1059,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
});
|
||||
_searchView.setOnSearchClickListener(v -> {
|
||||
String query = _submittedSearchQuery != null ? _submittedSearchQuery : _pendingSearchQuery;
|
||||
_groupChip.setVisibility(View.GONE);
|
||||
_searchView.setQuery(query, false);
|
||||
});
|
||||
|
||||
@@ -851,16 +1125,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
private void collapseSearchView() {
|
||||
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
_searchView.setQuery(null, false);
|
||||
_searchView.setIconified(true);
|
||||
}
|
||||
|
||||
private void loadEntries() {
|
||||
if (!_loaded) {
|
||||
setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
_entryListView.setUsageCounts(_prefs.getUsageCounts());
|
||||
_entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps());
|
||||
_entryListView.addEntries(_vaultManager.getVault().getEntries());
|
||||
_entryListView.runEntriesAnimation();
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
if (!_isRecreated) {
|
||||
_entryListView.runEntriesAnimation();
|
||||
}
|
||||
_loaded = true;
|
||||
}
|
||||
}
|
||||
@@ -948,6 +1226,19 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(@Nullable Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
if (savedInstanceState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<UUID> filter = (HashSet<UUID>) savedInstanceState.getSerializable("prefGroupFilter");
|
||||
if (filter != null) {
|
||||
_prefGroupFilter = filter;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryClick(VaultEntry entry) {
|
||||
if (_actionMode != null) {
|
||||
@@ -1061,6 +1352,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
if (_searchView != null && !_searchView.isIconified()) {
|
||||
collapseSearchView();
|
||||
}
|
||||
if (_fabMenuHelper != null && _fabMenuHelper.isOpen()) {
|
||||
_fabMenuHelper.close();
|
||||
}
|
||||
|
||||
_entryListView.clearEntries();
|
||||
_loaded = false;
|
||||
@@ -1072,6 +1366,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean saveAndBackupVault() {
|
||||
boolean res = super.saveAndBackupVault();
|
||||
updateErrorCard();
|
||||
return res;
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private void copyEntryCode(VaultEntry entry) {
|
||||
String otp;
|
||||
@@ -1139,6 +1440,19 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private class FabMenuBackPressHandler extends OnBackPressedCallback {
|
||||
public FabMenuBackPressHandler() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (_fabMenuHelper != null && _fabMenuHelper.isOpen()) {
|
||||
_fabMenuHelper.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ActionModeCallbacks implements ActionMode.Callback {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
@@ -1168,12 +1482,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
mode.finish();
|
||||
} else if (itemId == R.id.action_toggle_favorite) {
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
entry.setIsFavorite(!entry.isFavorite());
|
||||
_entryListView.replaceEntry(entry.getUUID(), entry);
|
||||
_vaultManager.getVault().editEntry(entry, newEntry -> {
|
||||
newEntry.setIsFavorite(!newEntry.isFavorite());
|
||||
});
|
||||
}
|
||||
_entryListView.refresh(true);
|
||||
|
||||
saveAndBackupVault();
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
mode.finish();
|
||||
} else if (itemId == R.id.action_share_qr) {
|
||||
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
|
||||
@@ -1191,8 +1506,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
mode.finish();
|
||||
} else if (itemId == R.id.action_delete) {
|
||||
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
|
||||
deleteEntries(_selectedEntries);
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
_vaultManager.getVault().removeEntry(entry);
|
||||
}
|
||||
saveAndBackupVault();
|
||||
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
mode.finish();
|
||||
});
|
||||
} else if (itemId == R.id.action_select_all) {
|
||||
@@ -1202,6 +1521,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
} else if (itemId == R.id.action_assign_icons) {
|
||||
startAssignIconsActivity(_selectedEntries);
|
||||
mode.finish();
|
||||
} else if (itemId == R.id.action_assign_groups) {
|
||||
startAssignGroupsDialog();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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 {
|
||||
@@ -27,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);
|
||||
|
||||
@@ -57,24 +59,8 @@ public class PreferencesActivity extends AegisActivity implements
|
||||
}
|
||||
}
|
||||
|
||||
@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"));
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<>();
|
||||
@@ -87,11 +88,6 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
_themeHelper.setTheme(ThemeMap.FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (_executor != null) {
|
||||
|
||||
@@ -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 (_themeHelper.getConfiguredTheme() == Theme.LIGHT) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
getTheme().resolveAttribute(androidx.appcompat.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;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.beemdevelopment.aegis.ui.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
public class NoAutofillEditText extends TextInputEditText {
|
||||
|
||||
public NoAutofillEditText(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public NoAutofillEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public NoAutofillEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAutofillType() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
return View.AUTOFILL_TYPE_NONE;
|
||||
} else {
|
||||
return super.getAutofillType();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ 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;
|
||||
|
||||
@@ -27,6 +29,7 @@ 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;
|
||||
@@ -244,8 +247,8 @@ public class Dialogs {
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret, null);
|
||||
}
|
||||
|
||||
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, @Nullable DialogInterface.OnCancelListener onCancel) {
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, onCancel, false, null);
|
||||
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, null, false, null);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, TextInputListener listener) {
|
||||
@@ -318,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 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);
|
||||
@@ -547,7 +557,7 @@ public class Dialogs {
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(context.getString(R.string.import_partial_export_anyway, entries), (dialog, which) -> {
|
||||
.setPositiveButton(context.getResources().getQuantityString(R.plurals.import_partial_export_anyway, entries, entries), (dialog, which) -> {
|
||||
dismissHandler.onClick(dialog, which);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
@@ -570,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()));
|
||||
@@ -598,4 +656,8 @@ public class Dialogs {
|
||||
public interface ImporterListener {
|
||||
void onImporterSelectionResult(DatabaseImporter.Definition definition);
|
||||
}
|
||||
|
||||
public interface BackupsVersioningStrategyListener {
|
||||
void onStrategySelectionResult(BackupsVersioningStrategy strategy);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-26
@@ -27,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");
|
||||
@@ -62,7 +61,6 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
requireActivity().recreate();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
@@ -74,13 +72,12 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
Preference dynamicColorsPreference = requirePreference("pref_dynamic_colors");
|
||||
dynamicColorsPreference.setEnabled(DynamicColors.isDynamicColorAvailable());
|
||||
dynamicColorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
requireActivity().recreate();
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference langPreference = requirePreference("pref_lang");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
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);
|
||||
@@ -96,7 +93,6 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
requireActivity().recreate();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
@@ -120,8 +116,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
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)
|
||||
@@ -130,6 +125,11 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
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));
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -143,19 +143,12 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
_prefs.setCodeGroupSize(Preferences.CodeGrouping.valueOf(codeGroupings[newCodeGroupingIndex]));
|
||||
|
||||
dialog.dismiss();
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create());
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference onlyShowNecessaryAccountNames = requirePreference("pref_shared_issuer_account_name");
|
||||
onlyShowNecessaryAccountNames.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
int currentAccountNamePosition = _prefs.getAccountNamePosition().ordinal();
|
||||
_currentAccountNamePositionPreference = requirePreference("pref_account_name_position");
|
||||
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[currentAccountNamePosition]));
|
||||
@@ -168,7 +161,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
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)
|
||||
@@ -177,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()]));
|
||||
}
|
||||
}
|
||||
|
||||
+18
-4
@@ -6,10 +6,11 @@ import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
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.lifecycle.Observer;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -60,14 +61,27 @@ public class AuditLogPreferencesFragment extends Fragment {
|
||||
_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) {
|
||||
referencedEntry = _vaultManager.getVault().getEntryByUUID(UUID.fromString(entry.getReference()));
|
||||
UUID referencedEntryUUID = UUID.fromString(entry.getReference());
|
||||
if (_vaultManager.getVault().hasEntryByUUID(referencedEntryUUID)) {
|
||||
referencedEntry = _vaultManager.getVault().getEntryByUUID(referencedEntryUUID);
|
||||
}
|
||||
}
|
||||
|
||||
AuditLogEntryModel auditLogEntryModel = new AuditLogEntryModel(entry, referencedEntry);
|
||||
|
||||
+83
-12
@@ -17,9 +17,11 @@ 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;
|
||||
|
||||
@@ -27,6 +29,7 @@ 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;
|
||||
@@ -52,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");
|
||||
@@ -76,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();
|
||||
@@ -101,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);
|
||||
@@ -111,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;
|
||||
});
|
||||
|
||||
@@ -131,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;
|
||||
});
|
||||
@@ -160,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() {
|
||||
@@ -176,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());
|
||||
}
|
||||
@@ -223,6 +252,14 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
||||
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
|
||||
@@ -242,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+68
-11
@@ -1,23 +1,68 @@
|
||||
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]));
|
||||
@@ -30,7 +75,6 @@ public class BehaviorPreferencesFragment extends PreferencesFragment {
|
||||
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)
|
||||
@@ -39,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());
|
||||
}
|
||||
}
|
||||
|
||||
+18
-9
@@ -6,6 +6,7 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup.MarginLayoutParams;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
@@ -14,6 +15,9 @@ import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -67,6 +71,19 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
|
||||
fab.setOnClickListener(v -> startImportIconPack());
|
||||
_fabScrollHelper = new FabScrollHelper(fab);
|
||||
|
||||
final MarginLayoutParams fabInitialMargin = (MarginLayoutParams) fab.getLayoutParams();
|
||||
ViewCompat.setOnApplyWindowInsetsListener(fab, (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
|
||||
MarginLayoutParams marginParams = (MarginLayoutParams) targetView.getLayoutParams();
|
||||
marginParams.leftMargin = fabInitialMargin.leftMargin + insets.left;
|
||||
marginParams.bottomMargin = fabInitialMargin.bottomMargin + insets.bottom;
|
||||
marginParams.rightMargin = fabInitialMargin.rightMargin + insets.right;
|
||||
targetView.setLayoutParams(marginParams);
|
||||
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
_noIconPacksView = view.findViewById(R.id.vEmptyList);
|
||||
((TextView) view.findViewById(R.id.txt_no_icon_packs)).setMovementMethod(LinkMovementMethod.getInstance());
|
||||
_adapter = new IconPackAdapter(this);
|
||||
@@ -107,15 +124,7 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
|
||||
.setMessage(R.string.remove_icon_pack_description)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
try {
|
||||
_iconPackManager.removeIconPack(pack);
|
||||
} catch (IconPackException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(requireContext(), R.string.icon_pack_delete_error, e);
|
||||
return;
|
||||
}
|
||||
_adapter.removeIconPack(pack);
|
||||
updateEmptyState();
|
||||
removeIconPack(pack);
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
|
||||
+7
-13
@@ -22,8 +22,8 @@ import androidx.core.content.FileProvider;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.GroupPlaceholderType;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.database.AuditLogRepository;
|
||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
@@ -64,18 +64,12 @@ import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
// keep a reference to the type of database converter that was selected
|
||||
private DatabaseImporter.Definition _importerDef;
|
||||
private Vault.EntryFilter _exportFilter;
|
||||
|
||||
private final ActivityResultLauncher<Intent> importResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> importSelectResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
Intent data = activityResult.getData();
|
||||
@@ -102,7 +96,6 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
addPreferencesFromResource(R.xml.preferences_import_export);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
@@ -169,7 +162,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
Intent intent = new Intent(requireContext(), ImportEntriesActivity.class);
|
||||
intent.putExtra("importerDef", importerDef);
|
||||
intent.putExtra("file", file);
|
||||
importResultLauncher.launch(intent);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void startExport() {
|
||||
@@ -198,7 +191,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
checkBoxExportAllGroups.setVisibility(View.VISIBLE);
|
||||
|
||||
ArrayList<VaultGroupModel> groupsArray = new ArrayList<>();
|
||||
groupsArray.add(new VaultGroupModel(getString(R.string.no_group)));
|
||||
groupsArray.add(new VaultGroupModel(requireContext(), GroupPlaceholderType.NO_GROUP));
|
||||
groupsArray.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
|
||||
|
||||
groupsSelection.setCheckedItemsCountTextRes(R.plurals.export_groups_selected_count);
|
||||
@@ -303,7 +296,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
boolean encrypt = checkBoxEncrypt.isChecked();
|
||||
try {
|
||||
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, encrypt);
|
||||
file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir());
|
||||
file = new File(getExportCacheDir(), fileInfo.toString());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(requireContext(), R.string.exporting_vault_error, e);
|
||||
@@ -525,11 +518,10 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
file = File.createTempFile(VaultRepository.FILENAME_PREFIX_EXPORT + "-", ".json", getExportCacheDir());
|
||||
outStream = new FileOutputStream(file);
|
||||
cb.exportVault(outStream);
|
||||
|
||||
new ExportTask(requireContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri));
|
||||
} catch (VaultRepositoryException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(requireContext(), R.string.exporting_vault_error, e);
|
||||
return;
|
||||
} finally {
|
||||
try {
|
||||
if (outStream != null) {
|
||||
@@ -539,6 +531,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
new ExportTask(requireContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri));
|
||||
}, _exportFilter);
|
||||
_exportFilter = null;
|
||||
}
|
||||
|
||||
-1
@@ -7,7 +7,6 @@ import com.beemdevelopment.aegis.R;
|
||||
public class MainPreferencesFragment extends PreferencesFragment {
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
}
|
||||
}
|
||||
|
||||
+21
-16
@@ -1,15 +1,20 @@
|
||||
package com.beemdevelopment.aegis.ui.fragments.preferences;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
@@ -31,8 +36,6 @@ public abstract class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
public static final int CODE_EXPORT_GOOGLE_URI = 7;
|
||||
public static final int CODE_EXPORT_HTML = 8;
|
||||
|
||||
private Intent _result;
|
||||
|
||||
@Inject
|
||||
Preferences _prefs;
|
||||
|
||||
@@ -42,12 +45,6 @@ public abstract class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
@Inject
|
||||
protected AuditLogRepository _auditLogRepository;
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setResult(new Intent());
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onResume() {
|
||||
@@ -61,10 +58,6 @@ public abstract class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
}
|
||||
|
||||
public Intent getResult() {
|
||||
return _result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
|
||||
@@ -75,9 +68,21 @@ public abstract class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return super.onCreateAnimation(transit, enter, nextAnim);
|
||||
}
|
||||
|
||||
public void setResult(Intent result) {
|
||||
_result = result;
|
||||
requireActivity().setResult(Activity.RESULT_OK, _result);
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView onCreateRecyclerView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent, @Nullable Bundle savedInstanceState) {
|
||||
RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(recyclerView, (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
targetView.setPadding(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
insets.bottom
|
||||
);
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
return recyclerView;
|
||||
}
|
||||
|
||||
protected boolean saveAndBackupVault() {
|
||||
|
||||
-9
@@ -55,18 +55,10 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
addPreferencesFromResource(R.xml.preferences_security);
|
||||
|
||||
Preference tapToRevealPreference = requirePreference("pref_tap_to_reveal");
|
||||
tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference screenPreference = requirePreference("pref_secure_screen");
|
||||
screenPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
Window window = requireActivity().getWindow();
|
||||
if ((boolean) newValue) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
@@ -82,7 +74,6 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
|
||||
Dialogs.showTapToRevealTimeoutPickerDialog(requireContext(), _prefs.getTapToRevealTime(), number -> {
|
||||
_prefs.setTapToRevealTime(number);
|
||||
tapToRevealTimePreference.setSummary(number + " seconds");
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -2,6 +2,10 @@ package com.beemdevelopment.aegis.ui.models;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ErrorCardInfo {
|
||||
private final String _message;
|
||||
private final View.OnClickListener _listener;
|
||||
@@ -18,4 +22,23 @@ public class ErrorCardInfo {
|
||||
public View.OnClickListener getListener() {
|
||||
return _listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return HashCode.fromString(_message).asInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof ErrorCardInfo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This equality check purposefully ignores the onclick listener
|
||||
ErrorCardInfo info = (ErrorCardInfo) o;
|
||||
return Objects.equals(getMessage(), info.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
package com.beemdevelopment.aegis.ui.models;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.GroupPlaceholderType;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
public class VaultGroupModel implements Serializable {
|
||||
private final VaultGroup _group;
|
||||
private final String _placeholderName;
|
||||
private final GroupPlaceholderType _placeholderType;
|
||||
private final String _placeholderText;
|
||||
|
||||
public VaultGroupModel(VaultGroup group) {
|
||||
_group = group;
|
||||
_placeholderName = null;
|
||||
_placeholderText = null;
|
||||
_placeholderType = null;
|
||||
}
|
||||
|
||||
public VaultGroupModel(String placeholderName) {
|
||||
public VaultGroupModel(Context context, GroupPlaceholderType placeholderType) {
|
||||
_group = null;
|
||||
_placeholderName = placeholderName;
|
||||
_placeholderType = placeholderType;
|
||||
_placeholderText = context.getString(placeholderType.getStringRes());
|
||||
}
|
||||
|
||||
public VaultGroup getGroup() {
|
||||
@@ -25,11 +33,15 @@ public class VaultGroupModel implements Serializable {
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _group != null ? _group.getName() : _placeholderName;
|
||||
return _group != null ? _group.getName() : _placeholderText;
|
||||
}
|
||||
|
||||
public GroupPlaceholderType getPlaceholderType() {
|
||||
return _placeholderType;
|
||||
}
|
||||
|
||||
public boolean isPlaceholder() {
|
||||
return _group == null;
|
||||
return _placeholderType != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package com.beemdevelopment.aegis.ui.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
public class SwitchPreference extends SwitchPreferenceCompat {
|
||||
private Preference.OnPreferenceChangeListener _listener;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ public class ExportTask extends ProgressDialogTask<ExportTask.Params, Exception>
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
return e;
|
||||
} finally {
|
||||
boolean ignored = params.getFile().delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.beemdevelopment.aegis.ui.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.BitmapHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class IconOptimizationTask extends ProgressDialogTask<Map<UUID, VaultEntryIcon>, Map<UUID, VaultEntryIcon>> {
|
||||
private final Callback _cb;
|
||||
|
||||
public IconOptimizationTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.optimizing_icon));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<UUID, VaultEntryIcon> doInBackground(Map<UUID, VaultEntryIcon>... params) {
|
||||
Map<UUID, VaultEntryIcon> res = new HashMap<>();
|
||||
Context context = getDialog().getContext();
|
||||
|
||||
int i = 0;
|
||||
Map<UUID, VaultEntryIcon> icons = params[0];
|
||||
for (Map.Entry<UUID, VaultEntryIcon> entry : icons.entrySet()) {
|
||||
if (icons.size() > 1) {
|
||||
publishProgress(context.getString(R.string.optimizing_icon_multiple, i + 1, icons.size()));
|
||||
}
|
||||
i++;
|
||||
|
||||
VaultEntryIcon oldIcon = entry.getValue();
|
||||
if (oldIcon == null || oldIcon.getType().equals(IconType.SVG)) {
|
||||
continue;
|
||||
}
|
||||
if (BitmapHelper.isVaultEntryIconOptimized(oldIcon)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(oldIcon.getBytes(), 0, oldIcon.getBytes().length);
|
||||
VaultEntryIcon newIcon = BitmapHelper.toVaultEntryIcon(bitmap, oldIcon.getType());
|
||||
bitmap.recycle();
|
||||
res.put(entry.getKey(), newIcon);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Map<UUID, VaultEntryIcon> results) {
|
||||
super.onPostExecute(results);
|
||||
_cb.onTaskFinished(results);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(Map<UUID, VaultEntryIcon> results);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,11 @@ package com.beemdevelopment.aegis.ui.tasks;
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
|
||||
import org.bouncycastle.crypto.digests.SHA512Digest;
|
||||
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
@@ -31,6 +36,15 @@ public class PBKDFTask extends ProgressDialogTask<PBKDFTask.Params, SecretKey> {
|
||||
|
||||
public static SecretKey deriveKey(Params params) {
|
||||
try {
|
||||
// Some older versions of Android (< 26) do not support PBKDF2withHmacSHA512, so use
|
||||
// BouncyCastle's implementation instead.
|
||||
if (params.getAlgorithm().equals("PBKDF2withHmacSHA512")) {
|
||||
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
|
||||
gen.init(CryptoUtils.toBytes(params.getPassword()), params.getSalt(), params.getIterations());
|
||||
byte[] key = ((KeyParameter) gen.generateDerivedParameters(params.getKeySize())).getKey();
|
||||
return new SecretKeySpec(key, "AES");
|
||||
}
|
||||
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance(params.getAlgorithm());
|
||||
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), params.getKeySize());
|
||||
SecretKey key = factory.generateSecret(spec);
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
public class AuditLogHolder extends RecyclerView.ViewHolder {
|
||||
@@ -60,6 +61,10 @@ public class AuditLogHolder extends RecyclerView.ViewHolder {
|
||||
if (auditLogEntryModel.getReferencedVaultEntry() != null) {
|
||||
VaultEntry referencedVaultEntry = auditLogEntryModel.getReferencedVaultEntry();
|
||||
_auditLogEntryReference.setText(String.format("%s (%s)", referencedVaultEntry.getIssuer(), referencedVaultEntry.getName()));
|
||||
_auditLogEntryReference.setVisibility(View.VISIBLE);
|
||||
} else if (auditLogEntryModel.getAuditLogEntry().getReference() != null) {
|
||||
_auditLogEntryReference.setText(R.string.audit_log_entry_deleted);
|
||||
_auditLogEntryReference.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
_auditLogEntryReference.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -133,7 +138,7 @@ public class AuditLogHolder extends RecyclerView.ViewHolder {
|
||||
String formattedTime = timestamp.format(timeFormatter);
|
||||
return context.getString(R.string.day_of_week_at_time, dayOfWeek, formattedTime);
|
||||
} else {
|
||||
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(context.getString(R.string.date_format));
|
||||
DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
|
||||
return timestamp.format(dateFormatter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.AccountNamePosition;
|
||||
@@ -32,8 +34,10 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
|
||||
import com.beemdevelopment.aegis.util.CollectionUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@@ -48,9 +52,9 @@ import java.util.UUID;
|
||||
|
||||
public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ItemTouchHelperAdapter {
|
||||
private EntryListView _view;
|
||||
private List<VaultEntry> _entries;
|
||||
private List<VaultEntry> _shownEntries;
|
||||
private EntryList _entryList;
|
||||
private List<VaultEntry> _selectedEntries;
|
||||
private Collection<VaultGroup> _groups;
|
||||
private Map<UUID, Integer> _usageCounts;
|
||||
private Map<UUID, Long> _lastUsedTimestamps;
|
||||
private VaultEntry _focusedEntry;
|
||||
@@ -58,12 +62,15 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
private Preferences.CodeGrouping _codeGroupSize;
|
||||
private AccountNamePosition _accountNamePosition;
|
||||
private boolean _showIcon;
|
||||
private boolean _showNextCode;
|
||||
private boolean _showExpirationState;
|
||||
private boolean _onlyShowNecessaryAccountNames;
|
||||
private boolean _highlightEntry;
|
||||
private boolean _tempHighlightEntry;
|
||||
private boolean _tapToReveal;
|
||||
private int _tapToRevealTime;
|
||||
private CopyBehavior _copyBehavior;
|
||||
private int _searchBehaviorMask;
|
||||
private Set<UUID> _groupFilter;
|
||||
private SortCategory _sortCategory;
|
||||
private ViewMode _viewMode;
|
||||
@@ -73,14 +80,12 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
private Handler _dimHandler;
|
||||
private Handler _doubleTapHandler;
|
||||
private boolean _pauseFocused;
|
||||
private ErrorCardInfo _errorCardInfo;
|
||||
|
||||
// keeps track of the EntryHolders that are currently bound
|
||||
private List<EntryHolder> _holders;
|
||||
|
||||
public EntryAdapter(EntryListView view) {
|
||||
_entries = new ArrayList<>();
|
||||
_shownEntries = new ArrayList<>();
|
||||
_entryList = new EntryList();
|
||||
_selectedEntries = new ArrayList<>();
|
||||
_groupFilter = new TreeSet<>();
|
||||
_holders = new ArrayList<>();
|
||||
@@ -112,6 +117,14 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
_showIcon = showIcon;
|
||||
}
|
||||
|
||||
public void setShowNextCode(boolean showNextCode) {
|
||||
_showNextCode = showNextCode;
|
||||
}
|
||||
|
||||
public void setShowExpirationState(boolean showExpirationState) {
|
||||
_showExpirationState = showExpirationState;
|
||||
}
|
||||
|
||||
public void setTapToReveal(boolean tapToReveal) {
|
||||
_tapToReveal = tapToReveal;
|
||||
}
|
||||
@@ -130,184 +143,72 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
|
||||
public void setCopyBehavior(CopyBehavior copyBehavior) { _copyBehavior = copyBehavior; }
|
||||
|
||||
public void setSearchBehaviorMask(int searchBehaviorMask) { _searchBehaviorMask = searchBehaviorMask; }
|
||||
|
||||
public void setPauseFocused(boolean pauseFocused) {
|
||||
_pauseFocused = pauseFocused;
|
||||
}
|
||||
|
||||
public void setErrorCardInfo(ErrorCardInfo info) {
|
||||
ErrorCardInfo oldInfo = _errorCardInfo;
|
||||
_errorCardInfo = info;
|
||||
|
||||
if (oldInfo == null && info != null) {
|
||||
notifyItemInserted(0);
|
||||
} else if (oldInfo != null && info == null) {
|
||||
notifyItemRemoved(0);
|
||||
} else {
|
||||
notifyItemChanged(0);
|
||||
if (Objects.equals(info, _entryList.getErrorCardInfo())) {
|
||||
return;
|
||||
}
|
||||
|
||||
replaceEntryList(new EntryList(
|
||||
_entryList.getEntries(),
|
||||
_entryList.getShownEntries(),
|
||||
info
|
||||
));
|
||||
}
|
||||
|
||||
public VaultEntry getEntryAtPos(int position) {
|
||||
return _shownEntries.get(translateEntryPosToIndex(position));
|
||||
public VaultEntry getEntryAtPosition(int position) {
|
||||
return _entryList.getShownEntries().get(_entryList.translateEntryPosToIndex(position));
|
||||
}
|
||||
|
||||
public int addEntry(VaultEntry entry) {
|
||||
_entries.add(entry);
|
||||
if (isEntryFiltered(entry)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int position = -1;
|
||||
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
|
||||
if (comparator != null) {
|
||||
// insert the entry in the correct order
|
||||
// note: this assumes that _shownEntries has already been sorted
|
||||
for (int i = getShownFavoritesCount(); i < _shownEntries.size(); i++) {
|
||||
if (comparator.compare(_shownEntries.get(i), entry) > 0) {
|
||||
_shownEntries.add(i, entry);
|
||||
position = translateEntryIndexToPos(i);
|
||||
notifyItemInserted(position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (position < 0) {
|
||||
_shownEntries.add(entry);
|
||||
|
||||
position = translateEntryIndexToPos(getShownEntriesCount() - 1);
|
||||
if (position == 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
}
|
||||
|
||||
_view.onListChange();
|
||||
checkPeriodUniformity();
|
||||
updateFooter();
|
||||
return position;
|
||||
public int getEntryPosition(VaultEntry entry) {
|
||||
return _entryList.translateEntryIndexToPos(_entryList.getShownEntries().indexOf(entry));
|
||||
}
|
||||
|
||||
public void addEntries(Collection<VaultEntry> entries) {
|
||||
for (VaultEntry entry: entries) {
|
||||
public void setEntries(List<VaultEntry> entries) {
|
||||
// TODO: Move these fields to separate dedicated model for the UI
|
||||
for (VaultEntry entry : entries) {
|
||||
entry.setUsageCount(_usageCounts.containsKey(entry.getUUID()) ? _usageCounts.get(entry.getUUID()) : 0);
|
||||
entry.setLastUsedTimestamp(_lastUsedTimestamps.containsKey(entry.getUUID()) ? _lastUsedTimestamps.get(entry.getUUID()) : 0);
|
||||
}
|
||||
|
||||
_entries.addAll(entries);
|
||||
updateShownEntries();
|
||||
checkPeriodUniformity(true);
|
||||
}
|
||||
|
||||
public void removeEntry(VaultEntry entry) {
|
||||
_entries.remove(entry);
|
||||
|
||||
if (_shownEntries.contains(entry)) {
|
||||
int index = _shownEntries.indexOf(entry);
|
||||
_shownEntries.remove(index);
|
||||
|
||||
int position = translateEntryIndexToPos(index);
|
||||
notifyItemRemoved(position);
|
||||
|
||||
updateFooter();
|
||||
}
|
||||
|
||||
_view.onListChange();
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void removeEntry(UUID uuid) {
|
||||
VaultEntry entry = getEntryByUUID(uuid);
|
||||
removeEntry(entry);
|
||||
replaceEntryList(new EntryList(
|
||||
entries,
|
||||
calculateShownEntries(entries),
|
||||
_entryList.getErrorCardInfo()
|
||||
));
|
||||
}
|
||||
|
||||
public void clearEntries() {
|
||||
_entries.clear();
|
||||
_shownEntries.clear();
|
||||
notifyDataSetChanged();
|
||||
checkPeriodUniformity();
|
||||
replaceEntryList(new EntryList());
|
||||
}
|
||||
|
||||
public void replaceEntry(UUID uuid, VaultEntry newEntry) {
|
||||
VaultEntry oldEntry = getEntryByUUID(uuid);
|
||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||
|
||||
if (_shownEntries.contains(oldEntry)) {
|
||||
int index = _shownEntries.indexOf(oldEntry);
|
||||
int position = translateEntryIndexToPos(index);
|
||||
if (isEntryFiltered(newEntry)) {
|
||||
_shownEntries.remove(index);
|
||||
notifyItemRemoved(position);
|
||||
} else {
|
||||
_shownEntries.set(index, newEntry);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
sortShownEntries();
|
||||
int newIndex = _shownEntries.indexOf(newEntry);
|
||||
int newPosition = translateEntryIndexToPos(newIndex);
|
||||
if (newPosition != NO_POSITION && position != newPosition) {
|
||||
notifyItemMoved(position, newPosition);
|
||||
}
|
||||
} else if (!isEntryFiltered(newEntry)) {
|
||||
// NOTE: This logic is wrong, because sorting is not taken into account. This code
|
||||
// path is currently never hit though, because it is not possible to edit an entry
|
||||
// that is not shown.
|
||||
_shownEntries.add(newEntry);
|
||||
|
||||
int position = getItemCount() - 1;
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
|
||||
checkPeriodUniformity();
|
||||
updateFooter();
|
||||
}
|
||||
|
||||
private VaultEntry getEntryByUUID(UUID uuid) {
|
||||
for (VaultEntry entry : _entries) {
|
||||
if (entry.getUUID().equals(uuid)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given entry position in the recycler view, to its index in the shown entries list.
|
||||
*/
|
||||
public int translateEntryPosToIndex(int position) {
|
||||
if (position == NO_POSITION) {
|
||||
return NO_POSITION;
|
||||
}
|
||||
|
||||
if (isErrorCardShown()) {
|
||||
position -= 1;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given entry index in the shown entries list, to its position in the recycler view.
|
||||
*/
|
||||
private int translateEntryIndexToPos(int index) {
|
||||
if (index == NO_POSITION) {
|
||||
return NO_POSITION;
|
||||
}
|
||||
|
||||
if (isErrorCardShown()) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return index;
|
||||
return _entryList.translateEntryPosToIndex(position);
|
||||
}
|
||||
|
||||
private boolean isEntryFiltered(VaultEntry entry) {
|
||||
Set<UUID> groups = entry.getGroups();
|
||||
String issuer = entry.getIssuer().toLowerCase();
|
||||
String name = entry.getName().toLowerCase();
|
||||
String note = entry.getNote().toLowerCase();
|
||||
|
||||
if (_searchFilter != null) {
|
||||
String[] tokens = _searchFilter.toLowerCase().split("\\s+");
|
||||
|
||||
// Return true if not all tokens match at least one of the relevant fields
|
||||
return !Arrays.stream(tokens)
|
||||
.allMatch(token ->
|
||||
((_searchBehaviorMask & Preferences.SEARCH_IN_ISSUER) != 0 && issuer.contains(token)) ||
|
||||
((_searchBehaviorMask & Preferences.SEARCH_IN_NAME) != 0 && name.contains(token)) ||
|
||||
((_searchBehaviorMask & Preferences.SEARCH_IN_NOTE) != 0 && note.contains(token)) ||
|
||||
((_searchBehaviorMask & Preferences.SEARCH_IN_GROUPS) != 0 && doesAnyGroupMatchSearchFilter(groups, token))
|
||||
);
|
||||
}
|
||||
|
||||
if (!_groupFilter.isEmpty()) {
|
||||
if (groups.isEmpty() && !_groupFilter.contains(null)) {
|
||||
@@ -318,16 +219,19 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
}
|
||||
}
|
||||
|
||||
if (_searchFilter == null) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return !issuer.contains(_searchFilter) && !name.contains(_searchFilter);
|
||||
private boolean doesAnyGroupMatchSearchFilter(Set<UUID> entryGroupUUIDs, String searchFilter) {
|
||||
return _groups.stream()
|
||||
.filter(group -> entryGroupUUIDs.contains(group.getUUID()))
|
||||
.map(VaultGroup::getName)
|
||||
.anyMatch(groupName -> groupName.toLowerCase().contains(searchFilter.toLowerCase()));
|
||||
}
|
||||
|
||||
public void refresh(boolean hard) {
|
||||
if (hard) {
|
||||
updateShownEntries();
|
||||
refreshEntryList();
|
||||
} else {
|
||||
for (EntryHolder holder : _holders) {
|
||||
holder.refresh();
|
||||
@@ -342,8 +246,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
}
|
||||
|
||||
_groupFilter = groups;
|
||||
updateShownEntries();
|
||||
checkPeriodUniformity();
|
||||
refreshEntryList();
|
||||
}
|
||||
|
||||
public void setSortCategory(SortCategory category, boolean apply) {
|
||||
@@ -353,7 +256,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
|
||||
_sortCategory = category;
|
||||
if (apply) {
|
||||
updateShownEntries();
|
||||
refreshEntryList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,25 +265,59 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
}
|
||||
|
||||
public void setSearchFilter(String search) {
|
||||
_searchFilter = (search != null && !search.isEmpty()) ? search.toLowerCase() : null;
|
||||
updateShownEntries();
|
||||
String newSearchFilter = (search != null && !search.isEmpty())
|
||||
? search.toLowerCase().trim() : null;
|
||||
|
||||
if (!Objects.equals(_searchFilter, newSearchFilter)) {
|
||||
_searchFilter = newSearchFilter;
|
||||
refreshEntryList();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShownEntries() {
|
||||
// clear the list of shown entries first
|
||||
_shownEntries.clear();
|
||||
private void refreshEntryList() {
|
||||
replaceEntryList(new EntryList(
|
||||
_entryList.getEntries(),
|
||||
calculateShownEntries(_entryList.getEntries()),
|
||||
_entryList.getErrorCardInfo()
|
||||
));
|
||||
}
|
||||
|
||||
// add entries back that are not filtered out
|
||||
for (VaultEntry entry : _entries) {
|
||||
private void replaceEntryList(EntryList newEntryList) {
|
||||
DiffUtil.DiffResult diffRes = DiffUtil.calculateDiff(new DiffCallback(_entryList, newEntryList));
|
||||
_entryList = newEntryList;
|
||||
updatePeriodUniformity();
|
||||
|
||||
// This scroll position trick is required in order to not have the recycler view
|
||||
// jump to some random position after a large change (like resorting entries)
|
||||
// Related: https://issuetracker.google.com/issues/70149059
|
||||
int scrollPos = _view.getScrollPosition();
|
||||
diffRes.dispatchUpdatesTo(this);
|
||||
_view.scrollToPosition(scrollPos);
|
||||
_view.onListChange();
|
||||
}
|
||||
|
||||
private List<VaultEntry> calculateShownEntries(List<VaultEntry> entries) {
|
||||
List<VaultEntry> res = new ArrayList<>();
|
||||
for (VaultEntry entry : entries) {
|
||||
if (!isEntryFiltered(entry)) {
|
||||
_shownEntries.add(entry);
|
||||
res.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
sortShownEntries();
|
||||
checkPeriodUniformity();
|
||||
_view.onListChange();
|
||||
notifyDataSetChanged();
|
||||
sortEntries(res, _sortCategory);
|
||||
return res;
|
||||
}
|
||||
|
||||
private static void sortEntries(List<VaultEntry> entries, SortCategory sortCategory) {
|
||||
if (sortCategory != null) {
|
||||
Comparator<VaultEntry> comparator = sortCategory.getComparator();
|
||||
if (comparator != null) {
|
||||
Collections.sort(entries, comparator);
|
||||
}
|
||||
}
|
||||
|
||||
Comparator<VaultEntry> favoriteComparator = new FavoriteComparator();
|
||||
Collections.sort(entries, favoriteComparator);
|
||||
}
|
||||
|
||||
private boolean isEntryDraggable(VaultEntry entry) {
|
||||
@@ -391,18 +328,6 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
&& _selectedEntries.get(0) == entry;
|
||||
}
|
||||
|
||||
private void sortShownEntries() {
|
||||
if (_sortCategory != null) {
|
||||
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
|
||||
if (comparator != null) {
|
||||
Collections.sort(_shownEntries, comparator);
|
||||
}
|
||||
}
|
||||
|
||||
Comparator<VaultEntry> favoriteComparator = new FavoriteComparator();
|
||||
Collections.sort(_shownEntries, favoriteComparator);
|
||||
}
|
||||
|
||||
public void setViewMode(ViewMode viewMode) {
|
||||
_viewMode = viewMode;
|
||||
}
|
||||
@@ -411,12 +336,14 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
|
||||
public Map<UUID, Integer> getUsageCounts() { return _usageCounts; }
|
||||
|
||||
public void setGroups(Collection<VaultGroup> groups) { _groups = groups; }
|
||||
|
||||
public void setLastUsedTimestamps(Map<UUID, Long> lastUsedTimestamps) { _lastUsedTimestamps = lastUsedTimestamps; }
|
||||
|
||||
public Map<UUID, Long> getLastUsedTimestamps() { return _lastUsedTimestamps; }
|
||||
|
||||
public int getShownFavoritesCount() {
|
||||
return (int) _shownEntries.stream().filter(VaultEntry::isFavorite).count();
|
||||
return (int) _entryList.getShownEntries().stream().filter(VaultEntry::isFavorite).count();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -428,43 +355,48 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
public void onItemDrop(int position) {
|
||||
// moving entries is not allowed when a filter is applied
|
||||
// footer cant be moved, nor can items be moved below it
|
||||
if (!_groupFilter.isEmpty() || isPositionFooter(position) || isPositionErrorCard(position)) {
|
||||
if (!_groupFilter.isEmpty() || _entryList.isPositionFooter(position) || _entryList.isPositionErrorCard(position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int index = translateEntryPosToIndex(position);
|
||||
_view.onEntryDrop(_shownEntries.get(index));
|
||||
int index = _entryList.translateEntryPosToIndex(position);
|
||||
_view.onEntryDrop(_entryList.getShownEntries().get(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMove(int firstPosition, int secondPosition) {
|
||||
// moving entries is not allowed when a filter is applied
|
||||
// footer cant be moved, nor can items be moved below it
|
||||
// Moving entries is not allowed when a filter is applied. The footer can't be
|
||||
// moved, nor can items be moved below it
|
||||
if (!_groupFilter.isEmpty()
|
||||
|| isPositionFooter(firstPosition) || isPositionFooter(secondPosition)
|
||||
|| isPositionErrorCard(firstPosition) || isPositionErrorCard(secondPosition)) {
|
||||
|| _entryList.isPositionFooter(firstPosition) || _entryList.isPositionFooter(secondPosition)
|
||||
|| _entryList.isPositionErrorCard(firstPosition) || _entryList.isPositionErrorCard(secondPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// notify the vault first
|
||||
int firstIndex = translateEntryPosToIndex(firstPosition);
|
||||
int secondIndex = translateEntryPosToIndex(secondPosition);
|
||||
_view.onEntryMove(_entries.get(firstIndex), _entries.get(secondIndex));
|
||||
// Notify the vault about the entry position change first
|
||||
int firstIndex = _entryList.translateEntryPosToIndex(firstPosition);
|
||||
int secondIndex = _entryList.translateEntryPosToIndex(secondPosition);
|
||||
VaultEntry firstEntry = _entryList.getShownEntries().get(firstIndex);
|
||||
VaultEntry secondEntry = _entryList.getShownEntries().get(secondIndex);
|
||||
_view.onEntryMove(firstEntry, secondEntry);
|
||||
|
||||
// then update our end
|
||||
CollectionUtils.move(_entries, firstIndex, secondIndex);
|
||||
CollectionUtils.move(_shownEntries, firstIndex, secondIndex);
|
||||
|
||||
notifyItemMoved(firstPosition, secondPosition);
|
||||
// Then update the visual end
|
||||
List<VaultEntry> newEntries = new ArrayList<>(_entryList.getEntries());
|
||||
CollectionUtils.move(newEntries, newEntries.indexOf(firstEntry), newEntries.indexOf(secondEntry));
|
||||
replaceEntryList(new EntryList(
|
||||
newEntries,
|
||||
calculateShownEntries(newEntries),
|
||||
_entryList.getErrorCardInfo()
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (isPositionErrorCard(position)) {
|
||||
if (_entryList.isPositionErrorCard(position)) {
|
||||
return R.layout.card_error;
|
||||
}
|
||||
|
||||
if (isPositionFooter(position)) {
|
||||
if (_entryList.isPositionFooter(position)) {
|
||||
return R.layout.card_footer;
|
||||
}
|
||||
|
||||
@@ -479,7 +411,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
View view = inflater.inflate(viewType, parent, false);
|
||||
|
||||
if (viewType == R.layout.card_error) {
|
||||
holder = new ErrorCardHolder(view, _errorCardInfo);
|
||||
holder = new ErrorCardHolder(view, Objects.requireNonNull(_entryList.getErrorCardInfo()));
|
||||
} else if (viewType == R.layout.card_footer) {
|
||||
holder = new FooterView(view);
|
||||
} else {
|
||||
@@ -505,23 +437,23 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
|
||||
if (holder instanceof EntryHolder) {
|
||||
EntryHolder entryHolder = (EntryHolder) holder;
|
||||
int index = translateEntryPosToIndex(position);
|
||||
VaultEntry entry = _shownEntries.get(index);
|
||||
int index = _entryList.translateEntryPosToIndex(position);
|
||||
VaultEntry entry = _entryList.getShownEntries().get(index);
|
||||
|
||||
boolean hidden = _tapToReveal && entry != _focusedEntry;
|
||||
boolean paused = _pauseFocused && entry == _focusedEntry;
|
||||
boolean dimmed = (_highlightEntry || _tempHighlightEntry) && _focusedEntry != null && _focusedEntry != entry;
|
||||
boolean hidden = _tapToReveal && !entry.equals(_focusedEntry);
|
||||
boolean paused = _pauseFocused && entry.equals(_focusedEntry);
|
||||
boolean dimmed = (_highlightEntry || _tempHighlightEntry) && _focusedEntry != null && !_focusedEntry.equals(entry);
|
||||
boolean showProgress = entry.getInfo() instanceof TotpInfo && ((TotpInfo) entry.getInfo()).getPeriod() != getMostFrequentPeriod();
|
||||
boolean showAccountName = true;
|
||||
if (_onlyShowNecessaryAccountNames) {
|
||||
// Only show account name when there's multiple entries found with the same issuer.
|
||||
showAccountName = _entries.stream()
|
||||
showAccountName = _entryList.getEntries().stream()
|
||||
.filter(x -> x.getIssuer().equals(entry.getIssuer()))
|
||||
.count() > 1;
|
||||
}
|
||||
|
||||
AccountNamePosition accountNamePosition = showAccountName ? _accountNamePosition : AccountNamePosition.HIDDEN;
|
||||
entryHolder.setData(entry, _codeGroupSize, _viewMode, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed);
|
||||
entryHolder.setData(entry, _codeGroupSize, _viewMode, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed, _showExpirationState, _showNextCode);
|
||||
entryHolder.setFocused(_selectedEntries.contains(entry));
|
||||
entryHolder.setShowDragHandle(isEntryDraggable(entry));
|
||||
|
||||
@@ -536,13 +468,13 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
|
||||
if (_selectedEntries.isEmpty()) {
|
||||
if (_highlightEntry || _tempHighlightEntry || _tapToReveal) {
|
||||
if (_focusedEntry == entry) {
|
||||
if (_focusedEntry != null && _focusedEntry.equals(entry)) {
|
||||
resetFocus();
|
||||
|
||||
// Prevent copying when singletap is set and focus is reset
|
||||
handled = _copyBehavior == CopyBehavior.SINGLETAP;
|
||||
} else {
|
||||
focusEntry(entry, _tapToRevealTime);
|
||||
|
||||
// Prevent copying when singletap is set and the entry is being revealed
|
||||
handled = _copyBehavior == CopyBehavior.SINGLETAP && _tapToReveal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,7 +482,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
case SINGLETAP:
|
||||
if (!handled) {
|
||||
_view.onEntryCopy(entry);
|
||||
entryHolder.animateCopyText(_viewMode != ViewMode.TILES);
|
||||
entryHolder.animateCopyText();
|
||||
_clickedEntry = null;
|
||||
}
|
||||
break;
|
||||
@@ -559,7 +491,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
|
||||
if(entry == _clickedEntry) {
|
||||
_view.onEntryCopy(entry);
|
||||
entryHolder.animateCopyText(_viewMode != ViewMode.TILES);
|
||||
entryHolder.animateCopyText();
|
||||
_clickedEntry = null;
|
||||
} else {
|
||||
_clickedEntry = entry;
|
||||
@@ -593,8 +525,8 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
entryHolder.setFocusedAndAnimate(true);
|
||||
}
|
||||
|
||||
int index = translateEntryPosToIndex(position);
|
||||
boolean returnVal = _view.onLongEntryClick(_shownEntries.get(index));
|
||||
int index = _entryList.translateEntryPosToIndex(position);
|
||||
boolean returnVal = _view.onLongEntryClick(_entryList.getShownEntries().get(index));
|
||||
if (_selectedEntries.size() == 0 || isEntryDraggable(entry)) {
|
||||
_view.startDrag(entryHolder);
|
||||
}
|
||||
@@ -640,15 +572,10 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
}
|
||||
}
|
||||
|
||||
private void checkPeriodUniformity() {
|
||||
checkPeriodUniformity(false);
|
||||
}
|
||||
|
||||
private void checkPeriodUniformity(boolean force) {
|
||||
private void updatePeriodUniformity() {
|
||||
int mostFrequentPeriod = getMostFrequentPeriod();
|
||||
boolean uniform = isPeriodUniform();
|
||||
|
||||
if (!force && uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) {
|
||||
if (uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -666,7 +593,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
|
||||
public int getMostFrequentPeriod() {
|
||||
List<TotpInfo> infos = new ArrayList<>();
|
||||
for (VaultEntry entry : _shownEntries) {
|
||||
for (VaultEntry entry : _entryList.getShownEntries()) {
|
||||
OtpInfo info = entry.getInfo();
|
||||
if (info instanceof TotpInfo) {
|
||||
infos.add((TotpInfo) info);
|
||||
@@ -708,7 +635,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
_dimHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
for (EntryHolder holder : _holders) {
|
||||
if (holder.getEntry() != _focusedEntry) {
|
||||
if (!holder.getEntry().equals(_focusedEntry)) {
|
||||
if (_highlightEntry || _tempHighlightEntry) {
|
||||
holder.dim();
|
||||
}
|
||||
@@ -781,9 +708,9 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
public List<VaultEntry> selectAllEntries() {
|
||||
_selectedEntries.clear();
|
||||
|
||||
for (VaultEntry entry: _shownEntries) {
|
||||
for (VaultEntry entry: _entryList.getShownEntries()) {
|
||||
for (EntryHolder holder: _holders) {
|
||||
if (holder.getEntry() == entry) {
|
||||
if (holder.getEntry().equals(entry)) {
|
||||
holder.setFocused(true);
|
||||
}
|
||||
}
|
||||
@@ -798,7 +725,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
public void deselectAllEntries() {
|
||||
for (VaultEntry entry: _selectedEntries) {
|
||||
for (EntryHolder holder : _holders) {
|
||||
if (holder.getEntry() == entry) {
|
||||
if (holder.getEntry().equals(entry)) {
|
||||
holder.setFocusedAndAnimate(false);
|
||||
break;
|
||||
}
|
||||
@@ -835,34 +762,23 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
// Always at least one item because of the footer
|
||||
// Two in case there's also an error card
|
||||
int baseCount = 1;
|
||||
if (isErrorCardShown()) {
|
||||
baseCount++;
|
||||
}
|
||||
|
||||
return baseCount + getShownEntriesCount();
|
||||
return _entryList.getItemCount();
|
||||
}
|
||||
|
||||
public int getShownEntriesCount() {
|
||||
return _shownEntries.size();
|
||||
return _entryList.getShownEntries().size();
|
||||
}
|
||||
|
||||
public boolean isPositionFooter(int position) {
|
||||
return position == (getItemCount() - 1);
|
||||
return _entryList.isPositionFooter(position);
|
||||
}
|
||||
|
||||
public boolean isPositionErrorCard(int position) {
|
||||
return isErrorCardShown() && position == 0;
|
||||
return _entryList.isPositionErrorCard(position);
|
||||
}
|
||||
|
||||
public boolean isErrorCardShown() {
|
||||
return _errorCardInfo != null;
|
||||
}
|
||||
|
||||
private void updateFooter() {
|
||||
notifyItemChanged(getItemCount() - 1);
|
||||
return _entryList.isErrorCardShown();
|
||||
}
|
||||
|
||||
private class FooterView extends RecyclerView.ViewHolder {
|
||||
@@ -889,6 +805,151 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
}
|
||||
}
|
||||
|
||||
private static class EntryList {
|
||||
private final List<VaultEntry> _entries;
|
||||
private final List<VaultEntry> _shownEntries;
|
||||
private final ErrorCardInfo _errorCardInfo;
|
||||
|
||||
public EntryList() {
|
||||
this(new ArrayList<>(), new ArrayList<>(), null);
|
||||
}
|
||||
|
||||
public EntryList(
|
||||
@NonNull List<VaultEntry> entries,
|
||||
@NonNull List<VaultEntry> shownEntries,
|
||||
@Nullable ErrorCardInfo errorCardInfo
|
||||
) {
|
||||
_entries = entries;
|
||||
_shownEntries = shownEntries;
|
||||
_errorCardInfo = errorCardInfo;
|
||||
}
|
||||
|
||||
public List<VaultEntry> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
public List<VaultEntry> getShownEntries() {
|
||||
return _shownEntries;
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
// Always at least one item because of the footer
|
||||
// Two in case there's also an error card
|
||||
int baseCount = 1;
|
||||
if (isErrorCardShown()) {
|
||||
baseCount++;
|
||||
}
|
||||
|
||||
return baseCount + getShownEntries().size();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ErrorCardInfo getErrorCardInfo() {
|
||||
return _errorCardInfo;
|
||||
}
|
||||
|
||||
public boolean isErrorCardShown() {
|
||||
return _errorCardInfo != null;
|
||||
}
|
||||
|
||||
public boolean isPositionErrorCard(int position) {
|
||||
return isErrorCardShown() && position == 0;
|
||||
}
|
||||
|
||||
public boolean isPositionFooter(int position) {
|
||||
return position == (getItemCount() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given entry position in the recycler view, to its index in the shown entries list.
|
||||
*/
|
||||
public int translateEntryPosToIndex(int position) {
|
||||
if (position == NO_POSITION) {
|
||||
return NO_POSITION;
|
||||
}
|
||||
|
||||
if (isErrorCardShown()) {
|
||||
position -= 1;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given entry index in the shown entries list, to its position in the recycler view.
|
||||
*/
|
||||
public int translateEntryIndexToPos(int index) {
|
||||
if (index == NO_POSITION) {
|
||||
return NO_POSITION;
|
||||
}
|
||||
|
||||
if (isErrorCardShown()) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class DiffCallback extends DiffUtil.Callback {
|
||||
private final EntryList _old;
|
||||
private final EntryList _new;
|
||||
|
||||
public DiffCallback(EntryList oldList, EntryList newList) {
|
||||
_old = oldList;
|
||||
_new = newList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOldListSize() {
|
||||
return _old.getItemCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewListSize() {
|
||||
return _new.getItemCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
if (_old.isPositionErrorCard(oldItemPosition) != _new.isPositionErrorCard(newItemPosition)
|
||||
|| _old.isPositionFooter(oldItemPosition) != _new.isPositionFooter(newItemPosition)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((_old.isPositionFooter(oldItemPosition) && _new.isPositionFooter(newItemPosition))
|
||||
|| (_old.isPositionErrorCard(oldItemPosition) && _new.isPositionErrorCard(newItemPosition))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int oldEntryIndex = _old.translateEntryPosToIndex(oldItemPosition);
|
||||
int newEntryIndex = _new.translateEntryPosToIndex(newItemPosition);
|
||||
if (oldEntryIndex < 0 || newEntryIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _old.getShownEntries().get(oldEntryIndex).getUUID()
|
||||
.equals(_new.getShownEntries().get(newEntryIndex).getUUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
if (_old.isPositionFooter(oldItemPosition) && _new.isPositionFooter(newItemPosition)) {
|
||||
return _old.getShownEntries().size() == _new.getShownEntries().size();
|
||||
}
|
||||
|
||||
if (_old.isPositionErrorCard(oldItemPosition) && _new.isPositionErrorCard(newItemPosition)) {
|
||||
return Objects.equals(_old.getErrorCardInfo(), _new.getErrorCardInfo());
|
||||
}
|
||||
|
||||
int oldEntryIndex = _old.translateEntryPosToIndex(oldItemPosition);
|
||||
int newEntryIndex = _new.translateEntryPosToIndex(newItemPosition);
|
||||
return _old.getShownEntries().get(oldEntryIndex)
|
||||
.equals(_new.getShownEntries().get(newEntryIndex));
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onEntryClick(VaultEntry entry);
|
||||
boolean onLongEntryClick(VaultEntry entry);
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
@@ -15,6 +27,7 @@ import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.CenterVerticalSpan;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
@@ -27,6 +40,7 @@ import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private static final float DEFAULT_ALPHA = 1.0f;
|
||||
@@ -36,6 +50,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private View _favoriteIndicator;
|
||||
private TextView _profileName;
|
||||
private TextView _profileCode;
|
||||
private TextView _nextProfileCode;
|
||||
private TextView _profileIssuer;
|
||||
private TextView _profileCopied;
|
||||
private ImageView _profileDrawable;
|
||||
@@ -44,21 +59,23 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private RelativeLayout _description;
|
||||
private ImageView _dragHandle;
|
||||
private ViewMode _viewMode;
|
||||
|
||||
|
||||
private final ImageView _selected;
|
||||
private final Handler _selectedHandler;
|
||||
|
||||
private Preferences.CodeGrouping _codeGrouping = Preferences.CodeGrouping.NO_GROUPING;
|
||||
private AccountNamePosition _accountNamePosition = AccountNamePosition.HIDDEN;
|
||||
|
||||
private boolean _hidden;
|
||||
private boolean _paused;
|
||||
|
||||
private TotpProgressBar _progressBar;
|
||||
private MaterialCardView _view;
|
||||
|
||||
private UiRefresher _refresher;
|
||||
private Handler _animationHandler;
|
||||
private Handler _copyAnimationHandler;
|
||||
private Handler _expirationHandler;
|
||||
private AnimatorSet _expirationAnimSet;
|
||||
private boolean _showNextCode;
|
||||
private boolean _showExpirationState;
|
||||
|
||||
private Animation _scaleIn;
|
||||
private Animation _scaleOut;
|
||||
@@ -69,6 +86,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_view = (MaterialCardView) view;
|
||||
_profileName = view.findViewById(R.id.profile_account_name);
|
||||
_profileCode = view.findViewById(R.id.profile_code);
|
||||
_nextProfileCode = view.findViewById(R.id.next_profile_code);
|
||||
_profileIssuer = view.findViewById(R.id.profile_issuer);
|
||||
_profileCopied = view.findViewById(R.id.profile_copied);
|
||||
_description = view.findViewById(R.id.description);
|
||||
@@ -78,8 +96,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_dragHandle = view.findViewById(R.id.drag_handle);
|
||||
_favoriteIndicator = view.findViewById(R.id.favorite_indicator);
|
||||
|
||||
_selectedHandler = new Handler();
|
||||
_animationHandler = new Handler();
|
||||
_copyAnimationHandler = new Handler();
|
||||
_expirationHandler = new Handler();
|
||||
|
||||
_progressBar = view.findViewById(R.id.progressBar);
|
||||
|
||||
@@ -89,38 +107,51 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_refresher = new UiRefresher(new UiRefresher.Listener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
if (!_hidden && !_paused) {
|
||||
refreshCode();
|
||||
}
|
||||
refreshCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExpiring() { }
|
||||
|
||||
@Override
|
||||
public long getMillisTillNextRefresh() {
|
||||
return ((TotpInfo) _entry.getInfo()).getMillisTillNextRotation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPeriodMillis() {
|
||||
return ((TotpInfo) _entry.getInfo()).getPeriod() * 1000L;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed) {
|
||||
public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean nonUniform, boolean hidden, boolean paused, boolean dimmed, boolean showExpirationState, boolean showNextCode) {
|
||||
_entry = entry;
|
||||
_hidden = hidden;
|
||||
_paused = paused;
|
||||
_codeGrouping = groupSize;
|
||||
_viewMode = viewMode;
|
||||
|
||||
_accountNamePosition = accountNamePosition;
|
||||
if (viewMode.equals(ViewMode.TILES) && _accountNamePosition == AccountNamePosition.END) {
|
||||
_accountNamePosition = AccountNamePosition.BELOW;
|
||||
}
|
||||
|
||||
_selected.clearAnimation();
|
||||
_selected.setVisibility(View.GONE);
|
||||
_selectedHandler.removeCallbacksAndMessages(null);
|
||||
_animationHandler.removeCallbacksAndMessages(null);
|
||||
_copyAnimationHandler.removeCallbacksAndMessages(null);
|
||||
_expirationHandler.removeCallbacksAndMessages(null);
|
||||
_showNextCode = entry.getInfo() instanceof TotpInfo && showNextCode;
|
||||
_showExpirationState = _entry.getInfo() instanceof TotpInfo && showExpirationState;
|
||||
|
||||
_favoriteIndicator.setVisibility(_entry.isFavorite() ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
// only show the progress bar if there is no uniform period and the entry type is TotpInfo
|
||||
setShowProgress(showProgress);
|
||||
setShowProgress(nonUniform);
|
||||
|
||||
// only show the button if this entry is of type HotpInfo
|
||||
_buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE);
|
||||
_nextProfileCode.setVisibility(_showNextCode ? View.VISIBLE : View.GONE);
|
||||
|
||||
String profileIssuer = entry.getIssuer();
|
||||
String profileName = entry.getName();
|
||||
@@ -138,20 +169,29 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
}
|
||||
|
||||
showIcon(showIcon);
|
||||
|
||||
itemView.setAlpha(dimmed ? DIMMED_ALPHA : DEFAULT_ALPHA);
|
||||
}
|
||||
|
||||
private void setAccountNameLayout(AccountNamePosition accountNamePosition, Boolean hasBothIssuerAndName) {
|
||||
if (_viewMode == ViewMode.TILES) {
|
||||
return;
|
||||
}
|
||||
|
||||
RelativeLayout.LayoutParams profileNameLayoutParams;
|
||||
RelativeLayout.LayoutParams copiedLayoutParams;
|
||||
|
||||
switch (accountNamePosition) {
|
||||
case HIDDEN:
|
||||
_profileName.setVisibility(View.GONE);
|
||||
|
||||
if (_viewMode == ViewMode.TILES) {
|
||||
_profileCopied.setGravity(Gravity.CENTER_VERTICAL);
|
||||
((RelativeLayout.LayoutParams)_profileCopied.getLayoutParams()).removeRule(RelativeLayout.BELOW);
|
||||
_profileCopied.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
_profileCopied.setTextSize(14);
|
||||
|
||||
_profileIssuer.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
_profileIssuer.setGravity(Gravity.CENTER_VERTICAL);
|
||||
_profileIssuer.setTextSize(14);
|
||||
|
||||
_profileName.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case BELOW:
|
||||
@@ -256,11 +296,24 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public void refreshCode() {
|
||||
if (!_hidden && !_paused) {
|
||||
updateCode();
|
||||
updateCodes();
|
||||
startExpirationAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCode() {
|
||||
private void updateCodes() {
|
||||
_profileCode.setText(getOtp());
|
||||
|
||||
if (_showNextCode) {
|
||||
_nextProfileCode.setText(getOtp(1));
|
||||
}
|
||||
}
|
||||
|
||||
private String getOtp() {
|
||||
return getOtp(0);
|
||||
}
|
||||
|
||||
private String getOtp(int offset) {
|
||||
OtpInfo info = _entry.getInfo();
|
||||
|
||||
// In previous versions of Aegis, it was possible to import entries with an empty
|
||||
@@ -269,7 +322,12 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
// the OTP, instead of crashing.
|
||||
String otp;
|
||||
try {
|
||||
otp = info.getOtp();
|
||||
if (info instanceof TotpInfo) {
|
||||
otp = ((TotpInfo)info).getOtp((System.currentTimeMillis() / 1000) + ((long) (offset) * ((TotpInfo) _entry.getInfo()).getPeriod()));
|
||||
} else {
|
||||
otp = info.getOtp();
|
||||
}
|
||||
|
||||
if (!(info instanceof SteamInfo || info instanceof YandexInfo)) {
|
||||
otp = formatCode(otp);
|
||||
}
|
||||
@@ -277,7 +335,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
otp = _view.getResources().getString(R.string.error_all_caps);
|
||||
}
|
||||
|
||||
_profileCode.setText(otp);
|
||||
return otp;
|
||||
}
|
||||
|
||||
private String formatCode(String code) {
|
||||
@@ -310,17 +368,130 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
}
|
||||
|
||||
public void revealCode() {
|
||||
updateCode();
|
||||
updateCodes();
|
||||
startExpirationAnimation();
|
||||
_hidden = false;
|
||||
}
|
||||
|
||||
public void hideCode() {
|
||||
String hiddenText = new String(new char[_entry.getInfo().getDigits()]).replace("\0", Character.toString(HIDDEN_CHAR));
|
||||
hiddenText = formatCode(hiddenText);
|
||||
_profileCode.setText(hiddenText);
|
||||
String code = getOtp();
|
||||
String hiddenText = code.replaceAll("\\S", Character.toString(HIDDEN_CHAR));
|
||||
stopExpirationAnimation();
|
||||
|
||||
updateTextViewWithDots(_profileCode, hiddenText, code);
|
||||
updateTextViewWithDots(_nextProfileCode, hiddenText, code);
|
||||
|
||||
_hidden = true;
|
||||
}
|
||||
|
||||
private void updateTextViewWithDots(TextView textView, String hiddenCode, String code) {
|
||||
Paint paint = new Paint();
|
||||
paint.setTextSize(_profileCode.getTextSize());
|
||||
|
||||
// Calculate the difference between the actual code width and the dots width
|
||||
float codeWidth = paint.measureText(code);
|
||||
float dotsWidth = paint.measureText(hiddenCode);
|
||||
float scaleFactor = codeWidth / dotsWidth;
|
||||
scaleFactor = (float)(Math.round(scaleFactor * 10.0) / 10.0);
|
||||
textView.setTextColor(MaterialColors.getColor(textView, R.attr.colorCodeHidden));
|
||||
|
||||
// If scale is higher or equal to 0.8, do nothing and proceed with the normal text rendering
|
||||
if (scaleFactor >= 0.8) {
|
||||
textView.setText(hiddenCode);
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to use an invisible character in order to get the height of the profileCode textview consistent
|
||||
// Tokens without a space (ie Steam TOTP) will get misaligned without this
|
||||
SpannableString dotsString = new SpannableString("\u200B" + hiddenCode);
|
||||
|
||||
// Only scale the digits/characters, skip the spaces
|
||||
int start = 1;
|
||||
for (int i = 0; i <= dotsString.length(); i++) {
|
||||
if (i == dotsString.length() || dotsString.charAt(i) == ' ') {
|
||||
if (i > start) {
|
||||
dotsString.setSpan(new RelativeSizeSpan(scaleFactor), start, i, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
Rect dotsRectBounds = new Rect();
|
||||
paint.getTextBounds(hiddenCode, 1, hiddenCode.length(), dotsRectBounds);
|
||||
|
||||
// Use custom CenterVerticalSpan to make sure the dots are vertically aligned
|
||||
dotsString.setSpan(new CenterVerticalSpan(dotsRectBounds), 1, dotsString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
textView.setText(dotsString);
|
||||
}
|
||||
|
||||
public void startExpirationAnimation() {
|
||||
stopExpirationAnimation();
|
||||
if (!_showExpirationState) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int totalStateDuration = 7000;
|
||||
TotpInfo info = (TotpInfo) _entry.getInfo();
|
||||
if (info.getPeriod() * 1000 <= totalStateDuration) {
|
||||
_profileCode.setTextColor(MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError));
|
||||
return;
|
||||
}
|
||||
|
||||
// Workaround for when animations are disabled or Android version being too old
|
||||
float durationScale = AnimationsHelper.Scale.ANIMATOR.getValue(itemView.getContext());
|
||||
if (durationScale == 0.0 || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
int color = MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError);
|
||||
if (info.getMillisTillNextRotation() < totalStateDuration) {
|
||||
_profileCode.setTextColor(color);
|
||||
} else {
|
||||
_expirationHandler.postDelayed(() -> {
|
||||
_profileCode.setTextColor(color);
|
||||
}, info.getMillisTillNextRotation() - totalStateDuration);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final int colorShiftDuration = 300;
|
||||
long delayAnimDuration = info.getPeriod() * 1000L - totalStateDuration - colorShiftDuration;
|
||||
ValueAnimator delayAnim = ValueAnimator.ofFloat(0f, 0f);
|
||||
delayAnim.setDuration((long) (delayAnimDuration / durationScale));
|
||||
|
||||
int colorFrom = _profileCode.getCurrentTextColor();
|
||||
int colorTo = MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError);
|
||||
ValueAnimator colorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
|
||||
colorAnim.setDuration((long) (colorShiftDuration / durationScale));
|
||||
colorAnim.addUpdateListener(a -> _profileCode.setTextColor((int) a.getAnimatedValue()));
|
||||
|
||||
final int blinkDuration = 3000;
|
||||
ValueAnimator delayAnim2 = ValueAnimator.ofFloat(0f, 0f);
|
||||
delayAnim2.setDuration((long) ((totalStateDuration - blinkDuration) / durationScale));
|
||||
|
||||
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(_profileCode, "alpha", 1f, .5f);
|
||||
alphaAnim.setDuration((long) (500 / durationScale));
|
||||
alphaAnim.setRepeatCount(blinkDuration / 500 - 1);
|
||||
alphaAnim.setRepeatMode(ValueAnimator.REVERSE);
|
||||
|
||||
_expirationAnimSet = new AnimatorSet();
|
||||
_expirationAnimSet.playSequentially(delayAnim, colorAnim, delayAnim2, alphaAnim);
|
||||
_expirationAnimSet.start();
|
||||
long currentPlayTime = (info.getPeriod() * 1000L) - info.getMillisTillNextRotation();
|
||||
_expirationAnimSet.setCurrentPlayTime((long) (currentPlayTime / durationScale));
|
||||
}
|
||||
|
||||
private void stopExpirationAnimation() {
|
||||
_expirationHandler.removeCallbacksAndMessages(null);
|
||||
if (_expirationAnimSet != null) {
|
||||
_expirationAnimSet.cancel();
|
||||
_expirationAnimSet = null;
|
||||
}
|
||||
|
||||
int colorTo = MaterialColors.getColor(_profileCode, R.attr.colorCode);
|
||||
_profileCode.setTextColor(colorTo);
|
||||
_profileCode.setAlpha(1f);
|
||||
}
|
||||
|
||||
public void showIcon(boolean show) {
|
||||
if (show) {
|
||||
_profileDrawable.setVisibility(View.VISIBLE);
|
||||
@@ -336,8 +507,11 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
public void setPaused(boolean paused) {
|
||||
_paused = paused;
|
||||
|
||||
if (!_hidden && !_paused) {
|
||||
updateCode();
|
||||
if (_paused) {
|
||||
stopExpirationAnimation();
|
||||
} else if (!_hidden) {
|
||||
updateCodes();
|
||||
startExpirationAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,31 +523,33 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
animateAlphaTo(DEFAULT_ALPHA);
|
||||
}
|
||||
|
||||
public void animateCopyText(boolean includeSlideAnimation) {
|
||||
_animationHandler.removeCallbacksAndMessages(null);
|
||||
public void animateCopyText() {
|
||||
_copyAnimationHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
Animation slideDownFadeIn = AnimationsHelper.loadScaledAnimation(itemView.getContext(), R.anim.slide_down_fade_in);
|
||||
Animation slideDownFadeOut = AnimationsHelper.loadScaledAnimation(itemView.getContext(), R.anim.slide_down_fade_out);
|
||||
Animation fadeOut = AnimationsHelper.loadScaledAnimation(itemView.getContext(), R.anim.fade_out);
|
||||
Animation fadeIn = AnimationsHelper.loadScaledAnimation(itemView.getContext(), R.anim.fade_in);
|
||||
|
||||
if (includeSlideAnimation) {
|
||||
// Use slideDown animation when user is not using Tiles mode
|
||||
if (_viewMode != ViewMode.TILES) {
|
||||
_profileCopied.startAnimation(slideDownFadeIn);
|
||||
View fadeOutView = (_accountNamePosition == AccountNamePosition.BELOW) ? _profileName : _description;
|
||||
View fadeOutView = (_accountNamePosition == AccountNamePosition.BELOW) ? _profileName : _description;
|
||||
fadeOutView.startAnimation(slideDownFadeOut);
|
||||
|
||||
fadeOutView.startAnimation(slideDownFadeOut);
|
||||
|
||||
_animationHandler.postDelayed(() -> {
|
||||
_copyAnimationHandler.postDelayed(() -> {
|
||||
_profileCopied.startAnimation(fadeOut);
|
||||
fadeOutView.startAnimation(fadeIn);
|
||||
}, 3000);
|
||||
} else {
|
||||
_profileCopied.startAnimation(fadeIn);
|
||||
_profileName.startAnimation(fadeOut);
|
||||
View visibleProfileText = _accountNamePosition == AccountNamePosition.BELOW ? _profileName : _profileIssuer;
|
||||
|
||||
_animationHandler.postDelayed(() -> {
|
||||
_profileCopied.startAnimation(fadeIn);
|
||||
visibleProfileText.startAnimation(fadeOut);
|
||||
|
||||
_copyAnimationHandler.postDelayed(() -> {
|
||||
_profileCopied.startAnimation(fadeOut);
|
||||
_profileName.startAnimation(fadeIn);
|
||||
visibleProfileText.startAnimation(fadeIn);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,15 @@ import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.LayoutAnimationController;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.widget.NestedScrollView;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
@@ -33,17 +33,16 @@ import com.beemdevelopment.aegis.CopyBehavior;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.VibrationPatterns;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.MetricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
import com.beemdevelopment.aegis.helpers.VibrationHelper;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
|
||||
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.bumptech.glide.Glide;
|
||||
@@ -51,29 +50,25 @@ 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.BottomSheetBehavior;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.android.material.shape.CornerFamily;
|
||||
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private EntryAdapter _adapter;
|
||||
private Listener _listener;
|
||||
private SimpleItemTouchHelperCallback _touchCallback;
|
||||
private ItemTouchHelper _touchHelper;
|
||||
private VibrationHelper _vibrationHelper;
|
||||
|
||||
private RecyclerView _recyclerView;
|
||||
private RecyclerView.ItemDecoration _itemDecoration;
|
||||
@@ -81,11 +76,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private TotpProgressBar _progressBar;
|
||||
private boolean _showProgress;
|
||||
private ViewMode _viewMode;
|
||||
private Collection<VaultGroup> _groups;
|
||||
private LinearLayout _emptyStateView;
|
||||
private Chip _groupChip;
|
||||
private Set<UUID> _groupFilter;
|
||||
private Set<UUID> _prefGroupFilter;
|
||||
|
||||
private UiRefresher _refresher;
|
||||
|
||||
@@ -106,8 +97,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
|
||||
_progressBar = view.findViewById(R.id.progressBar);
|
||||
_groupChip = view.findViewById(R.id.chip_group);
|
||||
initializeGroupChip();
|
||||
_vibrationHelper = new VibrationHelper(getContext());
|
||||
|
||||
// set up the recycler view
|
||||
_recyclerView = view.findViewById(R.id.rvKeyProfiles);
|
||||
@@ -157,12 +147,40 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
refresh(false);
|
||||
_vibrationHelper.vibratePattern(getContext(), VibrationPatterns.REFRESH_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExpiring() {
|
||||
_vibrationHelper.vibratePattern(getContext(), VibrationPatterns.EXPIRING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMillisTillNextRefresh() {
|
||||
return TotpInfo.getMillisTillNextRotation(_adapter.getMostFrequentPeriod());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPeriodMillis() {
|
||||
return _adapter.getMostFrequentPeriod() * 1000L;
|
||||
}
|
||||
});
|
||||
|
||||
final int rvInitialPaddingLeft = _recyclerView.getPaddingLeft();
|
||||
final int rvInitialPaddingTop = _recyclerView.getPaddingTop();
|
||||
final int rvInitialPaddingRight = _recyclerView.getPaddingRight();
|
||||
final int rvInitialPaddingBottom = _recyclerView.getPaddingBottom();
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(_recyclerView, (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
// left and right padding seems to be handled by fitsSystemWindows="true" on the CoordinatorLayout in activity_main.xml
|
||||
targetView.setPadding(
|
||||
rvInitialPaddingLeft,
|
||||
rvInitialPaddingTop,
|
||||
rvInitialPaddingRight,
|
||||
rvInitialPaddingBottom + insets.bottom
|
||||
);
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
_emptyStateView = view.findViewById(R.id.vEmptyList);
|
||||
@@ -173,27 +191,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_preloadSizeProvider.setView(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
if (savedInstanceState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<UUID> filter = (HashSet<UUID>) savedInstanceState.getSerializable("prefGroupFilter");
|
||||
if (filter != null) {
|
||||
_prefGroupFilter = filter;
|
||||
}
|
||||
public int getScrollPosition() {
|
||||
return ((LinearLayoutManager) _recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
// user can apply _groupFilter without saving
|
||||
// restore _groupFilter as _prefGroupFilter in order to reapply correct filter after screen rotate
|
||||
if (_groupFilter != null) {
|
||||
outState.putSerializable("prefGroupFilter", new HashSet<>(_groupFilter));
|
||||
}
|
||||
public void scrollToPosition(int position) {
|
||||
_recyclerView.getLayoutManager().scrollToPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -202,16 +205,26 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
public void setGroupFilter(Set<UUID> groups, boolean animate) {
|
||||
_groupFilter = groups;
|
||||
public void onRefreshStop() {
|
||||
_refresher.stop();
|
||||
}
|
||||
|
||||
public void onRefreshStart() {
|
||||
if (_adapter.getMostFrequentPeriod() != -1){
|
||||
_refresher.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void setGroups(Collection<VaultGroup> groups) {
|
||||
_adapter.setGroups(groups);
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setGroupFilter(Set<UUID> groups) {
|
||||
_adapter.setGroupFilter(groups);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
updateEmptyState();
|
||||
updateGroupChip();
|
||||
|
||||
if (animate) {
|
||||
runEntriesAnimation();
|
||||
}
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setIsLongPressDragEnabled(boolean enabled) {
|
||||
@@ -222,6 +235,10 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_adapter.setCopyBehavior(copyBehavior);
|
||||
}
|
||||
|
||||
public void setSearchBehaviorMask(int searchBehaviorMask) {
|
||||
_adapter.setSearchBehaviorMask(searchBehaviorMask);
|
||||
}
|
||||
|
||||
public List<VaultEntry> selectAllEntries() {
|
||||
return _adapter.selectAllEntries();
|
||||
}
|
||||
@@ -240,10 +257,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
public void setSortCategory(SortCategory sortCategory, boolean apply) {
|
||||
_adapter.setSortCategory(sortCategory, apply);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
|
||||
if (apply) {
|
||||
runEntriesAnimation();
|
||||
}
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setUsageCounts(Map<UUID, Integer> usageCounts) {
|
||||
@@ -265,8 +279,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
public void setSearchFilter(String search) {
|
||||
_adapter.setSearchFilter(search);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
|
||||
updateEmptyState();
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setSelectedEntry(VaultEntry entry) {
|
||||
@@ -365,11 +379,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_progressBar.setVisibility(View.VISIBLE);
|
||||
_progressBar.setPeriod(period);
|
||||
_progressBar.start();
|
||||
_refresher.start();
|
||||
onRefreshStart();
|
||||
} else {
|
||||
_progressBar.setVisibility(View.GONE);
|
||||
_progressBar.stop();
|
||||
_refresher.stop();
|
||||
onRefreshStop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,10 +394,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
public void setPrefGroupFilter(Set<UUID> groupFilter) {
|
||||
_prefGroupFilter = groupFilter;
|
||||
}
|
||||
|
||||
public void setCodeGroupSize(Preferences.CodeGrouping codeGrouping) {
|
||||
_adapter.setCodeGroupSize(codeGrouping);
|
||||
}
|
||||
@@ -400,6 +410,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_adapter.setShowIcon(showIcon);
|
||||
}
|
||||
|
||||
public void setShowNextCode(boolean showNextCode) {
|
||||
_adapter.setShowNextCode(showNextCode);
|
||||
}
|
||||
|
||||
public void setShowExpirationState(boolean showExpirationState) {
|
||||
_adapter.setShowExpirationState(showExpirationState);
|
||||
}
|
||||
|
||||
public void setHighlightEntry(boolean highlightEntry) {
|
||||
_adapter.setHighlightEntry(highlightEntry);
|
||||
}
|
||||
@@ -420,61 +438,57 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_adapter.setErrorCardInfo(info);
|
||||
}
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
addEntry(entry, false);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void addEntry(VaultEntry entry, boolean focusEntry) {
|
||||
int position = _adapter.addEntry(entry);
|
||||
updateEmptyState();
|
||||
public void onEntryAdded(VaultEntry entry) {
|
||||
int position = _adapter.getEntryPosition(entry);
|
||||
if (position < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
LinearLayoutManager layoutManager = (LinearLayoutManager) _recyclerView.getLayoutManager();
|
||||
if (focusEntry && position >= 0) {
|
||||
if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition())
|
||||
|| (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) {
|
||||
boolean smoothScroll = !AnimationsHelper.Scale.TRANSITION.isZero(requireContext());
|
||||
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
|
||||
private void handleScroll() {
|
||||
_recyclerView.removeOnScrollListener(this);
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
tempHighlightEntry(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
if (smoothScroll && newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
handleScroll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (!smoothScroll) {
|
||||
handleScroll();
|
||||
}
|
||||
}
|
||||
};
|
||||
_recyclerView.addOnScrollListener(scrollListener);
|
||||
_recyclerView.setOnTouchListener((v, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
_recyclerView.removeOnScrollListener(scrollListener);
|
||||
_recyclerView.stopScroll();
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
// We can't easily control the speed of the smooth scroll animation, but we
|
||||
// can at least disable it if animations are disabled
|
||||
if (smoothScroll) {
|
||||
_recyclerView.smoothScrollToPosition(position);
|
||||
} else {
|
||||
_recyclerView.scrollToPosition(position);
|
||||
if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition())
|
||||
|| (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) {
|
||||
boolean smoothScroll = !AnimationsHelper.Scale.TRANSITION.isZero(requireContext());
|
||||
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
|
||||
private void handleScroll() {
|
||||
_recyclerView.removeOnScrollListener(this);
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
tempHighlightEntry(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
if (smoothScroll && newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
handleScroll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (!smoothScroll) {
|
||||
handleScroll();
|
||||
}
|
||||
}
|
||||
};
|
||||
_recyclerView.addOnScrollListener(scrollListener);
|
||||
_recyclerView.setOnTouchListener((v, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
_recyclerView.removeOnScrollListener(scrollListener);
|
||||
_recyclerView.stopScroll();
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
// We can't easily control the speed of the smooth scroll animation, but we
|
||||
// can at least disable it if animations are disabled
|
||||
if (smoothScroll) {
|
||||
_recyclerView.smoothScrollToPosition(position);
|
||||
} else {
|
||||
tempHighlightEntry(entry);
|
||||
_recyclerView.scrollToPosition(position);
|
||||
}
|
||||
} else {
|
||||
tempHighlightEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,27 +499,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_adapter.focusEntry(entry, secondsToFocus);
|
||||
}
|
||||
|
||||
public void addEntries(Collection<VaultEntry> entries) {
|
||||
_adapter.addEntries(entries);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void removeEntry(VaultEntry entry) {
|
||||
_adapter.removeEntry(entry);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void removeEntry(UUID uuid) {
|
||||
_adapter.removeEntry(uuid);
|
||||
public void setEntries(Collection<VaultEntry> entries) {
|
||||
_adapter.setEntries(new ArrayList<>(entries));
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void clearEntries() {
|
||||
_adapter.clearEntries();
|
||||
}
|
||||
|
||||
public void replaceEntry(UUID uuid, VaultEntry newEntry) {
|
||||
_adapter.replaceEntry(uuid, newEntry);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void runEntriesAnimation() {
|
||||
@@ -515,115 +516,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_recyclerView.scheduleLayoutAnimation();
|
||||
}
|
||||
|
||||
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) {
|
||||
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
|
||||
chip.setText(group.getName());
|
||||
chip.setCheckable(true);
|
||||
chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID()));
|
||||
chip.setCheckedIconVisible(true);
|
||||
chip.setOnCheckedChangeListener((group1, checkedId) -> {
|
||||
Set<UUID> groupFilter = getGroupFilter(chipGroup);
|
||||
setGroupFilter(groupFilter, true);
|
||||
});
|
||||
chip.setTag(group);
|
||||
chipGroup.addView(chip);
|
||||
}
|
||||
|
||||
private void initializeGroupChip() {
|
||||
View view = getLayoutInflater().inflate(R.layout.dialog_select_groups, null);
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(requireContext());
|
||||
NestedScrollView scrollView = view.findViewById(R.id.scrollView);
|
||||
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) scrollView.getLayoutParams();
|
||||
layoutParams.matchConstraintMaxHeight = getResources().getConfiguration().screenHeightDp;
|
||||
|
||||
dialog.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
dialog.getBehavior().setSkipCollapsed(false);
|
||||
dialog.setContentView(view);
|
||||
|
||||
ChipGroup chipGroup = view.findViewById(R.id.groupChipGroup);
|
||||
Button clearButton = view.findViewById(R.id.btnClear);
|
||||
Button saveButton = view.findViewById(R.id.btnSave);
|
||||
clearButton.setOnClickListener(v -> {
|
||||
chipGroup.clearCheck();
|
||||
Set<UUID> groupFilter = Collections.emptySet();
|
||||
if (_listener != null) {
|
||||
_listener.onSaveGroupFilter(groupFilter);
|
||||
}
|
||||
setGroupFilter(groupFilter, true);
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
saveButton.setOnClickListener(v -> {
|
||||
Set<UUID> groupFilter = getGroupFilter(chipGroup);
|
||||
if (_listener != null) {
|
||||
_listener.onSaveGroupFilter(groupFilter);
|
||||
}
|
||||
setGroupFilter(groupFilter, true);
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
_groupChip.setOnClickListener(v -> {
|
||||
chipGroup.removeAllViews();
|
||||
|
||||
for (VaultGroup group : _groups) {
|
||||
addChipTo(chipGroup, new VaultGroupModel(group));
|
||||
}
|
||||
addChipTo(chipGroup, new VaultGroupModel(getString(R.string.no_group)));
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
private static Set<UUID> getGroupFilter(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 void updateGroupChip() {
|
||||
if (_groupFilter.isEmpty()) {
|
||||
_groupChip.setText(R.string.groups);
|
||||
} else {
|
||||
_groupChip.setText(String.format("%s (%d)", getString(R.string.groups), _groupFilter.size()));
|
||||
}
|
||||
}
|
||||
|
||||
private void setShowProgress(boolean showProgress) {
|
||||
_showProgress = showProgress;
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setGroups(Collection<VaultGroup> groups) {
|
||||
_groups = groups;
|
||||
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
updateDividerDecoration();
|
||||
|
||||
if (_prefGroupFilter != null) {
|
||||
Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter);
|
||||
_prefGroupFilter = null;
|
||||
if (!groupFilter.isEmpty()) {
|
||||
setGroupFilter(groupFilter, false);
|
||||
}
|
||||
} else if (_groupFilter != null) {
|
||||
Set<UUID> groupFilter = cleanGroupFilter(_groupFilter);
|
||||
if (!_groupFilter.equals(groupFilter)) {
|
||||
setGroupFilter(groupFilter, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) {
|
||||
Set<UUID> groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet());
|
||||
|
||||
return groupFilter.stream()
|
||||
.filter(g -> g == null || groupUuids.contains(g))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private void updateDividerDecoration() {
|
||||
if (_itemDecoration != null) {
|
||||
_recyclerView.removeItemDecoration(_itemDecoration);
|
||||
@@ -700,15 +597,15 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
|
||||
int entryIndex = _adapter.translateEntryPosToIndex(adapterPosition);
|
||||
// The first entry should have a top margin, but only if the group chip is not shown and the error card is not shown
|
||||
if (entryIndex == 0 && (_groups == null || _groups.isEmpty()) && !_adapter.isErrorCardShown()) {
|
||||
// The first entry should have a top margin, but only if the error card is not shown
|
||||
if (entryIndex == 0 && !_adapter.isErrorCardShown()) {
|
||||
outRect.top = _offset;
|
||||
}
|
||||
|
||||
// Only non-favorite entries have a bottom margin, except for the final favorite entry
|
||||
int totalFavorites = _adapter.getShownFavoritesCount();
|
||||
if (totalFavorites == 0
|
||||
|| (entryIndex < _adapter.getShownEntriesCount() && !_adapter.getEntryAtPos(adapterPosition).isFavorite())
|
||||
|| (entryIndex < _adapter.getShownEntriesCount() && !_adapter.getEntryAtPosition(adapterPosition).isFavorite())
|
||||
|| totalFavorites == entryIndex + 1) {
|
||||
outRect.bottom = _offset;
|
||||
}
|
||||
@@ -801,7 +698,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
VaultEntry entry = _adapter.getEntryAtPos(position);
|
||||
VaultEntry entry = _adapter.getEntryAtPosition(position);
|
||||
if (!entry.hasIcon()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import android.view.ViewGroup;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
|
||||
import com.beemdevelopment.aegis.util.CollectionUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> implements ItemTouchHelperAdapter {
|
||||
private GroupAdapter.Listener _listener;
|
||||
private ArrayList<VaultGroup> _groups;
|
||||
|
||||
@@ -32,6 +34,10 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<VaultGroup> getGroups() {
|
||||
return _groups;
|
||||
}
|
||||
|
||||
public void replaceGroup(UUID uuid, VaultGroup newGroup) {
|
||||
VaultGroup oldGroup = getGroupByUUID(uuid);
|
||||
int position = _groups.indexOf(oldGroup);
|
||||
@@ -64,6 +70,18 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMove(int firstPosition, int secondPosition) {
|
||||
CollectionUtils.move(_groups, firstPosition, secondPosition);
|
||||
notifyItemMoved(firstPosition, secondPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDismiss(int position) { }
|
||||
|
||||
@Override
|
||||
public void onItemDrop(int position) { }
|
||||
|
||||
private VaultGroup getGroupByUUID(UUID uuid) {
|
||||
for (VaultGroup group : _groups) {
|
||||
if (group.getUUID().equals(uuid)) {
|
||||
|
||||
@@ -35,8 +35,10 @@ public class IconPackAdapter extends RecyclerView.Adapter<IconPackHolder> {
|
||||
|
||||
public void removeIconPack(IconPack pack) {
|
||||
int position = _iconPacks.indexOf(pack);
|
||||
_iconPacks.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
if (position >= 0) {
|
||||
_iconPacks.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -2,14 +2,11 @@ package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
|
||||
@@ -30,7 +27,6 @@ public class TotpProgressBar extends ProgressBar {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public TotpProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.beemdevelopment.aegis.util;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
|
||||
public final class ClipboardUtils {
|
||||
public static String readText(Context context) {
|
||||
ClipboardManager clipboard =
|
||||
(ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
if (clipboard == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ClipData clip = clipboard.getPrimaryClip();
|
||||
if (clip == null || clip.getItemCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ClipData.Item item = clip.getItemAt(0);
|
||||
CharSequence cs = item.coerceToText(context);
|
||||
if (cs == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String text = cs.toString().trim();
|
||||
if (text.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public class Vault {
|
||||
private static final int VERSION = 3;
|
||||
private final UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||
private final UUIDMap<VaultGroup> _groups = new UUIDMap<>();
|
||||
private boolean _iconsOptimized = true;
|
||||
|
||||
// Whether we've migrated the group list to the new format while parsing the vault
|
||||
private boolean _isGroupsMigrationFresh = false;
|
||||
@@ -42,6 +43,7 @@ public class Vault {
|
||||
obj.put("version", VERSION);
|
||||
obj.put("entries", entriesArray);
|
||||
obj.put("groups", groupsArray);
|
||||
obj.put("icons_optimized", _iconsOptimized);
|
||||
|
||||
return obj;
|
||||
} catch (JSONException e) {
|
||||
@@ -86,6 +88,10 @@ public class Vault {
|
||||
|
||||
entries.add(entry);
|
||||
}
|
||||
|
||||
if (!obj.optBoolean("icons_optimized")) {
|
||||
vault.setIconsOptimized(false);
|
||||
}
|
||||
} catch (VaultEntryException | JSONException e) {
|
||||
throw new VaultException(e);
|
||||
}
|
||||
@@ -101,6 +107,14 @@ public class Vault {
|
||||
return _isGroupsMigrationFresh;
|
||||
}
|
||||
|
||||
public void setIconsOptimized(boolean optimized) {
|
||||
_iconsOptimized = optimized;
|
||||
}
|
||||
|
||||
public boolean areIconsOptimized() {
|
||||
return _iconsOptimized;
|
||||
}
|
||||
|
||||
public boolean migrateOldGroup(VaultEntry entry) {
|
||||
if (entry.getOldGroup() != null) {
|
||||
Optional<VaultGroup> optGroup = getGroups().getValues()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.UriPermission;
|
||||
import android.net.Uri;
|
||||
@@ -9,6 +10,7 @@ import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.database.AuditLogRepository;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
@@ -38,6 +40,7 @@ public class VaultBackupManager {
|
||||
new StrictDateFormat("yyyyMMdd-HHmmss", Locale.ENGLISH);
|
||||
|
||||
public static final String FILENAME_PREFIX = "aegis-backup";
|
||||
public static final String FILENAME_SINGLE = String.format("%s.json", FILENAME_PREFIX);
|
||||
|
||||
private final Context _context;
|
||||
private final Preferences _prefs;
|
||||
@@ -51,10 +54,10 @@ public class VaultBackupManager {
|
||||
_auditLogRepository = auditLogRepository;
|
||||
}
|
||||
|
||||
public void scheduleBackup(File tempFile, Uri dirUri, int versionsToKeep) {
|
||||
public void scheduleBackup(File tempFile, BackupsVersioningStrategy strategy, Uri uri, int versionsToKeep) {
|
||||
_executor.execute(() -> {
|
||||
try {
|
||||
createBackup(tempFile, dirUri, versionsToKeep);
|
||||
createBackup(tempFile, strategy, uri, versionsToKeep);
|
||||
_auditLogRepository.addBackupCreatedEvent();
|
||||
_prefs.setBuiltInBackupResult(new Preferences.BackupResult(null));
|
||||
} catch (VaultRepositoryException | VaultBackupPermissionException e) {
|
||||
@@ -64,6 +67,46 @@ public class VaultBackupManager {
|
||||
});
|
||||
}
|
||||
|
||||
private void createBackup(File tempFile, BackupsVersioningStrategy strategy, Uri uri, int versionsToKeep)
|
||||
throws VaultRepositoryException, VaultBackupPermissionException {
|
||||
if (uri == null) {
|
||||
throw new VaultRepositoryException("getBackupsLocation returned null");
|
||||
}
|
||||
if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
createBackup(tempFile, uri);
|
||||
} else if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
createBackup(tempFile, uri, versionsToKeep);
|
||||
} else {
|
||||
throw new VaultRepositoryException("Invalid backups versioning strategy");
|
||||
}
|
||||
}
|
||||
|
||||
private void createBackup(File tempFile, Uri fileUri)
|
||||
throws VaultRepositoryException, VaultBackupPermissionException {
|
||||
Log.i(TAG, String.format("Creating backup at %s", fileUri));
|
||||
try {
|
||||
if (!hasPermissionsAt(fileUri)) {
|
||||
throw new VaultBackupPermissionException("No persisted URI permissions");
|
||||
}
|
||||
ContentResolver resolver = _context.getContentResolver();
|
||||
try (FileInputStream inStream = new FileInputStream(tempFile);
|
||||
OutputStream outStream = resolver.openOutputStream(fileUri, "wt")
|
||||
) {
|
||||
if (outStream == null) {
|
||||
throw new IOException("openOutputStream returned null");
|
||||
}
|
||||
IOUtils.copy(inStream, outStream);
|
||||
} catch (IOException exception) {
|
||||
throw new VaultRepositoryException(exception);
|
||||
}
|
||||
} catch (VaultRepositoryException | VaultBackupPermissionException exception) {
|
||||
Log.e(TAG, String.format("Unable to create backup: %s", exception));
|
||||
throw exception;
|
||||
} finally {
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private void createBackup(File tempFile, Uri dirUri, int versionsToKeep)
|
||||
throws VaultRepositoryException, VaultBackupPermissionException {
|
||||
FileInfo fileInfo = new FileInfo(FILENAME_PREFIX);
|
||||
@@ -118,6 +161,10 @@ public class VaultBackupManager {
|
||||
}
|
||||
|
||||
private void enforceVersioning(DocumentFile dir, int versionsToKeep) {
|
||||
if (versionsToKeep <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, String.format("Scanning directory %s for backup files", Uri.decode(dir.getUri().toString())));
|
||||
|
||||
List<BackupFile> files = new ArrayList<>();
|
||||
|
||||
@@ -103,8 +103,14 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
entry.setOldGroup(JsonUtils.optString(obj, "group"));
|
||||
}
|
||||
|
||||
VaultEntryIcon icon = VaultEntryIcon.fromJson(obj);
|
||||
entry.setIcon(icon);
|
||||
// Silently ignore any errors that occur when trying to parse the icon of an
|
||||
// entry. This allows us to introduce new icon types in the future (e.g. WebP)
|
||||
// without breaking compatibility with older versions of Aegis.
|
||||
try {
|
||||
VaultEntryIcon icon = VaultEntryIcon.fromJson(obj);
|
||||
entry.setIcon(icon);
|
||||
} catch (VaultEntryIconException ignored) {
|
||||
}
|
||||
|
||||
return entry;
|
||||
} catch (OtpInfoException | JSONException e) {
|
||||
@@ -233,6 +239,10 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
&& getGroups().equals(entry.getGroups());
|
||||
}
|
||||
|
||||
public boolean hasSameNameAndIssuer(VaultEntry entry) {
|
||||
return getName().equals(entry.getName()) && getIssuer().equals(entry.getIssuer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this entry has its values set to the defaults.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
@@ -7,8 +10,6 @@ import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@@ -23,21 +24,23 @@ public class VaultEntryIcon implements Serializable {
|
||||
private final byte[] _hash;
|
||||
private final IconType _type;
|
||||
|
||||
public VaultEntryIcon(byte @NonNull [] bytes, @NonNull IconType type) {
|
||||
public static final int MAX_DIMENS = 512;
|
||||
|
||||
public VaultEntryIcon(@NonNull byte[] bytes, @NonNull IconType type) {
|
||||
this(bytes, type, generateHash(bytes, type));
|
||||
}
|
||||
|
||||
VaultEntryIcon(byte @NonNull [] bytes, @NonNull IconType type, byte @NonNull [] hash) {
|
||||
VaultEntryIcon(@NonNull byte[] bytes, @NonNull IconType type, @NonNull byte[] hash) {
|
||||
_bytes = bytes;
|
||||
_hash = hash;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
public byte @NonNull [] getBytes() {
|
||||
public @NonNull byte[] getBytes() {
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
public byte @NonNull [] getHash() {
|
||||
public @NonNull byte[] getHash() {
|
||||
return _hash;
|
||||
}
|
||||
|
||||
@@ -70,7 +73,7 @@ public class VaultEntryIcon implements Serializable {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static VaultEntryIcon fromJson(@NonNull JSONObject obj) throws VaultEntryException {
|
||||
static VaultEntryIcon fromJson(@NonNull JSONObject obj) throws VaultEntryIconException {
|
||||
try {
|
||||
Object icon = obj.get("icon");
|
||||
if (icon == JSONObject.NULL) {
|
||||
@@ -80,7 +83,7 @@ public class VaultEntryIcon implements Serializable {
|
||||
String mime = JsonUtils.optString(obj, "icon_mime");
|
||||
IconType iconType = mime == null ? IconType.JPEG : IconType.fromMimeType(mime);
|
||||
if (iconType == IconType.INVALID) {
|
||||
throw new VaultEntryException(String.format("Bad icon MIME type: %s", mime));
|
||||
throw new VaultEntryIconException(String.format("Bad icon MIME type: %s", mime));
|
||||
}
|
||||
|
||||
byte[] iconBytes = Base64.decode((String) icon);
|
||||
@@ -92,11 +95,11 @@ public class VaultEntryIcon implements Serializable {
|
||||
|
||||
return new VaultEntryIcon(iconBytes, iconType);
|
||||
} catch (JSONException | EncodingException e) {
|
||||
throw new VaultEntryException(e);
|
||||
throw new VaultEntryIconException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte @NonNull [] generateHash(byte @NonNull [] bytes, @NonNull IconType type) {
|
||||
private static @NonNull byte[] generateHash(@NonNull byte[] bytes, @NonNull IconType type) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
md.update(type.toMimeType().getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
public class VaultEntryIconException extends Exception {
|
||||
public VaultEntryIconException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public VaultEntryIconException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,14 @@ import android.app.backup.BackupManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
@@ -172,8 +174,11 @@ public class VaultManager {
|
||||
try (OutputStream outStream = new FileOutputStream(tempFile)) {
|
||||
_repo.export(outStream);
|
||||
}
|
||||
BackupsVersioningStrategy strategy = _prefs.getBackupVersioningStrategy();
|
||||
Uri uri = _prefs.getBackupsLocation();
|
||||
int versionsToKeep = _prefs.getBackupsVersionCount();
|
||||
|
||||
_backups.scheduleBackup(tempFile, _prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
|
||||
_backups.scheduleBackup(tempFile, strategy, uri, versionsToKeep);
|
||||
} catch (IOException e) {
|
||||
throw new VaultRepositoryException(e);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.util.AtomicFile;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.google.zxing.WriterException;
|
||||
|
||||
@@ -225,6 +226,10 @@ public class VaultRepository {
|
||||
_vault.getEntries().add(entry);
|
||||
}
|
||||
|
||||
public boolean hasEntryByUUID(UUID uuid) {
|
||||
return _vault.getEntries().has(uuid);
|
||||
}
|
||||
|
||||
public VaultEntry getEntryByUUID(UUID uuid) {
|
||||
return _vault.getEntries().getByUUID(uuid);
|
||||
}
|
||||
@@ -245,6 +250,13 @@ public class VaultRepository {
|
||||
return _vault.getEntries().replace(entry);
|
||||
}
|
||||
|
||||
public VaultEntry editEntry(VaultEntry entry, EntryEditor editor) {
|
||||
VaultEntry newEntry = Cloner.clone(entry);
|
||||
editor.edit(newEntry);
|
||||
replaceEntry(newEntry);
|
||||
return newEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves entry1 to the position of entry2.
|
||||
*/
|
||||
@@ -287,8 +299,11 @@ public class VaultRepository {
|
||||
removeGroup(group);
|
||||
}
|
||||
|
||||
public void renameGroup(VaultGroup renamedGroup) {
|
||||
_vault.getGroups().replace(renamedGroup);
|
||||
public void replaceGroups(Collection<VaultGroup> groups) {
|
||||
_vault.getGroups().wipe();
|
||||
for (VaultGroup group : groups) {
|
||||
_vault.getGroups().add(group);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeGroup(VaultGroup group) {
|
||||
@@ -318,6 +333,14 @@ public class VaultRepository {
|
||||
return _vault.isGroupsMigrationFresh();
|
||||
}
|
||||
|
||||
public boolean areIconsOptimized() {
|
||||
return _vault.areIconsOptimized();
|
||||
}
|
||||
|
||||
public void setIconsOptimized(boolean optimized) {
|
||||
_vault.setIconsOptimized(optimized);
|
||||
}
|
||||
|
||||
public VaultFileCredentials getCredentials() {
|
||||
return _creds == null ? null : _creds.clone();
|
||||
}
|
||||
@@ -337,4 +360,8 @@ public class VaultRepository {
|
||||
|
||||
return getCredentials().getSlots().findBackupPasswordSlots().size() > 0;
|
||||
}
|
||||
|
||||
public interface EntryEditor {
|
||||
void edit(VaultEntry entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ public class SlotList extends UUIDMap<Slot> {
|
||||
|
||||
/**
|
||||
* Returns a copy of this SlotList that is suitable for exporting.
|
||||
* Strips biometric slots.
|
||||
* In case there's a backup password slot, any regular password slots are stripped.
|
||||
*/
|
||||
public SlotList exportable() {
|
||||
@@ -85,18 +86,16 @@ public class SlotList extends UUIDMap<Slot> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasBackupSlots) {
|
||||
return this;
|
||||
}
|
||||
|
||||
SlotList slots = new SlotList();
|
||||
for (Slot slot : this) {
|
||||
if (!(slot instanceof PasswordSlot) || ((PasswordSlot) slot).isBackup()) {
|
||||
slots.add(slot);
|
||||
if (slot instanceof BiometricSlot) {
|
||||
continue;
|
||||
}
|
||||
if (hasBackupSlots && slot instanceof PasswordSlot && !((PasswordSlot) slot).isBackup()) {
|
||||
continue;
|
||||
}
|
||||
slots.add(slot);
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
|
||||
package info.guardianproject;
|
||||
|
||||
import info.guardianproject.trustedintents.ApkSignaturePin;
|
||||
|
||||
/**
|
||||
* This is the second Guardian Project APK signing key. It was generated since
|
||||
* RSA 1024-bit keys are deprecated. So any new Guardian Project app will be
|
||||
* signed by this key. It is used to sign these apps:
|
||||
* <ul>
|
||||
* <li>Checkey</li>
|
||||
* <li>Courier</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author hans
|
||||
*/
|
||||
public final class GuardianProjectRSA4096 extends ApkSignaturePin {
|
||||
|
||||
public GuardianProjectRSA4096() {
|
||||
fingerprints = new String[] {
|
||||
"f006a20481c71a690de02e385ab0c9fa4ac1245240f68102682703ba0656867a",
|
||||
};
|
||||
certificates = new byte[][] {
|
||||
{
|
||||
48, -126, 5, -84, 48, -126, 3, -108, 2, 9, 0, -126, -20, 93, -43, 112, 34,
|
||||
-87, 29, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 5, 5, 0, 48, -127,
|
||||
-105, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 83, 49, 17, 48, 15, 6, 3,
|
||||
85, 4, 8, 12, 8, 78, 101, 119, 32, 89, 111, 114, 107, 49, 17, 48, 15, 6, 3,
|
||||
85, 4, 7, 12, 8, 78, 101, 119, 32, 89, 111, 114, 107, 49, 25, 48, 23, 6, 3,
|
||||
85, 4, 10, 12, 16, 71, 117, 97, 114, 100, 105, 97, 110, 32, 80, 114, 111,
|
||||
106, 101, 99, 116, 49, 29, 48, 27, 6, 3, 85, 4, 3, 12, 20, 103, 117, 97,
|
||||
114, 100, 105, 97, 110, 112, 114, 111, 106, 101, 99, 116, 46, 105, 110,
|
||||
102, 111, 49, 40, 48, 38, 6, 9, 42, -122, 72, -122, -9, 13, 1, 9, 1, 22,
|
||||
25, 114, 111, 111, 116, 64, 103, 117, 97, 114, 100, 105, 97, 110, 112, 114,
|
||||
111, 106, 101, 99, 116, 46, 105, 110, 102, 111, 48, 30, 23, 13, 49, 52, 48,
|
||||
53, 49, 52, 49, 55, 53, 55, 50, 57, 90, 23, 13, 52, 49, 48, 57, 50, 56, 49,
|
||||
55, 53, 55, 50, 57, 90, 48, -127, -105, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19,
|
||||
2, 85, 83, 49, 17, 48, 15, 6, 3, 85, 4, 8, 12, 8, 78, 101, 119, 32, 89,
|
||||
111, 114, 107, 49, 17, 48, 15, 6, 3, 85, 4, 7, 12, 8, 78, 101, 119, 32, 89,
|
||||
111, 114, 107, 49, 25, 48, 23, 6, 3, 85, 4, 10, 12, 16, 71, 117, 97, 114,
|
||||
100, 105, 97, 110, 32, 80, 114, 111, 106, 101, 99, 116, 49, 29, 48, 27, 6,
|
||||
3, 85, 4, 3, 12, 20, 103, 117, 97, 114, 100, 105, 97, 110, 112, 114, 111,
|
||||
106, 101, 99, 116, 46, 105, 110, 102, 111, 49, 40, 48, 38, 6, 9, 42, -122,
|
||||
72, -122, -9, 13, 1, 9, 1, 22, 25, 114, 111, 111, 116, 64, 103, 117, 97,
|
||||
114, 100, 105, 97, 110, 112, 114, 111, 106, 101, 99, 116, 46, 105, 110,
|
||||
102, 111, 48, -126, 2, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1,
|
||||
1, 5, 0, 3, -126, 2, 15, 0, 48, -126, 2, 10, 2, -126, 2, 1, 0, -51, 108,
|
||||
83, -44, 61, 81, 46, 61, 2, -61, 46, 60, 2, -30, 44, 38, -70, -63, -93,
|
||||
-66, -57, 1, 2, -32, -80, -82, 98, -26, -70, -34, 60, -65, -55, -43, 16,
|
||||
-63, -89, -104, 50, 108, -26, -2, -3, -55, -77, 47, 75, 89, 99, 12, -32,
|
||||
32, 120, -26, -81, 54, 95, -57, -114, -100, -39, -110, -4, 120, 90, 90,
|
||||
-40, -89, -53, 63, -122, -45, -80, 32, -6, -12, -29, -107, 45, 10, -23, 76,
|
||||
5, 112, -15, -63, 48, 35, -61, 0, -107, -110, 44, -46, -126, -30, -30, -33,
|
||||
86, 22, 30, 8, -72, 29, 75, 121, 31, -120, 119, 59, -57, 19, -64, 31, 84,
|
||||
-94, -38, 91, 82, 92, -95, 66, 2, 120, -37, -113, 126, 54, -83, 83, -125,
|
||||
122, 110, -106, 80, 59, -127, -72, -23, 64, 105, 20, 25, -41, -3, -61, -44,
|
||||
51, -28, 17, 36, -90, -18, -25, 96, 37, -93, -48, 98, 47, 27, 49, 40, -31,
|
||||
-62, -102, -49, 28, -55, 50, -38, -70, -83, 101, 97, 96, -122, -114, 18,
|
||||
47, 119, 117, -7, -55, -64, -1, 96, -120, 61, -89, -70, -7, -89, 113, -41,
|
||||
88, 27, 26, 95, -52, 53, 59, 7, 11, 79, 86, 31, -109, -21, 120, -15, 38,
|
||||
106, 33, -121, 82, 18, 45, 32, 49, 93, -26, -74, -104, 4, -122, 96, 39,
|
||||
126, -24, 16, 119, -45, -119, 110, -31, 55, -109, 53, 53, -11, -58, -124,
|
||||
-84, 41, -42, 64, 17, -5, -78, -57, -100, 118, -105, -38, -94, 84, -16,
|
||||
-53, -106, -11, 76, 81, 70, -83, -56, 73, -96, -100, -18, 55, -3, -3, 34,
|
||||
-97, -62, 59, -17, -79, 91, -9, 122, 95, -108, -121, -76, -62, -72, 61,
|
||||
-82, 105, -99, -59, 10, 51, 52, 77, -105, -127, 37, 79, 88, -127, 27, 39,
|
||||
44, 15, 123, -59, -118, -96, 61, -28, 23, -54, 124, 100, 57, 37, 123, -83,
|
||||
-124, 11, 123, 73, -53, 18, 119, -39, -13, 46, 123, -55, 4, -91, 114, 93,
|
||||
-116, -98, 21, 95, -30, -82, -108, 87, -65, -8, 30, 67, 14, 22, 79, -64,
|
||||
86, -128, -83, 74, 69, -42, 9, -18, -120, -52, 7, 62, 78, 88, -53, -125,
|
||||
41, -122, -91, -34, -110, 111, -118, -25, 25, -83, 90, -95, 84, -121, 95,
|
||||
72, -53, -14, -14, -48, 65, 4, 23, 99, 56, 23, -20, 9, 46, 63, 83, -26,
|
||||
-86, 54, -104, 79, 9, 94, -91, -69, 10, -125, -17, -28, -32, -79, 5, -11,
|
||||
37, 103, -46, -75, 71, -119, 39, 48, -53, -51, 118, 43, 28, 68, 14, -33,
|
||||
-82, -76, -98, 1, 41, 94, -128, -86, 51, -89, 17, -31, 38, -70, -2, -27,
|
||||
-105, -87, 103, 93, 19, 73, -106, -82, -76, 110, -48, -124, -51, 92, -91,
|
||||
-51, 22, 1, -48, 52, -127, -24, 26, -77, 4, 22, 33, 24, -128, -1, 9, -42,
|
||||
23, -53, 78, 10, -39, -115, -95, 17, 100, 90, 16, -23, -127, -38, 10, -62,
|
||||
-64, 113, -115, -114, 78, -11, -124, 113, 113, -42, -66, 114, -98, -40,
|
||||
-41, 2, 3, 1, 0, 1, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 5, 5,
|
||||
0, 3, -126, 2, 1, 0, -54, 125, -113, -9, -45, 82, 2, -79, -87, 31, -17,
|
||||
-10, 117, 16, 1, 67, -63, 57, -49, -81, -90, 48, 7, -58, 89, -29, 120, 84,
|
||||
116, 51, 54, -94, 74, -111, -11, 53, -13, 53, -125, -76, -64, 46, -35, 19,
|
||||
-19, 25, 36, -82, 66, 113, -14, -6, -63, 83, 15, 8, -77, -74, -94, -53,
|
||||
-35, -61, -75, 52, 13, -128, 9, 96, 66, 46, 46, -57, 24, -28, -39, -46, 96,
|
||||
-115, -63, 92, -70, 51, 49, -94, 96, 77, 16, 108, -59, -28, 112, -2, -3,
|
||||
-87, 27, 56, 118, 29, -95, -76, -70, 34, -105, -24, -104, 70, 24, -88, -29,
|
||||
-28, -101, -98, 24, -48, -32, -111, 97, 61, -70, -21, -111, 70, -55, 68,
|
||||
-76, 125, -61, 56, -122, 48, -118, 74, 61, -101, -46, -43, -12, -114, -116,
|
||||
-57, -108, 91, -109, -62, -117, -76, -48, 109, 81, -22, -113, -73, 64, -61,
|
||||
102, 92, -117, -9, -100, 62, -90, 99, 102, -50, -41, -75, 31, 5, -76, -31,
|
||||
-85, 118, -57, -21, 102, 71, 41, -34, -45, -92, 88, 82, -95, 65, -100, -31,
|
||||
-28, -85, -37, 94, -52, 72, 39, 55, -42, 18, 29, 115, 23, -106, -2, -54,
|
||||
-45, 61, 53, 62, 107, 109, -65, 69, 4, -123, 124, -30, -54, -44, 38, 119,
|
||||
49, -123, -27, -50, 77, -9, -40, -114, -50, -70, -123, 86, -115, 127, 45,
|
||||
42, 23, -25, -83, -81, -38, -84, -3, 99, -36, 12, -67, -39, -110, 21, -40,
|
||||
-128, 6, -96, -24, -116, -62, 63, 127, 39, 57, -83, -63, 0, 127, -12, 73,
|
||||
85, -41, -101, -70, -48, -94, -94, -73, -38, -115, -62, -34, 62, -92, 96,
|
||||
16, 6, -19, -84, 38, -93, -117, -52, 32, 92, -21, 123, -117, 81, 50, -71,
|
||||
103, 121, 127, 4, -3, -40, 62, 100, 22, -123, -68, -69, -54, -127, -67, 50,
|
||||
-114, 77, -30, 26, -102, 29, 106, 48, 83, 99, 73, 96, 124, -77, 51, 8, 15,
|
||||
40, 72, -108, 105, 62, 119, -113, -90, 57, -62, 127, -57, -21, -40, -109,
|
||||
96, 101, 71, -40, -101, 127, 69, 110, 43, 59, -102, -8, -42, -70, -24, 51,
|
||||
-51, 54, 42, -110, -119, 41, -101, 45, -101, -124, 56, -75, -26, 86, -65,
|
||||
54, 21, -88, -30, 79, -26, -127, 121, -102, -48, -25, -62, 99, 76, -90,
|
||||
-48, -37, 123, 9, 67, 51, -41, -116, 29, 69, 88, 93, -42, 23, 73, -112, 24,
|
||||
-85, 60, 1, 3, -95, 12, -49, -55, 95, 109, -37, 10, -124, 119, -52, -31,
|
||||
91, 55, -67, 99, 87, -55, 97, 25, -9, -119, -41, -98, 100, -14, 70, -44,
|
||||
-63, 60, -127, -99, 15, 49, 22, -118, -49, 66, -106, 36, -34, -5, 6, -48,
|
||||
123, -79, 115, 57, 30, -3, -34, -67, 91, 34, 3, -52, -106, 79, -63, 125,
|
||||
123, -16, -120, -53, -98, 34, 86, -60, 94, 78, -91, -34, 0, -8, 73, -119,
|
||||
-87, 12, -101, -112, -10, -79, 10, 105, -82, 120, -106, -9, 99, 57, -63,
|
||||
-26, 125, -80, 102, -106, -11, -91, -1, 37, 33
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
|
||||
package info.guardianproject.trustedintents;
|
||||
|
||||
import android.content.pm.Signature;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
public abstract class ApkSignaturePin {
|
||||
|
||||
protected String[] fingerprints; // hex-encoded SHA-256 hashes of the certs
|
||||
protected byte[][] certificates; // array of DER-encoded X.509 certificates
|
||||
private Signature[] signatures;
|
||||
|
||||
public Signature[] getSignatures() {
|
||||
if (signatures == null) {
|
||||
signatures = new Signature[certificates.length];
|
||||
for (int i = 0; i < certificates.length; i++)
|
||||
signatures[i] = new Signature(certificates[i]);
|
||||
}
|
||||
return signatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fingerprint of the first certificate in the signature.
|
||||
*
|
||||
* @param algorithm - Which hash to use (e.g. MD5, SHA1, SHA-256)
|
||||
* @return the fingerprint as hex String
|
||||
*/
|
||||
public String getFingerprint(String algorithm) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance(algorithm);
|
||||
byte[] hashBytes = md.digest(certificates[0]);
|
||||
BigInteger bi = new BigInteger(1, hashBytes);
|
||||
md.reset();
|
||||
return String.format("%0" + (hashBytes.length << 1) + "x", bi);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the MD5 fingerprint of the first certificate in the signature.
|
||||
*
|
||||
* @return the MD5 sum as hex String
|
||||
*/
|
||||
public String getMD5Fingerprint() {
|
||||
return getFingerprint("MD5");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the SHA1 fingerprint of the first certificate in the signature.
|
||||
*
|
||||
* @return the SHA1 sum as hex String
|
||||
*/
|
||||
public String getSHA1Fingerprint() {
|
||||
return getFingerprint("SHA1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the SHA-256 fingerprint of the first certificate in the signature.
|
||||
*
|
||||
* @return the SHA-256 sum as hex String
|
||||
*/
|
||||
public String getSHA256Fingerprint() {
|
||||
return getFingerprint("SHA-256");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the calculated SHA-256 cert fingerprint to the stored one.
|
||||
*
|
||||
* @return the result of the comparison
|
||||
*/
|
||||
public boolean doFingerprintsMatchCertificates() {
|
||||
if (fingerprints == null || certificates == null)
|
||||
return false;
|
||||
String[] calcedFingerprints = new String[certificates.length];
|
||||
for (int i = 0; i < calcedFingerprints.length; i++)
|
||||
calcedFingerprints[i] = getSHA256Fingerprint();
|
||||
if (fingerprints.length == 0 || calcedFingerprints.length == 0)
|
||||
return false;
|
||||
return Arrays.equals(fingerprints, calcedFingerprints);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
@@ -0,0 +1,271 @@
|
||||
|
||||
package info.guardianproject.trustedintents;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.pm.Signature;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.LinkedHashSet;
|
||||
|
||||
public class TrustedIntents {
|
||||
|
||||
private static TrustedIntents instance;
|
||||
|
||||
private static PackageManager pm;
|
||||
|
||||
private final LinkedHashSet<ApkSignaturePin> pinList;
|
||||
|
||||
private TrustedIntents(Context context) {
|
||||
pm = context.getPackageManager();
|
||||
this.pinList = new LinkedHashSet<ApkSignaturePin>();
|
||||
}
|
||||
|
||||
public static TrustedIntents get(Context context) {
|
||||
if (instance == null)
|
||||
instance = new TrustedIntents(context);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a resolved {@link Activity} is trusted.
|
||||
*
|
||||
* @param resolveInfo the one to check
|
||||
* @return whether the {@code Intent}'s receiver is trusted
|
||||
*/
|
||||
public boolean isReceiverTrusted(ResolveInfo resolveInfo) {
|
||||
return isPackageNameTrusted(resolveInfo.activityInfo.packageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a resolved {@link Activity} is trusted.
|
||||
*
|
||||
* @param activityInfo the one to check
|
||||
* @return whether the {@code Intent}'s receiver is trusted
|
||||
*/
|
||||
public boolean isReceiverTrusted(ActivityInfo activityInfo) {
|
||||
return isPackageNameTrusted(activityInfo.packageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check an {@link Intent} is trusted based on the {@code packageName} set
|
||||
* by {@link Intent#setPackage(String)}
|
||||
*
|
||||
* @param intent the one to check
|
||||
* @return whether the {@code Intent}'s receiver is trusted
|
||||
*/
|
||||
public boolean isReceiverTrusted(Intent intent) {
|
||||
if (!isIntentSane(intent))
|
||||
return false;
|
||||
String packageName = intent.getPackage();
|
||||
if (TextUtils.isEmpty(packageName)) {
|
||||
packageName = intent.getComponent().getPackageName();
|
||||
}
|
||||
return isPackageNameTrusted(packageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a {@code packageName} is trusted.
|
||||
*
|
||||
* @param packageName the one to check
|
||||
* @return whether the {@code packageName} is trusted
|
||||
*/
|
||||
public boolean isPackageNameTrusted(String packageName) {
|
||||
try {
|
||||
checkTrustedSigner(packageName);
|
||||
} catch (NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} catch (CertificateException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link Intent} if the sending app is signed by one of
|
||||
* the trusted signing keys as set in {@link #addTrustedSigner(Class)}.
|
||||
*
|
||||
* @returns {@code null} if there is no {@code Intent} or if the
|
||||
* sender is not trusted.
|
||||
* @see #addTrustedSigner(Class)
|
||||
*/
|
||||
public Intent getIntentFromTrustedSender(Activity activity) {
|
||||
Intent intent = activity.getIntent();
|
||||
String packageName = getCallingPackageName(activity);
|
||||
if (TextUtils.isEmpty(packageName)) {
|
||||
return null;
|
||||
}
|
||||
if (isPackageNameTrusted(packageName)) {
|
||||
return intent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the package name of the {@link Activity} that sent the
|
||||
* {@link Intent} that started this {@code Activity}.
|
||||
* <p/>
|
||||
* <strong>WARNING</strong>: If the {@code Activity} has
|
||||
* {@code android:launchMode="singleInstance"} or {@code "singleTask"}, then
|
||||
* this method will not disconnect because it is not possible to get the
|
||||
* calling {@code Activity}, as set by
|
||||
* {@link Activity#startActivityForResult(Intent, int)}
|
||||
*
|
||||
* @param activity the {@code Activity} to check for the {@code Intent}
|
||||
* @return the package of the sending app or {@code null} if it was not a
|
||||
* {@code ACTION_CONNECT Intent} or the {@code Intent} was not sent
|
||||
* with {@link Activity#startActivityForResult(Intent, int)}
|
||||
*/
|
||||
public static String getCallingPackageName(Activity activity) {
|
||||
// getCallingPackage() was unstable until android-18, use this
|
||||
ComponentName componentName = activity.getCallingActivity();
|
||||
if (componentName == null)
|
||||
return null;
|
||||
String packageName = componentName.getPackageName();
|
||||
if (TextUtils.isEmpty(packageName)) {
|
||||
Log.e(activity.getClass().getSimpleName(),
|
||||
"Received Intent without sender! The Intent must be sent using startActivityForResult() and received without launchMode singleTask or singleInstance!");
|
||||
}
|
||||
return packageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to check whether an {@link Intent} that will be sent is
|
||||
* complete. It should <strong>not</strong> be used with {@code Intent}s
|
||||
* that have been received already.
|
||||
*/
|
||||
private boolean isIntentSane(Intent intent) {
|
||||
if (intent == null)
|
||||
return false;
|
||||
if (TextUtils.isEmpty(intent.getPackage())) {
|
||||
ComponentName componentName = intent.getComponent();
|
||||
if (componentName == null || TextUtils.isEmpty(componentName.getPackageName())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an APK signature that is always trusted for any packageName.
|
||||
*
|
||||
* @param cls {@link Class} of the {@link ApkSignaturePin} to trust
|
||||
* @return boolean
|
||||
* @throws {@link IllegalArgumentException} the class cannot be instantiated
|
||||
*/
|
||||
public boolean addTrustedSigner(Class<? extends ApkSignaturePin> cls) {
|
||||
try {
|
||||
Constructor<? extends ApkSignaturePin> constructor = cls.getConstructor();
|
||||
return pinList.add((ApkSignaturePin) constructor.newInstance((Object[]) null));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an APK signature from the trusted set.
|
||||
*
|
||||
* @param cls {@link Class} of the {@link ApkSignaturePin} to remove
|
||||
*/
|
||||
public boolean removeTrustedSigner(Class<? extends ApkSignaturePin> cls) {
|
||||
for (ApkSignaturePin pin : pinList) {
|
||||
if (pin.getClass().equals(cls)) {
|
||||
return pinList.remove(pin);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all {@link ApkSignaturePin}s from the trusted set.
|
||||
*/
|
||||
public boolean removeAllTrustedSigners() {
|
||||
pinList.clear();
|
||||
return pinList.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a {@link ApkSignaturePin} is trusted.
|
||||
*
|
||||
* @param cls {@link Class} of the {@link ApkSignaturePin} to check
|
||||
*/
|
||||
public boolean isTrustedSigner(Class<? extends ApkSignaturePin> cls) {
|
||||
for (ApkSignaturePin pin : pinList) {
|
||||
if (pin.getClass().equals(cls)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void checkTrustedSigner(String packageName)
|
||||
throws NameNotFoundException, CertificateException {
|
||||
PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
|
||||
checkTrustedSigner(packageInfo.signatures);
|
||||
}
|
||||
|
||||
public void checkTrustedSigner(PackageInfo packageInfo)
|
||||
throws NameNotFoundException, CertificateException {
|
||||
checkTrustedSigner(packageInfo.signatures);
|
||||
}
|
||||
|
||||
public void checkTrustedSigner(Signature[] signatures)
|
||||
throws NameNotFoundException, CertificateException {
|
||||
if (signatures == null || signatures.length == 0)
|
||||
throw new CertificateException("signatures cannot be null or empty!");
|
||||
for (int i = 0; i < signatures.length; i++)
|
||||
if (signatures[i] == null || signatures[i].toByteArray().length == 0)
|
||||
throw new CertificateException("Certificates cannot be null or empty!");
|
||||
|
||||
// check whether the APK signer is trusted for all apps
|
||||
for (ApkSignaturePin pin : pinList)
|
||||
if (areSignaturesEqual(signatures, pin.getSignatures()))
|
||||
return; // found a matching trusted APK signer
|
||||
|
||||
throw new CertificateException("APK signatures did not match!");
|
||||
}
|
||||
|
||||
public boolean areSignaturesEqual(Signature[] sigs0, Signature[] sigs1) {
|
||||
// TODO where is Android's implementation of this that I can just call?
|
||||
if (sigs0 == null || sigs1 == null)
|
||||
return false;
|
||||
if (sigs0.length == 0 || sigs1.length == 0)
|
||||
return false;
|
||||
if (sigs0.length != sigs1.length)
|
||||
return false;
|
||||
for (int i = 0; i < sigs0.length; i++)
|
||||
if (!sigs0[i].equals(sigs1[i]))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void startActivity(Context context, Intent intent) throws CertificateException {
|
||||
if (!isIntentSane(intent))
|
||||
throw new ActivityNotFoundException("The intent was null or empty!");
|
||||
String packageName = intent.getPackage();
|
||||
if (TextUtils.isEmpty(packageName)) {
|
||||
packageName = intent.getComponent().getPackageName();
|
||||
intent.setPackage(packageName);
|
||||
}
|
||||
try {
|
||||
checkTrustedSigner(packageName);
|
||||
} catch (NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
throw new ActivityNotFoundException(e.getLocalizedMessage());
|
||||
}
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,6 @@ message MigrationPayload {
|
||||
repeated OtpParameters otp_parameters = 1;
|
||||
int32 version = 2;
|
||||
int32 batch_size = 3;
|
||||
int32 batch_index = 4;
|
||||
optional int32 batch_index = 4;
|
||||
int32 batch_id = 5;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorSecondary" android:state_enabled="true"/>
|
||||
<item android:alpha="0.38" android:color="?attr/colorSecondary"/>
|
||||
</selector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user