Compare commits
103
Commits
v1.4-beta1
...
v2.0-beta1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61ac83b134 | ||
|
|
9c907cf14a | ||
|
|
5f737582fb | ||
|
|
89256dfef5 | ||
|
|
94b4c098de | ||
|
|
1d848947d9 | ||
|
|
43ebe402ce | ||
|
|
ae0b00573f | ||
|
|
efb5b10a76 | ||
|
|
4f38988c0d | ||
|
|
c977b9a064 | ||
|
|
5353e7c3b1 | ||
|
|
b999ccc83b | ||
|
|
fea5b8e75a | ||
|
|
6ce661180e | ||
|
|
230f21b9f0 | ||
|
|
801e677928 | ||
|
|
143b9a85b4 | ||
|
|
71f66e3142 | ||
|
|
a6ab669811 | ||
|
|
a519b31bd3 | ||
|
|
c7ef10367f | ||
|
|
085e8e51dc | ||
|
|
167f2d6dad | ||
|
|
e653e42455 | ||
|
|
158eb4609c | ||
|
|
2242dbc6cd | ||
|
|
ca8f3e49ea | ||
|
|
c3d9ab86c7 | ||
|
|
441a943a9d | ||
|
|
293e2ddeb0 | ||
|
|
2c8a64f943 | ||
|
|
bc6cb35dc0 | ||
|
|
ad138e4a4c | ||
|
|
f4bdf4645b | ||
|
|
fd9a387b4e | ||
|
|
778000aa24 | ||
|
|
40fbe39474 | ||
|
|
bda1a1d5af | ||
|
|
e6e2809c9c | ||
|
|
b91355da47 | ||
|
|
4bb5ee52d9 | ||
|
|
de3fcfdb26 | ||
|
|
ae71febf10 | ||
|
|
7be1a74cfd | ||
|
|
22c93bf5c6 | ||
|
|
68436fba9c | ||
|
|
44ff321f8f | ||
|
|
d9d6733878 | ||
|
|
eae31ef94b | ||
|
|
29ea2df696 | ||
|
|
a6bf3b7c87 | ||
|
|
c6cb390990 | ||
|
|
f7a428b8cb | ||
|
|
f080eaa8f9 | ||
|
|
07c768893a | ||
|
|
ef8c116bdc | ||
|
|
260a3b9c78 | ||
|
|
a5ec7666ec | ||
|
|
ff450c3edf | ||
|
|
e6e179307d | ||
|
|
2c0068bea7 | ||
|
|
6dcde38667 | ||
|
|
ec1913c77a | ||
|
|
3e40cc9c8f | ||
|
|
40d1e22079 | ||
|
|
38bbb60414 | ||
|
|
98d39ee399 | ||
|
|
e324f649ee | ||
|
|
4f8a0b9020 | ||
|
|
b78b22acbb | ||
|
|
737b9da292 | ||
|
|
dea13f56f5 | ||
|
|
1f0c51b45b | ||
|
|
466e296988 | ||
|
|
fb17f48013 | ||
|
|
f203eab3ef | ||
|
|
1bd9cf3a3f | ||
|
|
a795015487 | ||
|
|
0a6c89cd9d | ||
|
|
951241e95d | ||
|
|
ddbe9ccfb7 | ||
|
|
1d44e05866 | ||
|
|
4482a21a54 | ||
|
|
15e28abecd | ||
|
|
1748e2221f | ||
|
|
b1b4c021cf | ||
|
|
a27e3c6364 | ||
|
|
8f7b152af4 | ||
|
|
39f18b79cb | ||
|
|
9a81cccbe2 | ||
|
|
2c6be39f88 | ||
|
|
a6299ffb5f | ||
|
|
6e5a80a000 | ||
|
|
f012c7ce22 | ||
|
|
805a9001e1 | ||
|
|
884a1d07c4 | ||
|
|
e9f6371885 | ||
|
|
1f2e45fd12 | ||
|
|
6a5323b12b | ||
|
|
735c086726 | ||
|
|
d94be2420c | ||
|
|
0b6da5a93c |
@@ -11,9 +11,9 @@ guidelines](/CONTRIBUTING.md#bug-reports) before submitting an issue. -->
|
||||
|
||||
* __Version__:
|
||||
* __Source__: (Google Play/GitHub/F-Droid/?)
|
||||
* __Vault encrypted:__ Yes (with biometric unlock)/Yes/No
|
||||
* __Device:__
|
||||
* __Android version and ROM:__
|
||||
* __Vault encrypted__: Yes (with biometric unlock)/Yes/No
|
||||
* __Device__:
|
||||
* __Android version and ROM__:
|
||||
|
||||
###### Steps to reproduce
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
name: build
|
||||
on: [pull_request, push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build the app
|
||||
run: ./gradlew build
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
language: shell
|
||||
os: linux
|
||||
dist: bionic
|
||||
env:
|
||||
global:
|
||||
- ANDROID_HOME=${HOME}/android
|
||||
- ANDROID_SDK_ROOT=${ANDROID_HOME}
|
||||
- TOOLS=${ANDROID_HOME}/cmdline-tools
|
||||
- PATH=${ANDROID_HOME}/emulator:${TOOLS}/tools/bin:${ANDROID_HOME}/platform-tools:${PATH}
|
||||
before_install:
|
||||
- mkdir -p ${ANDROID_HOME}/licenses
|
||||
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > ${ANDROID_HOME}/licenses/android-sdk-license
|
||||
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> ${ANDROID_HOME}/licenses/android-sdk-license
|
||||
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> ${ANDROID_HOME}/licenses/android-sdk-license
|
||||
install:
|
||||
- mkdir -p ${TOOLS}
|
||||
- travis_retry wget https://dl.google.com/android/repository/commandlinetools-linux-6514223_latest.zip -O ${TOOLS}/tools.zip
|
||||
- unzip ${TOOLS}/tools.zip -d ${TOOLS}
|
||||
- yes | travis_retry sdkmanager "platform-tools" > /dev/null
|
||||
- yes | travis_retry sdkmanager "tools" > /dev/null
|
||||
- yes | travis_retry sdkmanager "build-tools;30.0.1" > /dev/null
|
||||
- yes | travis_retry sdkmanager "platforms;android-30" > /dev/null
|
||||
script:
|
||||
- ./gradlew assemble check
|
||||
+4
-3
@@ -8,7 +8,7 @@ contribution.
|
||||
|
||||
We use [Crowdin](https://crowdin.com/project/aegis-authenticator) to crowdsource
|
||||
translations of Aegis for lots of different languages. __Pull requests that
|
||||
add/update a translation are not longer accepted.__ Crowdin is our single source
|
||||
add/update a translation are no longer accepted.__ Crowdin is our single source
|
||||
of truth for translations, to keep things easy to maintain.
|
||||
|
||||
The top 30 languages are available for translation. It's possible that yours is
|
||||
@@ -96,7 +96,7 @@ __On your PC__:
|
||||
|
||||
```
|
||||
adb logcat -c
|
||||
adb logcat -f debug.log
|
||||
adb logcat > debug.log
|
||||
```
|
||||
|
||||
The logcat command captures the full system log by default, which may expose
|
||||
@@ -106,8 +106,9 @@ __On your PC__:
|
||||
one below:
|
||||
|
||||
```sh
|
||||
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) -f debug.log
|
||||
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) > debug.log
|
||||
```
|
||||
|
||||
_If you are using a debug APK, replace ``com.beemdevelopment.aegis`` with
|
||||
``com.beemdevelopment.aegis.debug``._
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<img align="left" width="80" height="80" src="metadata/en-US/images/icon.png"
|
||||
alt="App icon">
|
||||
|
||||
# Aegis Authenticator [](https://travis-ci.org/github/beemdevelopment/aegis) [](https://crowdin.com/project/aegis-authenticator) [](https://www.buymeacoffee.com/beemdevelopment) [](https://matrix.to/#/#aegis:matrix.org)
|
||||
# Aegis Authenticator [](https://github.com/beemdevelopment/Aegis/actions?query=workflow%3Abuild) [](https://crowdin.com/project/aegis-authenticator) [](https://www.buymeacoffee.com/beemdevelopment) [](https://matrix.to/#/#aegis:matrix.org)
|
||||
|
||||
__Aegis Authenticator__ is a free, secure and open source 2FA app for Android.
|
||||
It aims to provide a secure authenticator for your online services, while also
|
||||
@@ -103,16 +103,27 @@ Certificate fingerprints:
|
||||
SHA256: C6:DB:80:A8:E1:4E:52:30:C1:DE:84:15:EF:82:0D:13:DC:90:1D:8F:E3:3C:F3:AC:B5:7B:68:62:D8:58:A8:23
|
||||
```
|
||||
|
||||
### Community
|
||||
### Icon packs
|
||||
|
||||
- [aegis-icons](https://github.com/krisu5/aegis-icons)
|
||||
Aegis supports icon packs to make it easier to assign icons to the entries in
|
||||
your vault. There are no official icon packs, but the community maintains a
|
||||
number of third-party icon packs you may want to check out. To learn how to
|
||||
create your own Aegis-compatible icon pack, see [the
|
||||
documenation](docs/iconpacks.md).
|
||||
|
||||
While we're still [working on better icon
|
||||
support](https://github.com/beemdevelopment/Aegis/issues/509) in Aegis, krisu5
|
||||
has started a third-party repository for icons that you may want to check out.
|
||||
- [aegis-icons](https://github.com/aegis-icons/aegis-icons)
|
||||
|
||||
[<img width=500 alt="Aegis-icons preview"
|
||||
src="https://raw.githubusercontent.com/krisu5/aegis-icons/master/showcase.png">](https://github.com/krisu5/aegis-icons)
|
||||
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)
|
||||
|
||||
- [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/). 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).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
+61
-37
@@ -13,22 +13,22 @@ def getCmdOutput = { cmd ->
|
||||
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
|
||||
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
|
||||
|
||||
def fileProviderAuthority = "com.beemdevelopment.aegis.fileprovider"
|
||||
def packageName = "com.beemdevelopment.aegis"
|
||||
def fileProviderAuthority = "${packageName}.fileprovider"
|
||||
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.beemdevelopment.aegis"
|
||||
applicationId "${packageName}"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 41
|
||||
versionName "1.4-beta1"
|
||||
versionCode 46
|
||||
versionName "2.0-beta1"
|
||||
multiDexEnabled true
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthority}\"")
|
||||
manifestPlaceholders = [fileProviderAuthority: "${fileProviderAuthority}"]
|
||||
|
||||
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
@@ -42,20 +42,34 @@ android {
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
unitTests {
|
||||
all {
|
||||
maxHeapSize "3g"
|
||||
|
||||
ignoreFailures false
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
ignoreFailures false
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
|
||||
showExceptions true
|
||||
exceptionFormat "full"
|
||||
showCauses true
|
||||
showStackTraces true
|
||||
}
|
||||
}
|
||||
|
||||
includeAndroidResources true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
manifestPlaceholders = [title: "AegisDev", iconName: "ic_launcher_debug"]
|
||||
manifestPlaceholders = [
|
||||
title: "AegisDev",
|
||||
iconName: "ic_launcher_debug",
|
||||
fileProviderAuthority: "${fileProviderAuthorityDebug}"
|
||||
]
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthorityDebug}\"")
|
||||
resValue "bool", "pref_secure_screen_default", "false"
|
||||
postprocessing {
|
||||
removeUnusedCode true
|
||||
@@ -66,7 +80,12 @@ android {
|
||||
}
|
||||
}
|
||||
release {
|
||||
manifestPlaceholders = [title: "Aegis", iconName: "ic_launcher"]
|
||||
manifestPlaceholders = [
|
||||
title: "Aegis",
|
||||
iconName: "ic_launcher",
|
||||
fileProviderAuthority: "${fileProviderAuthority}"
|
||||
]
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthority}\"")
|
||||
resValue "bool", "pref_secure_screen_default", "true"
|
||||
postprocessing {
|
||||
removeUnusedCode true
|
||||
@@ -101,31 +120,33 @@ protobuf {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def cameraxVersion = '1.0.0-beta11'
|
||||
def glideVersion = '4.11.0'
|
||||
def guavaVersion = '30.0'
|
||||
def junitVersion = '5.7.0'
|
||||
def libsuVersion = '2.5.1'
|
||||
def androidTestVersion = '1.3.0'
|
||||
def cameraxVersion = '1.0.0-rc04'
|
||||
def glideVersion = '4.12.0'
|
||||
def guavaVersion = '30.1.1'
|
||||
def junitVersion = '4.13.2'
|
||||
def libsuVersion = '3.0.2'
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||
annotationProcessor 'androidx.annotation:annotation:1.2.0'
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.camera:camera-camera2:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha18"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha23"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
|
||||
implementation "androidx.core:core:1.3.2"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.3.1"
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.getbase:floatingactionbutton:1.10.1'
|
||||
implementation 'com.github.avito-tech:krop:0.44'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation 'com.github.avito-tech:krop:0.52'
|
||||
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
|
||||
implementation "com.github.bumptech.glide:glide:${glideVersion}"
|
||||
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
|
||||
@@ -134,31 +155,34 @@ 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.0.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.13.0'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.15.7'
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
|
||||
implementation 'com.nulab-inc:zxcvbn:1.3.0'
|
||||
implementation 'com.nulab-inc:zxcvbn:1.4.0'
|
||||
implementation 'de.hdodenhof:circleimageview:3.1.0'
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
||||
implementation 'net.lingala.zip4j:zip4j:2.6.4'
|
||||
// NOTE: this is kept at an old version on purpose (something in newer versions breaks the Authenticator Plus importer)
|
||||
implementation 'net.lingala.zip4j:zip4j:2.6.0'
|
||||
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.68'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation "androidx.test:core:${androidTestVersion}"
|
||||
androidTestImplementation "androidx.test:runner:${androidTestVersion}"
|
||||
androidTestImplementation "androidx.test:rules:${androidTestVersion}"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
|
||||
androidTestImplementation 'junit:junit:4.13.1'
|
||||
androidTestImplementation "junit:junit:${junitVersion}"
|
||||
androidTestUtil 'androidx.test:orchestrator:1.3.0'
|
||||
|
||||
testImplementation "androidx.test:core:${androidTestVersion}"
|
||||
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||
testImplementation "junit:junit:${junitVersion}"
|
||||
testImplementation "org.json:json:20210307"
|
||||
testImplementation 'org.robolectric:robolectric:4.5.1'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -18,6 +18,7 @@
|
||||
|
||||
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
|
||||
|
||||
-keep class com.beemdevelopment.aegis.ui.fragments.*
|
||||
-keep class com.beemdevelopment.aegis.importers.** { *; }
|
||||
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
||||
@@ -39,6 +39,19 @@ public abstract class AegisTest {
|
||||
}
|
||||
|
||||
protected VaultManager initVault() {
|
||||
VaultFileCredentials creds = generateCredentials();
|
||||
VaultManager vault = getApp().initVaultManager(new Vault(), creds);
|
||||
try {
|
||||
vault.save(false);
|
||||
} catch (VaultManagerException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
getApp().getPreferences().setIntroDone(true);
|
||||
return vault;
|
||||
}
|
||||
|
||||
protected VaultFileCredentials generateCredentials() {
|
||||
PasswordSlot slot = new PasswordSlot();
|
||||
byte[] salt = CryptoUtils.generateSalt();
|
||||
SCryptParameters scryptParams = new SCryptParameters(
|
||||
@@ -59,17 +72,9 @@ public abstract class AegisTest {
|
||||
| SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
creds.getSlots().add(slot);
|
||||
|
||||
VaultManager vault = getApp().initVaultManager(new Vault(), creds);
|
||||
try {
|
||||
vault.save(false);
|
||||
} catch (VaultManagerException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
getApp().getPreferences().setIntroDone(true);
|
||||
return vault;
|
||||
return creds;
|
||||
}
|
||||
|
||||
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.espresso.AmbiguousViewMatcherException;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.espresso.matcher.RootMatchers;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
@@ -26,7 +25,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onData;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
|
||||
import static androidx.test.espresso.action.ViewActions.clearText;
|
||||
@@ -42,7 +40,6 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.hamcrest.Matchers.anything;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
@@ -79,7 +76,8 @@ public class OverallTest extends AegisTest {
|
||||
|
||||
List<VaultEntry> realEntries = new ArrayList<>(vault.getEntries());
|
||||
for (int i = 0; i < realEntries.size(); i++) {
|
||||
assertTrue(realEntries.get(i).equivalates(entries.get(i)));
|
||||
String message = String.format("%s != %s", realEntries.get(i).toJson().toString(), entries.get(i).toJson().toString());
|
||||
assertTrue(message, realEntries.get(i).equivalates(entries.get(i)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
@@ -92,8 +90,8 @@ public class OverallTest extends AegisTest {
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(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.spinner_group)).perform(click());
|
||||
onData(anything()).atPosition(1).perform(click());
|
||||
onView(withId(R.id.dropdown_group)).perform(click());
|
||||
onView(withText(R.string.new_group)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
@@ -105,9 +103,9 @@ public class OverallTest extends AegisTest {
|
||||
changeSort(R.string.sort_alphabetically_reverse);
|
||||
changeSort(R.string.sort_custom);
|
||||
|
||||
changeFilter(_groupName);
|
||||
/*changeFilter(_groupName);
|
||||
changeFilter(R.string.filter_ungrouped);
|
||||
changeFilter(R.string.all);
|
||||
changeFilter(R.string.all);*/
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
|
||||
@@ -127,13 +125,14 @@ public class OverallTest extends AegisTest {
|
||||
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.action_settings)).perform(click());
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(vault.getCredentials());
|
||||
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
@@ -147,7 +146,7 @@ public class OverallTest extends AegisTest {
|
||||
onView(withText(resId)).perform(click());
|
||||
}
|
||||
|
||||
private void changeFilter(String text) {
|
||||
/*private void changeFilter(String text) {
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.filter)).perform(click());
|
||||
onView(withText(text)).perform(click());
|
||||
@@ -155,31 +154,30 @@ public class OverallTest extends AegisTest {
|
||||
|
||||
private void changeFilter(@IdRes int resId) {
|
||||
changeFilter(ApplicationProvider.getApplicationContext().getString(resId));
|
||||
}
|
||||
}*/
|
||||
|
||||
private void addEntry(VaultEntry entry) {
|
||||
onView(withId(R.id.fab_expand_menu_button)).perform(click());
|
||||
onView(withId(R.id.fab)).perform(click());
|
||||
onView(withId(R.id.fab_enter)).perform(click());
|
||||
|
||||
onView(withId(R.id.accordian_header)).perform(click());
|
||||
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
|
||||
|
||||
if (entry.getInfo().getClass() != TotpInfo.class) {
|
||||
int i = entry.getInfo() instanceof HotpInfo ? 1 : 2;
|
||||
try {
|
||||
onView(withId(R.id.spinner_type)).perform(click());
|
||||
onData(anything()).atPosition(i).perform(click());
|
||||
} catch (AmbiguousViewMatcherException e) {
|
||||
// for some reason, clicking twice is sometimes necessary, otherwise the test fails on the next line
|
||||
onView(withId(R.id.spinner_type)).perform(click());
|
||||
onData(anything()).atPosition(i).perform(click());
|
||||
}
|
||||
String otpType;
|
||||
if (entry.getInfo() instanceof HotpInfo) {
|
||||
onView(withId(R.id.text_counter)).perform(typeText("0"), closeSoftKeyboard());
|
||||
}
|
||||
if (entry.getInfo() instanceof SteamInfo) {
|
||||
onView(withId(R.id.text_digits)).perform(clearText(), typeText("5"), closeSoftKeyboard());
|
||||
otpType = "HOTP";
|
||||
} else if (entry.getInfo() instanceof SteamInfo) {
|
||||
otpType = "Steam";
|
||||
} else if (entry.getInfo() instanceof TotpInfo) {
|
||||
otpType = "TOTP";
|
||||
} else {
|
||||
throw new RuntimeException(String.format("Unexpected entry type: %s", entry.getInfo().getClass().getSimpleName()));
|
||||
}
|
||||
|
||||
onView(withId(R.id.dropdown_type)).perform(click());
|
||||
onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
}
|
||||
|
||||
String secret = Base32.encode(entry.getInfo().getSecret());
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisTest;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SmallTest
|
||||
public class VaultManagerTest extends AegisTest {
|
||||
@Before
|
||||
public void before() {
|
||||
initVault();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToggleEncryption() throws VaultManagerException {
|
||||
getVault().disableEncryption();
|
||||
assertFalse(getVault().isEncryptionEnabled());
|
||||
assertNull(getVault().getCredentials());
|
||||
|
||||
VaultFileCredentials creds = generateCredentials();
|
||||
getVault().enableEncryption(creds);
|
||||
assertTrue(getVault().isEncryptionEnabled());
|
||||
assertNotNull(getVault().getCredentials());
|
||||
assertEquals(getVault().getCredentials().getSlots().findAll(PasswordSlot.class).size(), 1);
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,14 @@
|
||||
|
||||
<application
|
||||
android:name=".AegisApplication"
|
||||
android:allowBackup="false"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupOnly="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:backupAgent=".AegisBackupAgent"
|
||||
android:icon="@mipmap/${iconName}"
|
||||
android:label="Aegis"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
android:theme="@style/Theme.Aegis.Launch"
|
||||
tools:replace="android:theme">
|
||||
<activity android:name=".ui.TransferEntriesActivity"
|
||||
android:label="@string/title_activity_transfer" />
|
||||
@@ -28,8 +30,8 @@
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/title_activity_about" />
|
||||
<activity
|
||||
android:name=".ui.SelectEntriesActivity"
|
||||
android:label="@string/title_activity_select_entries" />
|
||||
android:name=".ui.ImportEntriesActivity"
|
||||
android:label="@string/title_activity_import_entries" />
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:label="${title}">
|
||||
@@ -46,6 +48,13 @@
|
||||
|
||||
<data android:scheme="otpauth" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.ScannerActivity"
|
||||
@@ -57,11 +66,9 @@
|
||||
android:label="@string/title_activity_edit_entry" />
|
||||
<activity
|
||||
android:name=".ui.IntroActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ui.AuthActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
android:name=".ui.AuthActivity" />
|
||||
<activity
|
||||
android:name=".ui.PreferencesActivity"
|
||||
android:label="@string/title_activity_preferences" />
|
||||
@@ -95,6 +102,18 @@
|
||||
android:resource="@xml/file_paths">
|
||||
</meta-data>
|
||||
</provider>
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<package android:name="com.authy.authy" />
|
||||
<package android:name="org.fedorahosted.freeotp" />
|
||||
<package android:name="org.liberty.android.freeotpplus" />
|
||||
<package android:name="com.google.android.apps.authenticator2" />
|
||||
<package android:name="com.azure.authenticator" />
|
||||
<package android:name="com.valvesoftware.android.steam.community" />
|
||||
<package android:name="com.authenticator.authservice2" />
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../../LICENSE
|
||||
@@ -31,6 +31,42 @@
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 2.0</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Support for icon packs</li>
|
||||
<li>Support for participation in Android's backup system (Google Drive, Seedvault)</li>
|
||||
<li>UI refresh (switched to the Material Components theme)</li>
|
||||
<li>Bottom sheet with chips to filter on groups</li>
|
||||
<li>Search in account names by default (and remove the setting)</li>
|
||||
<li>Replaced the FAB with a bottom sheet dialog</li>
|
||||
<li>Reorganization of settings into separate categories</li>
|
||||
<li>Ability to 'share' images of QR codes to scan in Aegis</li>
|
||||
<li>New translations for Bulgarian, Danish, Latvian, Swedish and Ukranian</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The QR code scanner had trouble detecting QR codes on some devices due to low resolution image capture</li>
|
||||
<li>The app would vanish from the recent apps list after locking</li>
|
||||
<li>When importing from Nextcloud, Aegis would report that the file could not be found.</li>
|
||||
<li>The biometrics prompt would not appear on some devices</li>
|
||||
<li>Various other usability, performance and stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 1.4.2</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The app would crash if DocumentsUI is not present on the device</li>
|
||||
<li>The app would close when selecting an icon if auto lock on minimize was enabled</li>
|
||||
<li>Importing from Authy was flaky for entries that have an icon</li>
|
||||
<li>The dark theme was not properly applied to the QR code scanner view</li>
|
||||
<li>The app would crash on plain text export on some devices</li>
|
||||
<li>Importing from Authenticator Plus stopped working</li>
|
||||
</ul>
|
||||
<h3>Version 1.4.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Scanning QR codes stopped working on certain devices (primarily OnePlus)</li>
|
||||
</ul>
|
||||
<h3>Version 1.4</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
@@ -58,6 +94,8 @@
|
||||
<li>The app would crash if an entry was added to the vault twice due to an IO error</li>
|
||||
<li>The app would crash if the device was rotated while a progress dialog was shown</li>
|
||||
<li>The PIN keyboard would show even if a new non-digit password was set</li>
|
||||
<li>The password reminder popup would be occluded by the autofill popup</li>
|
||||
<li>Importing from other apps on Android 11 was broken due to some permission issues</li>
|
||||
</ul>
|
||||
<h3>Version 1.3</h3>
|
||||
<h4>New</h4>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: %2$s;
|
||||
color: %3$s;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre>%1$s</pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -19,6 +19,7 @@ import androidx.lifecycle.LifecycleEventObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconPackManager;
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
@@ -29,6 +30,7 @@ import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -39,15 +41,23 @@ public class AegisApplication extends Application {
|
||||
private VaultManager _manager;
|
||||
private Preferences _prefs;
|
||||
private List<LockListener> _lockListeners;
|
||||
private boolean _blockAutoLock;
|
||||
private IconPackManager _iconPackManager;
|
||||
|
||||
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
|
||||
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
|
||||
|
||||
static {
|
||||
// to access other app's internal storage directory, run libsu commands inside the global mount namespace
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
_prefs = new Preferences(this);
|
||||
_lockListeners = new ArrayList<>();
|
||||
_iconPackManager = new IconPackManager(this);
|
||||
|
||||
Iconics.init(this);
|
||||
Iconics.registerFont(new MaterialDesignIconic());
|
||||
@@ -89,7 +99,7 @@ public class AegisApplication extends Application {
|
||||
}
|
||||
|
||||
if (_vaultFile == null) {
|
||||
_vaultFile = VaultManager.readFile(this);
|
||||
_vaultFile = VaultManager.readVaultFile(this);
|
||||
}
|
||||
|
||||
return _vaultFile;
|
||||
@@ -119,6 +129,10 @@ public class AegisApplication extends Application {
|
||||
return _manager;
|
||||
}
|
||||
|
||||
public IconPackManager getIconPackManager() {
|
||||
return _iconPackManager;
|
||||
}
|
||||
|
||||
public Preferences getPreferences() {
|
||||
return _prefs;
|
||||
}
|
||||
@@ -135,6 +149,15 @@ public class AegisApplication extends Application {
|
||||
_lockListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to block automatic lock on minimization. This should only be called
|
||||
* by activities before invoking an intent that shows a DocumentsUI, because that
|
||||
* action leads AppLifecycleObserver to believe that the app has been minimized.
|
||||
*/
|
||||
public void setBlockAutoLock(boolean block) {
|
||||
_blockAutoLock = block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the vault and the app.
|
||||
* @param userInitiated whether or not the user initiated the lock in MainActivity.
|
||||
@@ -189,7 +212,9 @@ public class AegisApplication extends Application {
|
||||
private class AppLifecycleObserver implements LifecycleEventObserver {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
|
||||
if (event == Lifecycle.Event.ON_STOP && isAutoLockEnabled(Preferences.AUTO_LOCK_ON_MINIMIZE)) {
|
||||
if (event == Lifecycle.Event.ON_STOP
|
||||
&& isAutoLockEnabled(Preferences.AUTO_LOCK_ON_MINIMIZE)
|
||||
&& !_blockAutoLock) {
|
||||
lock(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.backup.BackupAgent;
|
||||
import android.app.backup.BackupDataInput;
|
||||
import android.app.backup.BackupDataOutput;
|
||||
import android.app.backup.FullBackupDataOutput;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class AegisBackupAgent extends BackupAgent {
|
||||
private static final String TAG = BackupAgent.class.getSimpleName();
|
||||
|
||||
private Preferences _prefs;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
_prefs = new Preferences(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onFullBackup(FullBackupDataOutput data) throws IOException {
|
||||
Log.i(TAG, String.format("onFullBackup() called: flags=%d, quota=%d",
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? data.getTransportFlags() : -1,
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1));
|
||||
|
||||
if (!_prefs.isAndroidBackupsEnabled()) {
|
||||
Log.i(TAG, "onFullBackup() skipped: Android backups disabled in preferences");
|
||||
return;
|
||||
}
|
||||
|
||||
// first copy the vault to the files/backup directory
|
||||
createBackupDir();
|
||||
File vaultBackupFile = getVaultBackupFile();
|
||||
try (FileInputStream inStream = VaultManager.getAtomicFile(this).openRead();
|
||||
FileOutputStream outStream = new FileOutputStream(vaultBackupFile)) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
|
||||
deleteBackupDir();
|
||||
throw e;
|
||||
}
|
||||
|
||||
// then call the original implementation so that fullBackupContent specified in AndroidManifest is read
|
||||
try {
|
||||
super.onFullBackup(data);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
|
||||
throw e;
|
||||
} finally {
|
||||
deleteBackupDir();
|
||||
}
|
||||
|
||||
Log.i(TAG, "onFullBackup() finished");
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime) throws IOException {
|
||||
Log.i(TAG, String.format("onRestoreFile() called: dest=%s", destination));
|
||||
super.onRestoreFile(data, size, destination, type, mode, mtime);
|
||||
|
||||
File vaultBackupFile = getVaultBackupFile();
|
||||
if (destination.getCanonicalFile().equals(vaultBackupFile.getCanonicalFile())) {
|
||||
try (InputStream inStream = new FileInputStream(vaultBackupFile)) {
|
||||
VaultManager.writeToFile(this, inStream);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, String.format("onRestoreFile() failed: dest=%s, error=%s", destination, e));
|
||||
throw e;
|
||||
} finally {
|
||||
deleteBackupDir();
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, String.format("onRestoreFile() finished: dest=%s", destination));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
|
||||
super.onQuotaExceeded(backupDataBytes, quotaBytes);
|
||||
Log.e(TAG, String.format("onQuotaExceeded() called: backupDataBytes=%d, quotaBytes=%d", backupDataBytes, quotaBytes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException {
|
||||
|
||||
}
|
||||
|
||||
private void createBackupDir() throws IOException {
|
||||
File dir = getVaultBackupFile().getParentFile();
|
||||
if (!dir.exists() && !dir.mkdir()) {
|
||||
throw new IOException(String.format("Unable to create backup directory: %s", dir.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteBackupDir() {
|
||||
File dir = getVaultBackupFile().getParentFile();
|
||||
IOUtils.clearDirectory(dir, true);
|
||||
}
|
||||
|
||||
private File getVaultBackupFile() {
|
||||
return new File(new File(getFilesDir(), "backup"), VaultManager.FILENAME);
|
||||
}
|
||||
}
|
||||
@@ -37,10 +37,6 @@ public class Preferences {
|
||||
return _prefs.getBoolean("pref_tap_to_reveal", false);
|
||||
}
|
||||
|
||||
public boolean isSearchAccountNameEnabled() {
|
||||
return _prefs.getBoolean("pref_search_names", false);
|
||||
}
|
||||
|
||||
public boolean isEntryHighlightEnabled() {
|
||||
return _prefs.getBoolean("pref_highlight_entry", false);
|
||||
}
|
||||
@@ -168,6 +164,14 @@ public class Preferences {
|
||||
return new Locale(parts[0], parts[1]);
|
||||
}
|
||||
|
||||
public boolean isAndroidBackupsEnabled() {
|
||||
return _prefs.getBoolean("pref_android_backups", false);
|
||||
}
|
||||
|
||||
public void setIsAndroidBackupsEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_android_backups", enabled).apply();
|
||||
}
|
||||
|
||||
public boolean isBackupsEnabled() {
|
||||
return _prefs.getBoolean("pref_backups", false);
|
||||
}
|
||||
|
||||
@@ -10,14 +10,20 @@ public class ThemeMap {
|
||||
}
|
||||
|
||||
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.AppTheme,
|
||||
Theme.DARK, R.style.AppTheme_Dark,
|
||||
Theme.AMOLED, R.style.AppTheme_TrueBlack
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_Default,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_Default,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Default
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> NO_ACTION_BAR = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.AppTheme_Light_NoActionBar,
|
||||
Theme.DARK, R.style.AppTheme_Dark_NoActionBar,
|
||||
Theme.AMOLED, R.style.AppTheme_TrueBlack_NoActionBar
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_NoActionBar,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_NoActionBar,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_NoActionBar
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_Fullscreen,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_Fullscreen,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Fullscreen
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.biometric.BiometricConstants;
|
||||
import androidx.biometric.BiometricManager;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
public class BiometricsHelper {
|
||||
private BiometricsHelper() {
|
||||
@@ -12,16 +12,16 @@ public class BiometricsHelper {
|
||||
|
||||
public static BiometricManager getManager(Context context) {
|
||||
BiometricManager manager = BiometricManager.from(context);
|
||||
if (manager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
if (manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
return manager;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isCanceled(int errorCode) {
|
||||
return errorCode == BiometricConstants.ERROR_CANCELED
|
||||
|| errorCode == BiometricConstants.ERROR_USER_CANCELED
|
||||
|| errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON;
|
||||
return errorCode == BiometricPrompt.ERROR_CANCELED
|
||||
|| errorCode == BiometricPrompt.ERROR_USER_CANCELED
|
||||
|| errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON;
|
||||
}
|
||||
|
||||
public static boolean isAvailable(Context context) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DropdownHelper {
|
||||
private DropdownHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static void fillDropdown(Context context, AutoCompleteTextView dropdown, @ArrayRes int textArrayResId) {
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, R.layout.dropdown_list_item);
|
||||
dropdown.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public static <T> void fillDropdown(Context context, AutoCompleteTextView dropdown, List<T> items) {
|
||||
ArrayAdapter<T> adapter = new ArrayAdapter<>(context, R.layout.dropdown_list_item, items);
|
||||
dropdown.setAdapter(adapter);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import android.view.View;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
||||
|
||||
public class FabScrollHelper {
|
||||
private View _fabMenu;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.os.Build;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
|
||||
public class IconViewHelper {
|
||||
private IconViewHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layer type of the given ImageView based on the given IconType. If the
|
||||
* icon type is SVG and SDK <= 27, the layer type is set to software. Otherwise, it
|
||||
* is set to hardware.
|
||||
*/
|
||||
public static void setLayerType(ImageView view, IconType iconType) {
|
||||
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
|
||||
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
|
||||
}
|
||||
|
||||
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
public class MetricsHelper {
|
||||
private MetricsHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static int convertDpToPixels(Context context, float dp) {
|
||||
return (int) (dp * (context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.ImageAnalysis;
|
||||
@@ -23,6 +26,7 @@ import static android.graphics.ImageFormat.YUV_444_888;
|
||||
|
||||
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
|
||||
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
|
||||
public static final Size RESOLUTION = new Size(1200, 1600);
|
||||
|
||||
private final QrCodeAnalyzer.Listener _listener;
|
||||
|
||||
@@ -53,7 +57,7 @@ public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
|
||||
try {
|
||||
Result result = reader.decode(bitmap);
|
||||
if (_listener != null) {
|
||||
_listener.onQrCodeDetected(result);
|
||||
new Handler(Looper.getMainLooper()).post(() -> _listener.onQrCodeDetected(result));
|
||||
}
|
||||
} catch (ChecksumException | FormatException | NotFoundException ignored) {
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
|
||||
public class SpinnerHelper {
|
||||
private SpinnerHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static void fillSpinner(Context context, Spinner spinner, @ArrayRes int textArrayResId) {
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, android.R.layout.simple_spinner_item);
|
||||
initSpinner(spinner, adapter);
|
||||
}
|
||||
|
||||
public static <T> void fillSpinner(Context context, Spinner spinner, List<T> items) {
|
||||
ArrayAdapter adapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, items);
|
||||
initSpinner(spinner, adapter);
|
||||
}
|
||||
|
||||
private static void initSpinner(Spinner spinner, ArrayAdapter adapter) {
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
spinner.setAdapter(adapter);
|
||||
spinner.invalidate();
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,6 @@ public class TextDrawableHelper {
|
||||
.width(view.getLayoutParams().width)
|
||||
.height(view.getLayoutParams().height)
|
||||
.endConfig()
|
||||
.buildRect(text.substring(0, 1).toUpperCase(), color);
|
||||
.buildRound(text.substring(0, 1).toUpperCase(), color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
package com.beemdevelopment.aegis.icons;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IconPack {
|
||||
private UUID _uuid;
|
||||
private String _name;
|
||||
private int _version;
|
||||
private List<Icon> _icons;
|
||||
|
||||
private File _dir;
|
||||
|
||||
private IconPack(UUID uuid, String name, int version, List<Icon> icons) {
|
||||
_uuid = uuid;
|
||||
_name = name;
|
||||
_version = version;
|
||||
_icons = icons;
|
||||
}
|
||||
|
||||
public UUID getUUID() {
|
||||
return _uuid;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return _version;
|
||||
}
|
||||
|
||||
public List<Icon> getIcons() {
|
||||
return Collections.unmodifiableList(_icons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of icons suggested for the given issuer.
|
||||
*/
|
||||
public List<Icon> getSuggestedIcons(String issuer) {
|
||||
if (issuer == null || issuer.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return _icons.stream()
|
||||
.filter(i -> i.isSuggestedFor(issuer))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public File getDirectory() {
|
||||
return _dir;
|
||||
}
|
||||
|
||||
void setDirectory(@NonNull File dir) {
|
||||
_dir = dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether some other object is "equal to" this one. The object does not
|
||||
* necessarily have to be the same instance. Equality of UUID and version will make
|
||||
* this method return true;
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof IconPack)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IconPack pack = (IconPack) o;
|
||||
return super.equals(pack) || (getUUID().equals(pack.getUUID()) && getVersion() == pack.getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(_uuid, _version);
|
||||
}
|
||||
|
||||
public static IconPack fromJson(JSONObject obj) throws JSONException {
|
||||
UUID uuid;
|
||||
String uuidString = obj.getString("uuid");
|
||||
try {
|
||||
uuid = UUID.fromString(uuidString);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new JSONException(String.format("Bad UUID format: %s", uuidString));
|
||||
}
|
||||
String name = obj.getString("name");
|
||||
int version = obj.getInt("version");
|
||||
JSONArray array = obj.getJSONArray("icons");
|
||||
|
||||
List<Icon> icons = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
Icon icon = Icon.fromJson(array.getJSONObject(i));
|
||||
icons.add(icon);
|
||||
}
|
||||
|
||||
return new IconPack(uuid, name, version, icons);
|
||||
}
|
||||
|
||||
public static IconPack fromBytes(byte[] data) throws JSONException {
|
||||
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
|
||||
return IconPack.fromJson(obj);
|
||||
}
|
||||
|
||||
public static class Icon implements Serializable {
|
||||
private final String _relFilename;
|
||||
private final String _category;
|
||||
private final List<String> _issuers;
|
||||
|
||||
private File _file;
|
||||
|
||||
protected Icon(String filename, String category, List<String> issuers) {
|
||||
_relFilename = filename;
|
||||
_category = category;
|
||||
_issuers = issuers;
|
||||
}
|
||||
|
||||
public String getRelativeFilename() {
|
||||
return _relFilename;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public File getFile() {
|
||||
return _file;
|
||||
}
|
||||
|
||||
void setFile(@NonNull File file) {
|
||||
_file = file;
|
||||
}
|
||||
|
||||
public IconType getIconType() {
|
||||
return IconType.fromFilename(_relFilename);
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public String getName() {
|
||||
return Files.getNameWithoutExtension(new File(_relFilename).getName());
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return _category;
|
||||
}
|
||||
|
||||
public List<String> getIssuers() {
|
||||
return Collections.unmodifiableList(_issuers);
|
||||
}
|
||||
|
||||
public boolean isSuggestedFor(String issuer) {
|
||||
return getIssuers().stream()
|
||||
.anyMatch(is -> is.toLowerCase().contains(issuer.toLowerCase()));
|
||||
}
|
||||
|
||||
public static Icon fromJson(JSONObject obj) throws JSONException {
|
||||
String filename = obj.getString("filename");
|
||||
String category = obj.isNull("category") ? null : obj.getString("category");
|
||||
JSONArray array = obj.getJSONArray("issuer");
|
||||
|
||||
List<String> issuers = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
String issuer = array.getString(i);
|
||||
issuers.add(issuer);
|
||||
}
|
||||
|
||||
return new Icon(filename, category, issuers);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.beemdevelopment.aegis.icons;
|
||||
|
||||
public class IconPackException extends Exception {
|
||||
public IconPackException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public IconPackException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.beemdevelopment.aegis.icons;
|
||||
|
||||
public class IconPackExistsException extends IconPackException {
|
||||
private IconPack _pack;
|
||||
|
||||
public IconPackExistsException(IconPack pack) {
|
||||
super(String.format("Icon pack %s (%d) already exists", pack.getName(), pack.getVersion()));
|
||||
_pack = pack;
|
||||
}
|
||||
|
||||
public IconPack getIconPack() {
|
||||
return _pack;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package com.beemdevelopment.aegis.icons;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
import net.lingala.zip4j.ZipFile;
|
||||
import net.lingala.zip4j.io.inputstream.ZipInputStream;
|
||||
import net.lingala.zip4j.model.FileHeader;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IconPackManager {
|
||||
private static final String _packDefFilename = "pack.json";
|
||||
|
||||
private File _iconsBaseDir;
|
||||
private List<IconPack> _iconPacks;
|
||||
|
||||
public IconPackManager(Context context) {
|
||||
_iconPacks = new ArrayList<>();
|
||||
_iconsBaseDir = new File(context.getFilesDir(), "icons");
|
||||
rescanIconPacks();
|
||||
}
|
||||
|
||||
private IconPack getIconPackByUUID(UUID uuid) {
|
||||
List<IconPack> packs = _iconPacks.stream().filter(i -> i.getUUID().equals(uuid)).collect(Collectors.toList());
|
||||
if (packs.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return packs.get(0);
|
||||
}
|
||||
|
||||
public List<IconPack> getIconPacks() {
|
||||
return new ArrayList<>(_iconPacks);
|
||||
}
|
||||
|
||||
public void removeIconPack(IconPack pack) throws IconPackException {
|
||||
try {
|
||||
File dir = getIconPackDir(pack);
|
||||
deleteDir(dir);
|
||||
} catch (IOException e) {
|
||||
throw new IconPackException(e);
|
||||
}
|
||||
|
||||
_iconPacks.remove(pack);
|
||||
}
|
||||
|
||||
public IconPack importPack(File inFile) throws IconPackException {
|
||||
try {
|
||||
// read and parse the icon pack definition file of the icon pack
|
||||
ZipFile zipFile = new ZipFile(inFile);
|
||||
FileHeader packHeader = zipFile.getFileHeader(_packDefFilename);
|
||||
if (packHeader == null) {
|
||||
throw new IOException("Unable to find pack.json in the root of the ZIP file");
|
||||
}
|
||||
IconPack pack;
|
||||
byte[] defBytes;
|
||||
try (ZipInputStream inStream = zipFile.getInputStream(packHeader)) {
|
||||
defBytes = IOUtils.readAll(inStream);
|
||||
pack = IconPack.fromBytes(defBytes);
|
||||
}
|
||||
|
||||
// create a new directory to store the icon pack, based on the UUID and version
|
||||
File packDir = getIconPackDir(pack);
|
||||
if (!packDir.getCanonicalPath().startsWith(_iconsBaseDir.getCanonicalPath() + File.separator)) {
|
||||
throw new IOException("Attempted to write outside of the parent directory");
|
||||
}
|
||||
if (packDir.exists()) {
|
||||
throw new IconPackExistsException(pack);
|
||||
}
|
||||
IconPack existingPack = getIconPackByUUID(pack.getUUID());
|
||||
if (existingPack != null) {
|
||||
throw new IconPackExistsException(existingPack);
|
||||
}
|
||||
if (!packDir.exists() && !packDir.mkdirs()) {
|
||||
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
|
||||
}
|
||||
|
||||
// extract each of the defined icons to the icon pack directory
|
||||
for (IconPack.Icon icon : pack.getIcons()) {
|
||||
File destFile = new File(packDir, icon.getRelativeFilename());
|
||||
FileHeader iconHeader = zipFile.getFileHeader(icon.getRelativeFilename());
|
||||
if (iconHeader == null) {
|
||||
throw new IOException(String.format("Unable to find %s relative to the root of the ZIP file", icon.getRelativeFilename()));
|
||||
}
|
||||
|
||||
// create new directories for this file if needed
|
||||
File parent = destFile.getParentFile();
|
||||
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
||||
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
|
||||
}
|
||||
|
||||
try (ZipInputStream inStream = zipFile.getInputStream(iconHeader);
|
||||
FileOutputStream outStream = new FileOutputStream(destFile)) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
}
|
||||
|
||||
// after successful copy of the icon, store the new filename
|
||||
icon.setFile(destFile);
|
||||
}
|
||||
|
||||
// write the icon pack definition file to the newly created directory
|
||||
try (FileOutputStream outStream = new FileOutputStream(new File(packDir, _packDefFilename))) {
|
||||
outStream.write(defBytes);
|
||||
}
|
||||
|
||||
// after successful extraction of the icon pack, store the new directory
|
||||
pack.setDirectory(packDir);
|
||||
_iconPacks.add(pack);
|
||||
return pack;
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new IconPackException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void rescanIconPacks() {
|
||||
_iconPacks.clear();
|
||||
|
||||
File[] dirs = _iconsBaseDir.listFiles();
|
||||
if (dirs == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File dir : dirs) {
|
||||
if (!dir.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UUID uuid;
|
||||
try {
|
||||
uuid = UUID.fromString(dir.getName());
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
continue;
|
||||
}
|
||||
|
||||
File versionDir = getLatestVersionDir(dir);
|
||||
if (versionDir != null) {
|
||||
IconPack pack;
|
||||
try (FileInputStream inStream = new FileInputStream(new File(versionDir, _packDefFilename))) {
|
||||
byte[] bytes = IOUtils.readAll(inStream);
|
||||
pack = IconPack.fromBytes(bytes);
|
||||
pack.setDirectory(versionDir);
|
||||
} catch (JSONException | IOException e) {
|
||||
e.printStackTrace();
|
||||
continue;
|
||||
}
|
||||
|
||||
for (IconPack.Icon icon : pack.getIcons()) {
|
||||
icon.setFile(new File(versionDir, icon.getRelativeFilename()));
|
||||
}
|
||||
|
||||
// do a sanity check on the UUID and version
|
||||
if (pack.getUUID().equals(uuid) && pack.getVersion() == Integer.parseInt(versionDir.getName())) {
|
||||
_iconPacks.add(pack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File getIconPackDir(IconPack pack) {
|
||||
return new File(_iconsBaseDir, pack.getUUID() + File.separator + pack.getVersion());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static File getLatestVersionDir(File packDir) {
|
||||
File[] dirs = packDir.listFiles();
|
||||
if (dirs == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int latestVersion = -1;
|
||||
for (File versionDir : dirs) {
|
||||
int version;
|
||||
try {
|
||||
version = Integer.parseInt(versionDir.getName());
|
||||
} catch (NumberFormatException ignored) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (latestVersion == -1 || version > latestVersion) {
|
||||
latestVersion = version;
|
||||
}
|
||||
}
|
||||
|
||||
if (latestVersion == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new File(packDir, Integer.toString(latestVersion));
|
||||
}
|
||||
|
||||
private static void deleteDir(File dir) throws IOException {
|
||||
if (dir.isDirectory()) {
|
||||
File[] children = dir.listFiles();
|
||||
if (children != null) {
|
||||
for (File child : children) {
|
||||
deleteDir(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!dir.delete()) {
|
||||
throw new IOException(String.format("Unable to delete directory: %s", dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.beemdevelopment.aegis.icons;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
|
||||
public enum IconType {
|
||||
INVALID,
|
||||
SVG,
|
||||
PNG,
|
||||
JPEG;
|
||||
|
||||
public static IconType fromMimeType(String mimeType) {
|
||||
switch (mimeType) {
|
||||
case "image/svg+xml":
|
||||
return SVG;
|
||||
case "image/png":
|
||||
return PNG;
|
||||
case "image/jpeg":
|
||||
return JPEG;
|
||||
default:
|
||||
return INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public static IconType fromFilename(String filename) {
|
||||
switch (Files.getFileExtension(filename).toLowerCase()) {
|
||||
case "svg":
|
||||
return SVG;
|
||||
case "png":
|
||||
return PNG;
|
||||
case "jpg":
|
||||
// intentional fallthrough
|
||||
case "jpeg":
|
||||
return JPEG;
|
||||
default:
|
||||
return INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
public String toMimeType() {
|
||||
switch (this) {
|
||||
case SVG:
|
||||
return "image/svg+xml";
|
||||
case PNG:
|
||||
return "image/png";
|
||||
case JPEG:
|
||||
return "image/jpeg";
|
||||
default:
|
||||
throw new RuntimeException(String.format("Can't convert icon type %s to MIME type", this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.beemdevelopment.aegis.helpers.ContextHelper;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryException;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
@@ -18,6 +24,7 @@ import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class AegisImporter extends DatabaseImporter {
|
||||
|
||||
@@ -56,14 +63,46 @@ public class AegisImporter extends DatabaseImporter {
|
||||
return _file.getHeader().getSlots();
|
||||
}
|
||||
|
||||
public State decrypt(VaultFileCredentials creds) throws VaultFileException {
|
||||
JSONObject obj = _file.getContent(creds);
|
||||
public State decrypt(VaultFileCredentials creds) throws DatabaseImporterException {
|
||||
JSONObject obj;
|
||||
try {
|
||||
obj = _file.getContent(creds);
|
||||
} catch (VaultFileException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return new DecryptedState(obj);
|
||||
}
|
||||
|
||||
public State decrypt(char[] password) throws DatabaseImporterException {
|
||||
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
|
||||
PasswordSlotDecryptTask.Result result = PasswordSlotDecryptTask.decrypt(slots, password);
|
||||
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
|
||||
return decrypt(creds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, (Dialogs.TextInputListener) password -> {
|
||||
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
|
||||
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
|
||||
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(context, result -> {
|
||||
try {
|
||||
if (result == null) {
|
||||
throw new DatabaseImporterException("Password incorrect");
|
||||
}
|
||||
|
||||
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
|
||||
State state = decrypt(creds);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
|
||||
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
|
||||
task.execute(lifecycle, params);
|
||||
}, (DialogInterface.OnCancelListener) dialog -> listener.onCanceled());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +139,7 @@ public class AegisImporter extends DatabaseImporter {
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
return VaultEntry.fromJson(obj);
|
||||
} catch (JSONException | OtpInfoException | EncodingException e) {
|
||||
} catch (VaultEntryException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ 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;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
@@ -95,7 +95,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
_data = data;
|
||||
}
|
||||
|
||||
private DecryptedState decryptData(SecretKey key, int offset) throws DatabaseImporterException {
|
||||
private DecryptedState decryptContent(SecretKey key, int offset) throws DatabaseImporterException {
|
||||
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
|
||||
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
|
||||
CryptParameters params = new CryptParameters(nonce, tag);
|
||||
@@ -116,35 +116,52 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
private KeyDerivationParams getKeyDerivationParams(char[] password) throws DatabaseImporterException {
|
||||
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
|
||||
int iterations = ByteBuffer.wrap(iterBytes).getInt();
|
||||
if (iterations < 1) {
|
||||
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
|
||||
}
|
||||
|
||||
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, INT_SIZE + SALT_SIZE);
|
||||
return new KeyDerivationParams(password, salt, iterations);
|
||||
}
|
||||
|
||||
protected DecryptedState decryptOldFormat(char[] password) throws DatabaseImporterException {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this exists solely to support the old andOTP backup format
|
||||
// it is not a secure way to derive a key from a password
|
||||
MessageDigest hash;
|
||||
try {
|
||||
hash = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
return decryptContent(key, 0);
|
||||
}
|
||||
|
||||
protected DecryptedState decryptNewFormat(SecretKey key) throws DatabaseImporterException {
|
||||
return decryptContent(key, INT_SIZE + SALT_SIZE);
|
||||
}
|
||||
|
||||
protected DecryptedState decryptNewFormat(char[] password)
|
||||
throws DatabaseImporterException {
|
||||
KeyDerivationParams params = getKeyDerivationParams(password);
|
||||
SecretKey key = AndOtpKeyDerivationTask.deriveKey(params);
|
||||
return decryptNewFormat(key);
|
||||
}
|
||||
|
||||
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
|
||||
if (oldFormat) {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this exists solely to support the old andOTP backup format
|
||||
// it is not a secure way to derive a key from a password
|
||||
MessageDigest hash;
|
||||
try {
|
||||
hash = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
DecryptedState state = decryptData(key, 0);
|
||||
DecryptedState state = decryptOldFormat(password);
|
||||
listener.onStateDecrypted(state);
|
||||
} else {
|
||||
int offset = INT_SIZE + SALT_SIZE;
|
||||
|
||||
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
|
||||
int iterations = ByteBuffer.wrap(iterBytes).getInt();
|
||||
if (iterations < 1) {
|
||||
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
|
||||
}
|
||||
|
||||
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
|
||||
AndOtpKeyDerivationTask.Params params = new AndOtpKeyDerivationTask.Params(password, salt, iterations);
|
||||
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key1 -> {
|
||||
KeyDerivationParams params = getKeyDerivationParams(password);
|
||||
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key -> {
|
||||
try {
|
||||
DecryptedState state = decryptData(key1, offset);
|
||||
DecryptedState state = decryptNewFormat(key);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
@@ -173,7 +190,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
}, dialog1 -> listener.onCanceled());
|
||||
})
|
||||
.create());
|
||||
}
|
||||
@@ -222,7 +239,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
|
||||
break;
|
||||
case "steam":
|
||||
info = new SteamInfo(secret, algo, digits, obj.optInt("period", 30));
|
||||
info = new SteamInfo(secret, algo, digits, obj.optInt("period", TotpInfo.DEFAULT_PERIOD));
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseImporterException("unsupported otp type: " + type);
|
||||
@@ -251,7 +268,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
private static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpKeyDerivationTask.Params, SecretKey> {
|
||||
protected static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpImporter.KeyDerivationParams, SecretKey> {
|
||||
private Callback _cb;
|
||||
|
||||
public AndOtpKeyDerivationTask(Context context, Callback cb) {
|
||||
@@ -260,20 +277,22 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SecretKey doInBackground(AndOtpKeyDerivationTask.Params... args) {
|
||||
protected SecretKey doInBackground(AndOtpImporter.KeyDerivationParams... args) {
|
||||
setPriority();
|
||||
|
||||
AndOtpKeyDerivationTask.Params params = args[0];
|
||||
SecretKey key;
|
||||
AndOtpImporter.KeyDerivationParams params = args[0];
|
||||
return deriveKey(params);
|
||||
}
|
||||
|
||||
protected static SecretKey deriveKey(KeyDerivationParams params) {
|
||||
try {
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
|
||||
key = factory.generateSecret(spec);
|
||||
SecretKey key = factory.generateSecret(spec);
|
||||
return new SecretKeySpec(key.getEncoded(), "AES");
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -282,32 +301,32 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
_cb.onTaskFinished(key);
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
private char[] _password;
|
||||
private byte[] _salt;
|
||||
private int _iterations;
|
||||
|
||||
public Params(char[] password, byte[] salt, int iterations) {
|
||||
_iterations = iterations;
|
||||
_password = password;
|
||||
_salt = salt;
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return _password;
|
||||
}
|
||||
|
||||
public int getIterations() {
|
||||
return _iterations;
|
||||
}
|
||||
|
||||
public byte[] getSalt() {
|
||||
return _salt;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(SecretKey key);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class KeyDerivationParams {
|
||||
private final char[] _password;
|
||||
private final byte[] _salt;
|
||||
private final int _iterations;
|
||||
|
||||
public KeyDerivationParams(char[] password, byte[] salt, int iterations) {
|
||||
_iterations = iterations;
|
||||
_password = password;
|
||||
_salt = salt;
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return _password;
|
||||
}
|
||||
|
||||
public int getIterations() {
|
||||
return _iterations;
|
||||
}
|
||||
|
||||
public byte[] getSalt() {
|
||||
return _salt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-18
@@ -2,7 +2,7 @@ package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
@@ -37,34 +37,41 @@ public class AuthenticatorPlusImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private byte[] _data;
|
||||
private final byte[] _data;
|
||||
|
||||
private EncryptedState(byte[] data) {
|
||||
super(true);
|
||||
_data = data;
|
||||
}
|
||||
|
||||
protected State decrypt(char[] password) throws DatabaseImporterException {
|
||||
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
|
||||
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
|
||||
LocalFileHeader header;
|
||||
while ((header = zipStream.getNextEntry()) != null) {
|
||||
File file = new File(header.getFileName());
|
||||
if (file.getName().equals(FILENAME)) {
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(null);
|
||||
return importer.read(zipStream);
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(FILENAME);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
|
||||
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
|
||||
LocalFileHeader header;
|
||||
while ((header = zipStream.getNextEntry()) != null) {
|
||||
File file = new File(header.getFileName());
|
||||
if (file.getName().equals(FILENAME)) {
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(context);
|
||||
DatabaseImporter.State state = importer.read(zipStream);
|
||||
listener.onStateDecrypted(state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(FILENAME);
|
||||
} catch (IOException | DatabaseImporterException e) {
|
||||
try {
|
||||
DatabaseImporter.State state = decrypt(password);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
}, dialog1 -> listener.onCanceled());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
@@ -42,6 +42,7 @@ import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class AuthyImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs";
|
||||
@@ -154,47 +155,56 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
_array = array;
|
||||
}
|
||||
|
||||
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
|
||||
try {
|
||||
for (int i = 0; i < _array.length(); i++) {
|
||||
JSONObject obj = _array.getJSONObject(i);
|
||||
String secretString = JsonUtils.optString(obj, "encryptedSecret");
|
||||
if (secretString == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] encryptedSecret = Base64.decode(secretString);
|
||||
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
|
||||
SecretKey key = factory.generateSecret(spec);
|
||||
key = new SecretKeySpec(key.getEncoded(), "AES");
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
|
||||
|
||||
byte[] secret = cipher.doFinal(encryptedSecret);
|
||||
obj.remove("encryptedSecret");
|
||||
obj.remove("salt");
|
||||
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
return new DecryptedState(_array);
|
||||
} catch (JSONException
|
||||
| EncodingException
|
||||
| NoSuchAlgorithmException
|
||||
| InvalidKeySpecException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| NoSuchPaddingException
|
||||
| BadPaddingException
|
||||
| IllegalBlockSizeException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
|
||||
try {
|
||||
for (int i = 0; i < _array.length(); i++) {
|
||||
JSONObject obj = _array.getJSONObject(i);
|
||||
String secretString = JsonUtils.optString(obj, "encryptedSecret");
|
||||
if (secretString == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] encryptedSecret = Base64.decode(secretString);
|
||||
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
|
||||
SecretKey key = factory.generateSecret(spec);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
|
||||
|
||||
byte[] secret = cipher.doFinal(encryptedSecret);
|
||||
obj.remove("encryptedSecret");
|
||||
obj.remove("salt");
|
||||
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
DecryptedState state = new DecryptedState(_array);
|
||||
DecryptedState state = decrypt(password);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (JSONException
|
||||
| EncodingException
|
||||
| NoSuchAlgorithmException
|
||||
| InvalidKeySpecException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| NoSuchPaddingException
|
||||
| BadPaddingException
|
||||
| IllegalBlockSizeException e) {
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
}, dialog1 -> listener.onCanceled());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +245,7 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
authyEntryInfo.AccountType = JsonUtils.optString(entry, "accountType");
|
||||
authyEntryInfo.Name = entry.optString("name");
|
||||
|
||||
boolean isAuthy = !entry.optString("accountType", "authy").equals("authenticator");
|
||||
boolean isAuthy = entry.has("secretSeed");
|
||||
sanitizeEntryInfo(authyEntryInfo, isAuthy);
|
||||
|
||||
byte[] secret;
|
||||
@@ -246,7 +256,7 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
int digits = entry.getInt("digits");
|
||||
OtpInfo info = new TotpInfo(secret, "SHA1", digits, digits == 7 ? 10 : 30);
|
||||
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, digits, isAuthy ? 10 : TotpInfo.DEFAULT_PERIOD);
|
||||
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
|
||||
} catch (OtpInfoException | JSONException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
@@ -274,6 +284,10 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
info.Issuer = info.Name;
|
||||
info.Name = "";
|
||||
}
|
||||
|
||||
if (info.Name.startsWith(": ")) {
|
||||
info.Name = info.Name.substring(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ public abstract class DatabaseImporter {
|
||||
// note: keep these lists sorted alphabetically
|
||||
_importers = new ArrayList<>();
|
||||
_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("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
|
||||
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, 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("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
|
||||
@@ -167,8 +167,9 @@ public abstract class DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
public interface DecryptListener {
|
||||
void onStateDecrypted(State state);
|
||||
void onError(Exception e);
|
||||
public static abstract class DecryptListener {
|
||||
protected abstract void onStateDecrypted(State state);
|
||||
protected abstract void onError(Exception e);
|
||||
protected abstract void onCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,30 +115,6 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<JSONObject> parseXml(XmlPullParser parser)
|
||||
throws IOException, XmlPullParserException, JSONException {
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
|
||||
parser.require(XmlPullParser.START_TAG, null, "map");
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parser.getName().equals("string")) {
|
||||
skip(parser);
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject entry = parseXmlEntry(parser);
|
||||
if (entry != null) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static byte[] toBytes(JSONArray array) throws JSONException {
|
||||
byte[] bytes = new byte[array.length()];
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
@@ -146,46 +122,4 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static JSONObject parseXmlEntry(XmlPullParser parser)
|
||||
throws IOException, XmlPullParserException, JSONException {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string");
|
||||
String name = parser.getAttributeValue(null, "name");
|
||||
String value = parseXmlText(parser);
|
||||
parser.require(XmlPullParser.END_TAG, null, "string");
|
||||
|
||||
if (name.equals("tokenOrder")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new JSONObject(value);
|
||||
}
|
||||
|
||||
private static String parseXmlText(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
String text = "";
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
text = parser.getText();
|
||||
parser.nextTag();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
// source: https://developer.android.com/training/basics/network-ops/xml.html
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
int depth = 1;
|
||||
while (depth != 0) {
|
||||
switch (parser.next()) {
|
||||
case XmlPullParser.END_TAG:
|
||||
depth--;
|
||||
break;
|
||||
case XmlPullParser.START_TAG:
|
||||
depth++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
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;
|
||||
@@ -73,7 +73,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
|
||||
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret = Base32.decode(entry.getSecret());
|
||||
byte[] secret = GoogleAuthInfo.parseSecret(entry.getSecret());
|
||||
|
||||
OtpInfo info;
|
||||
switch (entry.getType()) {
|
||||
|
||||
@@ -91,7 +91,7 @@ public class MicrosoftAuthImporter extends DatabaseImporter {
|
||||
throw new DatabaseImporterEntryException(String.format("Unsupported OTP type: %d", entry.getType()), entry.toString());
|
||||
}
|
||||
|
||||
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
|
||||
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, digits, TotpInfo.DEFAULT_PERIOD);
|
||||
return new VaultEntry(info, entry.getUserName(), entry.getIssuer());
|
||||
} catch (EncodingException | OtpInfoException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
@@ -117,10 +118,12 @@ public class SqlImporterHelper {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
public static String getString(Cursor cursor, String columnName) {
|
||||
return cursor.getString(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
public static String getString(Cursor cursor, String columnName, String def) {
|
||||
String res = cursor.getString(cursor.getColumnIndex(columnName));
|
||||
if (res == null) {
|
||||
@@ -129,10 +132,12 @@ public class SqlImporterHelper {
|
||||
return res;
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
public static int getInt(Cursor cursor, String columnName) {
|
||||
return cursor.getInt(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
public static long getLong(Cursor cursor, String columnName) {
|
||||
return cursor.getLong(cursor.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
+7
-7
@@ -14,7 +14,7 @@ import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
@@ -104,8 +104,8 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); ++i) {
|
||||
String s = array.getString(i);
|
||||
entries.add(new JSONObject(s));
|
||||
JSONObject obj = array.getJSONObject(i);
|
||||
entries.add(obj);
|
||||
}
|
||||
|
||||
return entries;
|
||||
@@ -119,7 +119,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||
_data = data;
|
||||
}
|
||||
|
||||
private DecryptedState decrypt(char[] password) throws DatabaseImporterException {
|
||||
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
|
||||
try {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this is not a secure way to derive a key from a password
|
||||
@@ -127,7 +127,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec spec = new IvParameterSpec(IV);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||
|
||||
@@ -159,7 +159,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
decrypt(password, listener);
|
||||
});
|
||||
}, dialog1 -> listener.onCanceled());
|
||||
})
|
||||
.setNegativeButton(R.string.no, (dialog, which) -> {
|
||||
decrypt(PASSWORD, listener);
|
||||
@@ -221,7 +221,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||
throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
|
||||
}
|
||||
|
||||
TotpInfo info = new TotpInfo(secret, "SHA1", 6, 30);
|
||||
TotpInfo info = new TotpInfo(secret);
|
||||
String name = obj.optString("name");
|
||||
String issuer = obj.optString("issuer");
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ public class GoogleAuthInfo implements Serializable {
|
||||
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException(String.format("Bad URI format: %s", s));
|
||||
throw new GoogleAuthInfoException(uri, String.format("Bad URI format: %s", s));
|
||||
}
|
||||
return GoogleAuthInfo.parseUri(uri);
|
||||
}
|
||||
@@ -73,27 +73,27 @@ public class GoogleAuthInfo implements Serializable {
|
||||
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals(SCHEME)) {
|
||||
throw new GoogleAuthInfoException("Unsupported protocol");
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported protocol: %s", scheme));
|
||||
}
|
||||
|
||||
// 'secret' is a required parameter
|
||||
String encodedSecret = uri.getQueryParameter("secret");
|
||||
if (encodedSecret == null) {
|
||||
throw new GoogleAuthInfoException("Parameter 'secret' is not present");
|
||||
throw new GoogleAuthInfoException(uri, "Parameter 'secret' is not present");
|
||||
}
|
||||
|
||||
byte[] secret;
|
||||
try {
|
||||
secret = parseSecret(encodedSecret);
|
||||
} catch (EncodingException e) {
|
||||
throw new GoogleAuthInfoException("Bad secret", e);
|
||||
throw new GoogleAuthInfoException(uri, "Bad secret", e);
|
||||
}
|
||||
|
||||
OtpInfo info;
|
||||
try {
|
||||
String type = uri.getHost();
|
||||
if (type == null) {
|
||||
throw new GoogleAuthInfoException(String.format("Host not present in URI: %s", uri.toString()));
|
||||
throw new GoogleAuthInfoException(uri, String.format("Host not present in URI: %s", uri.toString()));
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
@@ -117,16 +117,16 @@ public class GoogleAuthInfo implements Serializable {
|
||||
HotpInfo hotpInfo = new HotpInfo(secret);
|
||||
String counter = uri.getQueryParameter("counter");
|
||||
if (counter == null) {
|
||||
throw new GoogleAuthInfoException("Parameter 'counter' is not present");
|
||||
throw new GoogleAuthInfoException(uri, "Parameter 'counter' is not present");
|
||||
}
|
||||
hotpInfo.setCounter(Long.parseLong(counter));
|
||||
info = hotpInfo;
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(String.format("Unsupported OTP type: %s", type));
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
} catch (OtpInfoException | NumberFormatException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
// provider info used to disambiguate accounts
|
||||
@@ -166,7 +166,7 @@ public class GoogleAuthInfo implements Serializable {
|
||||
info.setDigits(Integer.parseInt(digits));
|
||||
}
|
||||
} catch (OtpInfoException | NumberFormatException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
return new GoogleAuthInfo(info, accountName, issuer);
|
||||
@@ -183,7 +183,7 @@ public class GoogleAuthInfo implements Serializable {
|
||||
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException("Bad URI format");
|
||||
throw new GoogleAuthInfoException(uri, "Bad URI format");
|
||||
}
|
||||
return GoogleAuthInfo.parseExportUri(uri);
|
||||
}
|
||||
@@ -191,17 +191,17 @@ public class GoogleAuthInfo implements Serializable {
|
||||
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
|
||||
throw new GoogleAuthInfoException("Unsupported protocol");
|
||||
throw new GoogleAuthInfoException(uri, "Unsupported protocol");
|
||||
}
|
||||
|
||||
String host = uri.getHost();
|
||||
if (host == null || !host.equals("offline")) {
|
||||
throw new GoogleAuthInfoException("Unsupported host");
|
||||
throw new GoogleAuthInfoException(uri, "Unsupported host");
|
||||
}
|
||||
|
||||
String data = uri.getQueryParameter("data");
|
||||
if (data == null) {
|
||||
throw new GoogleAuthInfoException("Parameter 'data' is not set");
|
||||
throw new GoogleAuthInfoException(uri, "Parameter 'data' is not set");
|
||||
}
|
||||
|
||||
GoogleAuthProtos.MigrationPayload payload;
|
||||
@@ -209,26 +209,59 @@ public class GoogleAuthInfo implements Serializable {
|
||||
byte[] bytes = Base64.decode(data);
|
||||
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
|
||||
} catch (EncodingException | InvalidProtocolBufferException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
List<GoogleAuthInfo> infos = new ArrayList<>();
|
||||
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
|
||||
OtpInfo otp;
|
||||
try {
|
||||
byte[] secret = params.getSecret().toByteArray();
|
||||
switch (params.getType()) {
|
||||
case OTP_HOTP:
|
||||
otp = new HotpInfo(secret, params.getCounter());
|
||||
int digits;
|
||||
switch (params.getDigits()) {
|
||||
case DIGIT_COUNT_UNSPECIFIED:
|
||||
// intentional fallthrough
|
||||
case DIGIT_COUNT_SIX:
|
||||
digits = TotpInfo.DEFAULT_DIGITS;
|
||||
break;
|
||||
case OTP_TOTP:
|
||||
otp = new TotpInfo(secret);
|
||||
case DIGIT_COUNT_EIGHT:
|
||||
digits = 8;
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(String.format("Unsupported algorithm: %d", params.getType().ordinal()));
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported digits: %d", params.getDigits().ordinal()));
|
||||
}
|
||||
|
||||
String algo;
|
||||
switch (params.getAlgorithm()) {
|
||||
case ALGORITHM_UNSPECIFIED:
|
||||
// intentional fallthrough
|
||||
case ALGORITHM_SHA1:
|
||||
algo = "SHA1";
|
||||
break;
|
||||
case ALGORITHM_SHA256:
|
||||
algo = "SHA256";
|
||||
break;
|
||||
case ALGORITHM_SHA512:
|
||||
algo = "SHA512";
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported hash algorithm: %d", params.getAlgorithm().ordinal()));
|
||||
}
|
||||
|
||||
byte[] secret = params.getSecret().toByteArray();
|
||||
switch (params.getType()) {
|
||||
case OTP_TYPE_UNSPECIFIED:
|
||||
// intentional fallthrough
|
||||
case OTP_TYPE_TOTP:
|
||||
otp = new TotpInfo(secret, algo, digits, TotpInfo.DEFAULT_PERIOD);
|
||||
break;
|
||||
case OTP_TYPE_HOTP:
|
||||
otp = new HotpInfo(secret, algo, digits, params.getCounter());
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported algorithm: %d", params.getType().ordinal()));
|
||||
}
|
||||
} catch (OtpInfoException e){
|
||||
throw new GoogleAuthInfoException(e);
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
String name = params.getName();
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
package com.beemdevelopment.aegis.otp;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
public class GoogleAuthInfoException extends Exception {
|
||||
public GoogleAuthInfoException(Throwable cause) {
|
||||
private final Uri _uri;
|
||||
|
||||
public GoogleAuthInfoException(Uri uri, Throwable cause) {
|
||||
super(cause);
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
public GoogleAuthInfoException(String message) {
|
||||
public GoogleAuthInfoException(Uri uri, String message) {
|
||||
super(message);
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
public GoogleAuthInfoException(String message, Throwable cause) {
|
||||
public GoogleAuthInfoException(Uri uri, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether the scheme of the URI is phonefactor://.
|
||||
*/
|
||||
public boolean isPhoneFactor() {
|
||||
return _uri != null && _uri.getScheme() != null && _uri.getScheme().equals("phonefactor");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -19,6 +33,7 @@ public class GoogleAuthInfoException extends Exception {
|
||||
if (cause == null) {
|
||||
return super.getMessage();
|
||||
}
|
||||
|
||||
return String.format("%s (%s)", super.getMessage(), cause.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class HotpInfo extends OtpInfo {
|
||||
public static final String ID = "hotp";
|
||||
public static final int DEFAULT_COUNTER = 0;
|
||||
|
||||
private long _counter;
|
||||
|
||||
@@ -20,7 +21,7 @@ public class HotpInfo extends OtpInfo {
|
||||
}
|
||||
|
||||
public HotpInfo(byte[] secret) throws OtpInfoException {
|
||||
this(secret, 0);
|
||||
this(secret, DEFAULT_COUNTER);
|
||||
}
|
||||
|
||||
public HotpInfo(byte[] secret, String algorithm, int digits, long counter) throws OtpInfoException {
|
||||
@@ -39,7 +40,7 @@ public class HotpInfo extends OtpInfo {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
public String getTypeId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,15 @@ import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
|
||||
public abstract class OtpInfo implements Serializable {
|
||||
public static final int DEFAULT_DIGITS = 6;
|
||||
public static final String DEFAULT_ALGORITHM = "SHA1";
|
||||
|
||||
private byte[] _secret;
|
||||
private String _algorithm;
|
||||
private int _digits;
|
||||
|
||||
public OtpInfo(byte[] secret) throws OtpInfoException {
|
||||
this(secret, "SHA1", 6);
|
||||
this(secret, DEFAULT_ALGORITHM, DEFAULT_DIGITS);
|
||||
}
|
||||
|
||||
public OtpInfo(byte[] secret, String algorithm, int digits) throws OtpInfoException {
|
||||
@@ -26,7 +29,11 @@ public abstract class OtpInfo implements Serializable {
|
||||
|
||||
public abstract String getOtp();
|
||||
|
||||
public abstract String getType();
|
||||
public abstract String getTypeId();
|
||||
|
||||
public String getType() {
|
||||
return getTypeId().toUpperCase();
|
||||
}
|
||||
|
||||
public JSONObject toJson() {
|
||||
JSONObject obj = new JSONObject();
|
||||
@@ -127,7 +134,7 @@ public abstract class OtpInfo implements Serializable {
|
||||
}
|
||||
|
||||
OtpInfo info = (OtpInfo) o;
|
||||
return getType().equals(info.getType())
|
||||
return getTypeId().equals(info.getTypeId())
|
||||
&& Arrays.equals(getSecret(), info.getSecret())
|
||||
&& getAlgorithm(false).equals(info.getAlgorithm(false))
|
||||
&& getDigits() == info.getDigits();
|
||||
|
||||
@@ -8,9 +8,10 @@ import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class SteamInfo extends TotpInfo {
|
||||
public static final String ID = "steam";
|
||||
public static final int DIGITS = 5;
|
||||
|
||||
public SteamInfo(byte[] secret) throws OtpInfoException {
|
||||
super(secret, "SHA1", 5, 30);
|
||||
super(secret, OtpInfo.DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD);
|
||||
}
|
||||
|
||||
public SteamInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
|
||||
@@ -28,7 +29,13 @@ public class SteamInfo extends TotpInfo {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
public String getTypeId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
String id = getTypeId();
|
||||
return id.substring(0, 1).toUpperCase() + id.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,13 @@ import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class TotpInfo extends OtpInfo {
|
||||
public static final String ID = "totp";
|
||||
public static final int DEFAULT_PERIOD = 30;
|
||||
|
||||
private int _period;
|
||||
|
||||
public TotpInfo(byte[] secret) throws OtpInfoException {
|
||||
super(secret);
|
||||
setPeriod(30);
|
||||
setPeriod(DEFAULT_PERIOD);
|
||||
}
|
||||
|
||||
public TotpInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
|
||||
@@ -34,8 +35,17 @@ public class TotpInfo extends OtpInfo {
|
||||
}
|
||||
}
|
||||
|
||||
public String getOtp(long time) {
|
||||
try {
|
||||
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
|
||||
return otp.toString();
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
public String getTypeId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.licenses.GlideLicense;
|
||||
import com.beemdevelopment.aegis.licenses.ProtobufLicense;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.ChangelogDialog;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.LicenseDialog;
|
||||
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
|
||||
|
||||
import de.psdev.licensesdialog.LicenseResolver;
|
||||
@@ -42,14 +44,22 @@ public class AboutActivity extends AegisActivity {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
View btnLicenses = findViewById(R.id.btn_licenses);
|
||||
btnLicenses.setOnClickListener(v -> showLicenseDialog());
|
||||
View btnLicense = findViewById(R.id.btn_license);
|
||||
btnLicense.setOnClickListener(v -> {
|
||||
LicenseDialog.create()
|
||||
.setTheme(getConfiguredTheme())
|
||||
.show(getSupportFragmentManager(), null);
|
||||
});
|
||||
|
||||
View btnThirdPartyLicenses = findViewById(R.id.btn_third_party_licenses);
|
||||
btnThirdPartyLicenses.setOnClickListener(v -> showThirdPartyLicenseDialog());
|
||||
|
||||
TextView appVersion = findViewById(R.id.app_version);
|
||||
appVersion.setText(getCurrentAppVersion());
|
||||
@@ -79,7 +89,9 @@ public class AboutActivity extends AegisActivity {
|
||||
|
||||
View btnChangelog = findViewById(R.id.btn_changelog);
|
||||
btnChangelog.setOnClickListener(v -> {
|
||||
ChangelogDialog.create().setTheme(getConfiguredTheme()).show(getSupportFragmentManager(), "CHANGELOG_DIALOG");
|
||||
ChangelogDialog.create()
|
||||
.setTheme(getConfiguredTheme())
|
||||
.show(getSupportFragmentManager(), null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,7 +127,7 @@ public class AboutActivity extends AegisActivity {
|
||||
startActivity(Intent.createChooser(mailIntent, this.getString(R.string.email)));
|
||||
}
|
||||
|
||||
private void showLicenseDialog() {
|
||||
private void showThirdPartyLicenseDialog() {
|
||||
String stylesheet = getString(R.string.custom_notices_format_style);
|
||||
int backgroundColorResource = getConfiguredTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
String backgroundColor = getThemeColorAsHex(backgroundColorResource);
|
||||
@@ -130,7 +142,7 @@ public class AboutActivity extends AegisActivity {
|
||||
|
||||
new LicensesDialog.Builder(this)
|
||||
.setNotices(R.raw.notices)
|
||||
.setTitle(R.string.licenses)
|
||||
.setTitle(R.string.third_party_licenses)
|
||||
.setNoticesCssStyle(stylesheet)
|
||||
.setIncludeOwnLicense(true)
|
||||
.build()
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
@@ -13,8 +17,11 @@ import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -50,15 +57,49 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
protected void onDestroy() {
|
||||
_app.unregisterLockListener(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
_app.setBlockAutoLock(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivityForResult(Intent intent, int requestCode, Bundle bundle) {
|
||||
if (isAutoLockBypassedForAction(intent.getAction())) {
|
||||
_app.setBlockAutoLock(true);
|
||||
}
|
||||
|
||||
try {
|
||||
super.startActivityForResult(intent, requestCode, bundle);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
if (isDocsAction(intent.getAction())) {
|
||||
Dialogs.showErrorDialog(this, R.string.documentsui_error, e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocked(boolean userInitiated) {
|
||||
setResult(RESULT_CANCELED, null);
|
||||
finishAndRemoveTask();
|
||||
try {
|
||||
Method method = Activity.class.getDeclaredMethod("finish", int.class);
|
||||
method.setAccessible(true);
|
||||
method.invoke(this, 2); // FINISH_TASK_WITH_ACTIVITY = 2
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
}
|
||||
|
||||
protected AegisApplication getApp() {
|
||||
@@ -90,13 +131,10 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
|
||||
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
|
||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
switch (currentNightMode) {
|
||||
case Configuration.UI_MODE_NIGHT_NO:
|
||||
theme = Theme.LIGHT;
|
||||
break;
|
||||
case Configuration.UI_MODE_NIGHT_YES:
|
||||
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
|
||||
break;
|
||||
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
|
||||
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
|
||||
} else {
|
||||
theme = Theme.LIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,4 +167,17 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
protected boolean isOrphan() {
|
||||
return !(this instanceof MainActivity) && !(this instanceof AuthActivity) && !(this instanceof IntroActivity) && _app.isVaultLocked();
|
||||
}
|
||||
|
||||
private static boolean isDocsAction(@Nullable String action) {
|
||||
return action != null && (action.equals(Intent.ACTION_GET_CONTENT)
|
||||
|| action.equals(Intent.ACTION_CREATE_DOCUMENT)
|
||||
|| action.equals(Intent.ACTION_OPEN_DOCUMENT)
|
||||
|| action.equals(Intent.ACTION_OPEN_DOCUMENT_TREE));
|
||||
}
|
||||
|
||||
private static boolean isAutoLockBypassedForAction(@Nullable String action) {
|
||||
return isDocsAction(action) || (action != null && (action.equals(Intent.ACTION_PICK)
|
||||
|| action.equals(Intent.ACTION_SEND)
|
||||
|| action.equals(Intent.ACTION_CHOOSER)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.MetricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
@@ -63,7 +65,6 @@ public class AuthActivity extends AegisActivity {
|
||||
private boolean _inhibitBioPrompt;
|
||||
|
||||
private Preferences _prefs;
|
||||
private boolean _stateless;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -92,19 +93,14 @@ public class AuthActivity extends AegisActivity {
|
||||
} else {
|
||||
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
|
||||
}
|
||||
_slots = (SlotList) intent.getSerializableExtra("slots");
|
||||
_stateless = _slots != null;
|
||||
if (!_stateless) {
|
||||
VaultFile vaultFile;
|
||||
try {
|
||||
vaultFile = getApp().loadVaultFile();
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> onBackPressed());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
VaultFile vaultFile = getApp().loadVaultFile();
|
||||
_slots = vaultFile.getHeader().getSlots();
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> onBackPressed());
|
||||
return;
|
||||
}
|
||||
|
||||
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
|
||||
@@ -179,11 +175,7 @@ public class AuthActivity extends AegisActivity {
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (_stateless) {
|
||||
super.onBackPressed();
|
||||
} else {
|
||||
finishAffinity();
|
||||
}
|
||||
finishAffinity();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -219,11 +211,6 @@ public class AuthActivity extends AegisActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isOrphan() {
|
||||
return _stateless && super.isOrphan();
|
||||
}
|
||||
|
||||
private void focusPasswordField() {
|
||||
_textPassword.requestFocus();
|
||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
@@ -231,13 +218,22 @@ public class AuthActivity extends AegisActivity {
|
||||
|
||||
private void showPasswordReminder() {
|
||||
View popupLayout = getLayoutInflater().inflate(R.layout.popup_password, null);
|
||||
popupLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
||||
|
||||
PopupWindow popup = new PopupWindow(popupLayout, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
popup.setFocusable(false);
|
||||
popup.setOutsideTouchable(true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
|
||||
popup.setElevation(5.0f);
|
||||
}
|
||||
_textPassword.post(() -> popup.showAsDropDown(_textPassword));
|
||||
_textPassword.post(() -> {
|
||||
// calculating the actual height of the popup window does not seem possible
|
||||
// adding 25dp seems to look good enough
|
||||
int yoff = _textPassword.getHeight()
|
||||
+ popupLayout.getMeasuredHeight()
|
||||
+ MetricsHelper.convertDpToPixels(this, 25);
|
||||
popup.showAsDropDown(_textPassword, 0, -yoff);
|
||||
});
|
||||
_textPassword.postDelayed(popup::dismiss, 5000);
|
||||
}
|
||||
|
||||
@@ -269,27 +265,19 @@ public class AuthActivity extends AegisActivity {
|
||||
private void finish(MasterKey key, boolean isSlotRepaired) {
|
||||
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
|
||||
|
||||
if (_stateless) {
|
||||
// send the master key back to the calling activity
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("creds", creds);
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
try {
|
||||
AegisApplication app = getApp();
|
||||
app.initVaultManager(app.loadVaultFile(), creds);
|
||||
if (isSlotRepaired) {
|
||||
saveVault(true);
|
||||
}
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.decryption_corrupt_error, e);
|
||||
return;
|
||||
try {
|
||||
AegisApplication app = getApp();
|
||||
app.initVaultManager(app.loadVaultFile(), creds);
|
||||
if (isSlotRepaired) {
|
||||
saveVault(true);
|
||||
}
|
||||
|
||||
setResult(RESULT_OK);
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.decryption_corrupt_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.InflateException;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
public class ChangelogDialog extends DialogFragment {
|
||||
private Theme _themeStyle;
|
||||
|
||||
public static ChangelogDialog create() {
|
||||
return new ChangelogDialog();
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final View customView;
|
||||
try {
|
||||
customView = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_web_view, null);
|
||||
} catch (InflateException e) {
|
||||
e.printStackTrace();
|
||||
return new AlertDialog.Builder(getActivity())
|
||||
.setTitle(android.R.string.dialog_alert_title)
|
||||
.setMessage(getString(R.string.webview_error))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
AlertDialog dialog = new AlertDialog.Builder(getActivity())
|
||||
.setTitle("Changelog")
|
||||
.setView(customView)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
|
||||
final WebView webView = customView.findViewById(R.id.web_view);
|
||||
StringBuilder buf = new StringBuilder();
|
||||
|
||||
try (InputStream html = getActivity().getAssets().open("changelog.html")) {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(html, "UTF-8"));
|
||||
String str;
|
||||
while ((str = in.readLine()) != null)
|
||||
buf.append(str);
|
||||
|
||||
in.close();
|
||||
String changelog = buf.toString();
|
||||
changelog = replaceStylesheet(changelog);
|
||||
webView.loadData(changelog, "text/html", "UTF-8");
|
||||
} catch (IOException e) {
|
||||
webView.loadData("<h1>Unable to load</h1><p>" + e.getLocalizedMessage() + "</p>", "text/html", "UTF-8");
|
||||
}
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private String replaceStylesheet(String changelog) {
|
||||
int backgroundColorResource = _themeStyle == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
String backgroundColor = colorToCSS(ThemeHelper.getThemeColor(backgroundColorResource, getContext().getTheme()));
|
||||
String textColor = colorToCSS(0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getContext().getTheme()));
|
||||
|
||||
return String.format(changelog, backgroundColor, textColor);
|
||||
}
|
||||
|
||||
private static String colorToCSS(int color) {
|
||||
return String.format("rgb(%d, %d, %d)", Color.red(color), Color.green(color), Color.blue(color));
|
||||
}
|
||||
|
||||
public ChangelogDialog setTheme(Theme theme) {
|
||||
_themeStyle = theme;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
@@ -13,17 +12,16 @@ import android.text.TextWatcher;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TableRow;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
@@ -34,29 +32,45 @@ import com.avito.android.krop.KropView;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
|
||||
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
@@ -64,29 +78,30 @@ public class EditEntryActivity extends AegisActivity {
|
||||
private static final int PICK_IMAGE_REQUEST = 0;
|
||||
|
||||
private boolean _isNew = false;
|
||||
private boolean _isManual = false;
|
||||
private VaultEntry _origEntry;
|
||||
private TreeSet<String> _groups;
|
||||
private boolean _hasCustomIcon = false;
|
||||
// keep track of icon changes separately as the generated jpeg's are not deterministic
|
||||
private boolean _hasChangedIcon = false;
|
||||
private IconPack.Icon _selectedIcon;
|
||||
private boolean _isEditingIcon;
|
||||
private CircleImageView _iconView;
|
||||
private ImageView _saveImageButton;
|
||||
|
||||
private EditText _textName;
|
||||
private EditText _textIssuer;
|
||||
private EditText _textPeriod;
|
||||
private EditText _textCounter;
|
||||
private EditText _textDigits;
|
||||
private EditText _textSecret;
|
||||
private TextInputEditText _textName;
|
||||
private TextInputEditText _textIssuer;
|
||||
private TextInputEditText _textPeriodCounter;
|
||||
private TextInputLayout _textPeriodCounterLayout;
|
||||
private TextInputEditText _textDigits;
|
||||
private TextInputLayout _textDigitsLayout;
|
||||
private TextInputEditText _textSecret;
|
||||
|
||||
private TableRow _rowPeriod;
|
||||
private TableRow _rowCounter;
|
||||
|
||||
private Spinner _spinnerType;
|
||||
private Spinner _spinnerAlgo;
|
||||
private Spinner _spinnerGroup;
|
||||
private List<String> _spinnerGroupList = new ArrayList<>();
|
||||
private AutoCompleteTextView _dropdownType;
|
||||
private AutoCompleteTextView _dropdownAlgo;
|
||||
private TextInputLayout _dropdownAlgoLayout;
|
||||
private AutoCompleteTextView _dropdownGroup;
|
||||
private List<String> _dropdownGroupList = new ArrayList<>();
|
||||
|
||||
private KropView _kropView;
|
||||
|
||||
@@ -99,6 +114,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_edit_entry);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
_vault = getApp().getVaultManager();
|
||||
_groups = _vault.getGroups();
|
||||
@@ -114,40 +130,59 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_origEntry = _vault.getEntryByUUID(entryUUID);
|
||||
} else {
|
||||
_origEntry = (VaultEntry) intent.getSerializableExtra("newEntry");
|
||||
_isManual = intent.getBooleanExtra("isManual", false);
|
||||
_isNew = true;
|
||||
setTitle(R.string.add_new_entry);
|
||||
}
|
||||
|
||||
String selectedGroup = intent.getStringExtra("selectedGroup");
|
||||
|
||||
// set up fields
|
||||
_iconView = findViewById(R.id.profile_drawable);
|
||||
_kropView = findViewById(R.id.krop_view);
|
||||
_saveImageButton = findViewById(R.id.iv_saveImage);
|
||||
_textName = findViewById(R.id.text_name);
|
||||
_textIssuer = findViewById(R.id.text_issuer);
|
||||
_textPeriod = findViewById(R.id.text_period);
|
||||
_textPeriodCounter = findViewById(R.id.text_period_counter);
|
||||
_textPeriodCounterLayout = findViewById(R.id.text_period_counter_layout);
|
||||
_textDigits = findViewById(R.id.text_digits);
|
||||
_rowPeriod = findViewById(R.id.row_period);
|
||||
_textCounter = findViewById(R.id.text_counter);
|
||||
_rowCounter = findViewById(R.id.row_counter);
|
||||
_textDigitsLayout = findViewById(R.id.text_digits_layout);
|
||||
_textSecret = findViewById(R.id.text_secret);
|
||||
_spinnerType = findViewById(R.id.spinner_type);
|
||||
SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array);
|
||||
_spinnerAlgo = findViewById(R.id.spinner_algo);
|
||||
SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
|
||||
_spinnerGroup = findViewById(R.id.spinner_group);
|
||||
updateGroupSpinnerList();
|
||||
SpinnerHelper.fillSpinner(this, _spinnerGroup, _spinnerGroupList);
|
||||
_dropdownType = findViewById(R.id.dropdown_type);
|
||||
DropdownHelper.fillDropdown(this, _dropdownType, R.array.otp_types_array);
|
||||
_dropdownAlgoLayout = findViewById(R.id.dropdown_algo_layout);
|
||||
_dropdownAlgo = findViewById(R.id.dropdown_algo);
|
||||
DropdownHelper.fillDropdown(this, _dropdownAlgo, R.array.otp_algo_array);
|
||||
_dropdownGroup = findViewById(R.id.dropdown_group);
|
||||
updateGroupDropdownList();
|
||||
DropdownHelper.fillDropdown(this, _dropdownGroup, _dropdownGroupList);
|
||||
|
||||
// if this is NOT a manually entered entry, move the "Secret" field from basic to advanced settings
|
||||
if (!_isNew || (_isNew && !_isManual)) {
|
||||
LinearLayout layoutSecret = findViewById(R.id.layout_secret);
|
||||
LinearLayout layoutBasic = findViewById(R.id.layout_basic);
|
||||
LinearLayout layoutAdvanced = findViewById(R.id.layout_advanced);
|
||||
layoutBasic.removeView(layoutSecret);
|
||||
layoutAdvanced.addView(layoutSecret, 0);
|
||||
((LinearLayout.LayoutParams) layoutSecret.getLayoutParams()).topMargin = 0;
|
||||
|
||||
if (_isNew && !_isManual) {
|
||||
setViewEnabled(layoutAdvanced, false);
|
||||
}
|
||||
} else {
|
||||
LinearLayout layoutTypeAlgo = findViewById(R.id.layout_type_algo);
|
||||
((LinearLayout.LayoutParams) layoutTypeAlgo.getLayoutParams()).topMargin = 0;
|
||||
}
|
||||
|
||||
_advancedSettingsHeader = findViewById(R.id.accordian_header);
|
||||
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
|
||||
_advancedSettings = findViewById(R.id.expandableLayout);
|
||||
|
||||
// fill the fields with values if possible
|
||||
if (_origEntry.hasIcon()) {
|
||||
IconViewHelper.setLayerType(_iconView, _origEntry.getIconType());
|
||||
Glide.with(this)
|
||||
.asDrawable()
|
||||
.load(_origEntry)
|
||||
.set(IconLoader.ICON_TYPE, _origEntry.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_iconView);
|
||||
@@ -162,11 +197,11 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
OtpInfo info = _origEntry.getInfo();
|
||||
if (info instanceof TotpInfo) {
|
||||
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
|
||||
_rowPeriod.setVisibility(View.VISIBLE);
|
||||
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
||||
_textPeriodCounter.setText(Integer.toString(((TotpInfo) info).getPeriod()));
|
||||
} else if (info instanceof HotpInfo) {
|
||||
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
|
||||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
_textPeriodCounterLayout.setHint(R.string.counter);
|
||||
_textPeriodCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
|
||||
} else {
|
||||
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", info.getClass()));
|
||||
}
|
||||
@@ -178,11 +213,9 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_textSecret.setText(secretString);
|
||||
}
|
||||
|
||||
String type = _origEntry.getInfo().getType();
|
||||
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
|
||||
|
||||
String algo = _origEntry.getInfo().getAlgorithm(false);
|
||||
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
|
||||
_dropdownType.setText(_origEntry.getInfo().getType(), false);
|
||||
_dropdownAlgo.setText(_origEntry.getInfo().getAlgorithm(false), false);
|
||||
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
|
||||
|
||||
String group = _origEntry.getGroup();
|
||||
setGroup(group);
|
||||
@@ -192,81 +225,72 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_textName.addTextChangedListener(_iconChangeListener);
|
||||
|
||||
// show/hide period and counter fields on type change
|
||||
_spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
String type = _spinnerType.getSelectedItem().toString().toLowerCase();
|
||||
switch (type) {
|
||||
case TotpInfo.ID:
|
||||
case SteamInfo.ID:
|
||||
_rowCounter.setVisibility(View.GONE);
|
||||
_rowPeriod.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case HotpInfo.ID:
|
||||
_rowPeriod.setVisibility(View.GONE);
|
||||
_rowCounter.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
_dropdownType.setOnItemClickListener((parent, view, position, id) -> {
|
||||
String type = _dropdownType.getText().toString().toLowerCase();
|
||||
switch (type) {
|
||||
case SteamInfo.ID:
|
||||
_dropdownAlgo.setText(OtpInfo.DEFAULT_ALGORITHM, false);
|
||||
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
||||
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
|
||||
_textDigits.setText(String.valueOf(SteamInfo.DIGITS));
|
||||
break;
|
||||
case TotpInfo.ID:
|
||||
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
||||
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
|
||||
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
|
||||
break;
|
||||
case HotpInfo.ID:
|
||||
_textPeriodCounterLayout.setHint(R.string.counter);
|
||||
_textPeriodCounter.setText(String.valueOf(HotpInfo.DEFAULT_COUNTER));
|
||||
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
updateAdvancedFieldStatus(type);
|
||||
});
|
||||
|
||||
final Activity activity = this;
|
||||
_spinnerGroup.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
private int prevPosition;
|
||||
_iconView.setOnClickListener(v -> {
|
||||
startIconSelection();
|
||||
});
|
||||
|
||||
_dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
private int prevPosition = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
|
||||
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (position == _spinnerGroupList.size() - 1) {
|
||||
Dialogs.showTextInputDialog(activity, R.string.set_group, R.string.group_name_hint, text -> {
|
||||
String str = new String(text);
|
||||
if (str.isEmpty()) {
|
||||
return;
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (position == _dropdownGroupList.size() - 1) {
|
||||
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, text -> {
|
||||
String groupName = new String(text);
|
||||
if (!groupName.isEmpty()) {
|
||||
_groups.add(groupName);
|
||||
updateGroupDropdownList();
|
||||
_dropdownGroup.setText(groupName, false);
|
||||
}
|
||||
_groups.add(str);
|
||||
// reset the selection to "No group" to work around a quirk
|
||||
_spinnerGroup.setSelection(0, false);
|
||||
updateGroupSpinnerList();
|
||||
_spinnerGroup.setSelection(_spinnerGroupList.indexOf(str), false);
|
||||
});
|
||||
_spinnerGroup.setSelection(prevPosition, false);
|
||||
_dropdownGroup.setText(_dropdownGroupList.get(prevPosition), false);
|
||||
} else {
|
||||
prevPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_iconView.setOnClickListener(v -> {
|
||||
startIconSelectionActivity();
|
||||
});
|
||||
|
||||
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
|
||||
|
||||
// automatically open advanced settings since 'Secret' is required.
|
||||
if (_isNew) {
|
||||
openAdvancedSettings();
|
||||
setGroup(selectedGroup);
|
||||
}
|
||||
private void updateAdvancedFieldStatus(String otpType) {
|
||||
boolean enabled = !otpType.equals(SteamInfo.ID) && (!_isNew || _isManual);
|
||||
_textDigitsLayout.setEnabled(enabled);
|
||||
_textPeriodCounterLayout.setEnabled(enabled);
|
||||
_dropdownAlgoLayout.setEnabled(enabled);
|
||||
}
|
||||
|
||||
private void setGroup(String groupName) {
|
||||
if (groupName == null) {
|
||||
return;
|
||||
int pos = 0;
|
||||
if (groupName != null) {
|
||||
pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() + 1 : 0;
|
||||
}
|
||||
|
||||
int pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() : -1;
|
||||
_spinnerGroup.setSelection(pos + 1, false);
|
||||
_dropdownGroup.setText(_dropdownGroupList.get(pos), false);
|
||||
}
|
||||
|
||||
private void openAdvancedSettings() {
|
||||
@@ -315,12 +339,12 @@ public class EditEntryActivity extends AegisActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void updateGroupSpinnerList() {
|
||||
private void updateGroupDropdownList() {
|
||||
Resources res = getResources();
|
||||
_spinnerGroupList.clear();
|
||||
_spinnerGroupList.add(res.getString(R.string.no_group));
|
||||
_spinnerGroupList.addAll(_groups);
|
||||
_spinnerGroupList.add(res.getString(R.string.new_group));
|
||||
_dropdownGroupList.clear();
|
||||
_dropdownGroupList.add(res.getString(R.string.no_group));
|
||||
_dropdownGroupList.addAll(_groups);
|
||||
_dropdownGroupList.add(res.getString(R.string.new_group));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -370,16 +394,18 @@ public class EditEntryActivity extends AegisActivity {
|
||||
onSave();
|
||||
break;
|
||||
case R.id.action_delete:
|
||||
Dialogs.showDeleteEntryDialog(this, (dialog, which) -> {
|
||||
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry.getIssuer()), (dialog, which) -> {
|
||||
deleteAndFinish(_origEntry);
|
||||
});
|
||||
break;
|
||||
case R.id.action_edit_icon:
|
||||
startIconSelectionActivity();
|
||||
startIconSelection();
|
||||
break;
|
||||
case R.id.action_default_icon:
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
|
||||
_selectedIcon = null;
|
||||
_hasCustomIcon = false;
|
||||
_hasChangedIcon = true;
|
||||
default:
|
||||
@@ -389,7 +415,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startIconSelectionActivity() {
|
||||
private void startImageSelectionActivity() {
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
@@ -401,6 +427,40 @@ public class EditEntryActivity extends AegisActivity {
|
||||
startActivityForResult(chooserIntent, PICK_IMAGE_REQUEST);
|
||||
}
|
||||
|
||||
private void startIconSelection() {
|
||||
List<IconPack> iconPacks = getApp().getIconPackManager().getIconPacks().stream()
|
||||
.sorted(Comparator.comparing(IconPack::getName))
|
||||
.collect(Collectors.toList());
|
||||
if (iconPacks.size() == 0) {
|
||||
startImageSelectionActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), new IconAdapter.Listener() {
|
||||
@Override
|
||||
public void onIconSelected(IconPack.Icon icon) {
|
||||
_selectedIcon = icon;
|
||||
_hasCustomIcon = true;
|
||||
_hasChangedIcon = true;
|
||||
|
||||
IconViewHelper.setLayerType(_iconView, icon.getIconType());
|
||||
Glide.with(EditEntryActivity.this)
|
||||
.asDrawable()
|
||||
.load(icon.getFile())
|
||||
.set(IconLoader.ICON_TYPE, icon.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_iconView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomSelected() {
|
||||
startImageSelectionActivity();
|
||||
}
|
||||
});
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private void startEditingIcon(Uri data) {
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
@@ -429,11 +489,12 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
private void stopEditingIcon(boolean save) {
|
||||
if (save) {
|
||||
if (save && _selectedIcon == null) {
|
||||
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
|
||||
}
|
||||
_iconView.setVisibility(View.VISIBLE);
|
||||
_kropView.setVisibility(View.GONE);
|
||||
|
||||
_hasCustomIcon = _hasCustomIcon || save;
|
||||
_hasChangedIcon = save;
|
||||
_isEditingIcon = false;
|
||||
@@ -493,7 +554,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
private int parsePeriod() throws ParseException {
|
||||
try {
|
||||
return Integer.parseInt(_textPeriod.getText().toString());
|
||||
return Integer.parseInt(_textPeriodCounter.getText().toString());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParseException("Period is not an integer.");
|
||||
}
|
||||
@@ -504,8 +565,8 @@ public class EditEntryActivity extends AegisActivity {
|
||||
throw new ParseException("Secret is a required field.");
|
||||
}
|
||||
|
||||
String type = _spinnerType.getSelectedItem().toString();
|
||||
String algo = _spinnerAlgo.getSelectedItem().toString();
|
||||
String type = _dropdownType.getText().toString();
|
||||
String algo = _dropdownAlgo.getText().toString();
|
||||
|
||||
int digits;
|
||||
try {
|
||||
@@ -537,7 +598,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
case HotpInfo.ID:
|
||||
long counter;
|
||||
try {
|
||||
counter = Long.parseLong(_textCounter.getText().toString());
|
||||
counter = Long.parseLong(_textPeriodCounter.getText().toString());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParseException("Counter is not an integer.");
|
||||
}
|
||||
@@ -558,9 +619,9 @@ public class EditEntryActivity extends AegisActivity {
|
||||
entry.setIssuer(_textIssuer.getText().toString());
|
||||
entry.setName(_textName.getText().toString());
|
||||
|
||||
int groupPos = _spinnerGroup.getSelectedItemPosition();
|
||||
int groupPos = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
|
||||
if (groupPos != 0) {
|
||||
String group = _spinnerGroupList.get(_spinnerGroup.getSelectedItemPosition());
|
||||
String group = _dropdownGroupList.get(groupPos);
|
||||
entry.setGroup(group);
|
||||
} else {
|
||||
entry.setGroup(null);
|
||||
@@ -568,13 +629,26 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
if (_hasChangedIcon) {
|
||||
if (_hasCustomIcon) {
|
||||
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
byte[] data = stream.toByteArray();
|
||||
entry.setIcon(data);
|
||||
if (_selectedIcon == null) {
|
||||
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
// the quality parameter is ignored for PNG
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
byte[] data = stream.toByteArray();
|
||||
entry.setIcon(data, IconType.PNG);
|
||||
} else {
|
||||
byte[] iconBytes;
|
||||
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
|
||||
iconBytes = IOUtils.readFile(inStream);
|
||||
} catch (IOException e) {
|
||||
// TODO: show dialog
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
entry.setIcon(iconBytes, _selectedIcon.getIconType());
|
||||
}
|
||||
} else {
|
||||
entry.setIcon(null);
|
||||
entry.setIcon(null, IconType.INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,7 +680,18 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private TextWatcher _iconChangeListener = new TextWatcher() {
|
||||
private static void setViewEnabled(View view, boolean enabled) {
|
||||
view.setEnabled(enabled);
|
||||
|
||||
if (view instanceof ViewGroup) {
|
||||
ViewGroup group = (ViewGroup) view;
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
setViewEnabled(group.getChildAt(i), enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final TextWatcher _iconChangeListener = new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
@@ -624,16 +709,6 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
};
|
||||
|
||||
private int getStringResourceIndex(@ArrayRes int id, String string) {
|
||||
String[] res = getResources().getStringArray(id);
|
||||
for (int i = 0; i < res.length; i++) {
|
||||
if (res[i].equalsIgnoreCase(string)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static class ParseException extends Exception {
|
||||
public ParseException(String message) {
|
||||
super(message);
|
||||
|
||||
@@ -5,17 +5,18 @@ import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
|
||||
|
||||
import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
|
||||
private GroupAdapter _adapter;
|
||||
private TreeSet<String> _groups;
|
||||
@@ -26,6 +27,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_groups);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
Intent intent = getIntent();
|
||||
_groups = new TreeSet<>(Collator.getInstance());
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
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.views.ImportEntriesAdapter;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ImportEntriesActivity extends AegisActivity {
|
||||
private Menu _menu;
|
||||
private ImportEntriesAdapter _adapter;
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_import_entries);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
_adapter = new ImportEntriesAdapter();
|
||||
RecyclerView 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);
|
||||
_fabScrollHelper.onScroll(dx, dy);
|
||||
}
|
||||
});
|
||||
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
entriesView.setLayoutManager(layoutManager);
|
||||
entriesView.setAdapter(_adapter);
|
||||
entriesView.setNestedScrollingEnabled(false);
|
||||
|
||||
FloatingActionButton fab = findViewById(R.id.fab);
|
||||
fab.setOnClickListener(v -> {
|
||||
if (getApp().getVaultManager().getEntries().size() > 0
|
||||
&& _menu.findItem(R.id.toggle_wipe_vault).isChecked()) {
|
||||
showWipeEntriesDialog();
|
||||
} else {
|
||||
saveAndFinish(false);
|
||||
}
|
||||
});
|
||||
_fabScrollHelper = new FabScrollHelper(fab);
|
||||
|
||||
Class<? extends DatabaseImporter> importerType = (Class<? extends DatabaseImporter>) getIntent().getSerializableExtra("importerType");
|
||||
startImport(importerType, (File) getIntent().getSerializableExtra("file"));
|
||||
}
|
||||
|
||||
private void startImport(@NonNull Class<? extends DatabaseImporter> importerType, @Nullable File file) {
|
||||
if (file == null) {
|
||||
startImportApp(importerType);
|
||||
} else {
|
||||
startImportFile(importerType, file);
|
||||
}
|
||||
}
|
||||
|
||||
private void startImportFile(@NonNull Class<? extends DatabaseImporter> importerType, @NonNull File file) {
|
||||
try (InputStream stream = new FileInputStream(file)) {
|
||||
DatabaseImporter importer = DatabaseImporter.create(this, importerType);
|
||||
DatabaseImporter.State state = importer.read(stream);
|
||||
processImporterState(state);
|
||||
} catch (FileNotFoundException e) {
|
||||
Toast.makeText(this, R.string.file_not_found, Toast.LENGTH_SHORT).show();
|
||||
} catch (DatabaseImporterException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.reading_file_error, e, (dialog, which) -> finish());
|
||||
}
|
||||
}
|
||||
|
||||
private void startImportApp(@NonNull Class<? extends DatabaseImporter> importerType) {
|
||||
DatabaseImporter importer = DatabaseImporter.create(this, importerType);
|
||||
|
||||
// obtain the global root shell and close it immediately after we're done
|
||||
// TODO: find a way to use SuFileInputStream with Shell.newInstance()
|
||||
try (Shell shell = Shell.getShell()) {
|
||||
if (!shell.isRoot()) {
|
||||
Toast.makeText(this, R.string.root_error, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseImporter.State state = importer.readFromApp();
|
||||
processImporterState(state);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(this, R.string.app_lookup_error, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
} catch (IOException | DatabaseImporterException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.reading_file_error, e, (dialog, which) -> finish());
|
||||
}
|
||||
}
|
||||
|
||||
private void processImporterState(DatabaseImporter.State state) {
|
||||
try {
|
||||
if (state.isEncrypted()) {
|
||||
state.decrypt(this, new DatabaseImporter.DecryptListener() {
|
||||
@Override
|
||||
public void onStateDecrypted(DatabaseImporter.State state) {
|
||||
importDatabase(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(ImportEntriesActivity.this, R.string.decryption_error, e, (dialog, which) -> finish());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
importDatabase(state);
|
||||
}
|
||||
} catch (DatabaseImporterException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.parsing_file_error, e, (dialog, which) -> finish());
|
||||
}
|
||||
}
|
||||
|
||||
private void importDatabase(DatabaseImporter.State state) {
|
||||
DatabaseImporter.Result result;
|
||||
try {
|
||||
result = state.convert();
|
||||
} catch (DatabaseImporterException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.parsing_file_error, e, (dialog, which) -> finish());
|
||||
return;
|
||||
}
|
||||
|
||||
UUIDMap<VaultEntry> entries = result.getEntries();
|
||||
for (VaultEntry entry : entries.getValues()) {
|
||||
_adapter.addEntry(new ImportEntry(entry));
|
||||
}
|
||||
|
||||
List<DatabaseImporterEntryException> errors = result.getErrors();
|
||||
if (errors.size() > 0) {
|
||||
showErrorDialog(errors);
|
||||
}
|
||||
}
|
||||
|
||||
private void showErrorDialog(List<DatabaseImporterEntryException> errors) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.import_error_title)
|
||||
.setMessage(getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size()))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(getString(R.string.details), (dialog, which) -> showDetailedErrorDialog(errors))
|
||||
.create());
|
||||
}
|
||||
|
||||
private void showDetailedErrorDialog(List<DatabaseImporterEntryException> errors) {
|
||||
List<String> messages = new ArrayList<>();
|
||||
for (DatabaseImporterEntryException e : errors) {
|
||||
messages.add(e.getMessage());
|
||||
}
|
||||
|
||||
String message = TextUtils.join("\n\n", messages);
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.import_error_title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(android.R.string.copy, (dialog2, which2) -> {
|
||||
ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("text/plain", message);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(this, R.string.errors_copied, Toast.LENGTH_SHORT).show();
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
private void showWipeEntriesDialog() {
|
||||
Dialogs.showCheckboxDialog(this, R.string.dialog_wipe_entries_title,
|
||||
R.string.dialog_wipe_entries_message,
|
||||
R.string.dialog_wipe_entries_checkbox,
|
||||
this::saveAndFinish
|
||||
);
|
||||
}
|
||||
|
||||
private void saveAndFinish(boolean wipeEntries) {
|
||||
VaultManager vault = getApp().getVaultManager();
|
||||
if (wipeEntries) {
|
||||
vault.wipeEntries();
|
||||
}
|
||||
|
||||
List<ImportEntry> selectedEntries = _adapter.getCheckedEntries();
|
||||
for (ImportEntry selectedEntry : selectedEntries) {
|
||||
VaultEntry entry = selectedEntry.getEntry();
|
||||
|
||||
// temporary: randomize the UUID of duplicate entries and add them anyway
|
||||
if (vault.isEntryDuplicate(entry)) {
|
||||
entry.resetUUID();
|
||||
}
|
||||
|
||||
vault.addEntry(entry);
|
||||
}
|
||||
|
||||
if (saveVault(true)) {
|
||||
String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size());
|
||||
Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show();
|
||||
|
||||
setResult(RESULT_OK, null);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
_menu = menu;
|
||||
getMenuInflater().inflate(R.menu.menu_import_entries, _menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
case R.id.toggle_checkboxes:
|
||||
_adapter.toggleCheckboxes();
|
||||
break;
|
||||
case R.id.toggle_wipe_vault:
|
||||
item.setChecked(!item.isChecked());
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.os.Bundle;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.intro.IntroBaseActivity;
|
||||
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
|
||||
import com.beemdevelopment.aegis.ui.slides.DoneSlide;
|
||||
|
||||
@@ -7,15 +7,12 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
@@ -32,14 +29,19 @@ import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.helpers.BitmapHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.helpers.PermissionHelper;
|
||||
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.ui.fragments.BackupsPreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.fragments.PreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryListView;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.ChecksumException;
|
||||
import com.google.zxing.FormatException;
|
||||
@@ -55,8 +57,8 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MainActivity extends AegisActivity implements EntryListView.Listener {
|
||||
// activity request codes
|
||||
@@ -75,7 +77,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
private AegisApplication _app;
|
||||
private VaultManager _vault;
|
||||
private boolean _loaded;
|
||||
private String _selectedGroup;
|
||||
private List<String> _selectedGroups;
|
||||
private boolean _searchSubmitted;
|
||||
|
||||
private boolean _isAuthenticating;
|
||||
@@ -86,7 +88,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
private Menu _menu;
|
||||
private SearchView _searchView;
|
||||
private FloatingActionsMenu _fabMenu;
|
||||
private EntryListView _entryListView;
|
||||
private LinearLayout _btnBackupError;
|
||||
|
||||
@@ -98,6 +99,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
_app = (AegisApplication) getApplication();
|
||||
_vault = _app.getVaultManager();
|
||||
@@ -108,12 +110,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_isDoingIntro = savedInstanceState.getBoolean("isDoingIntro");
|
||||
}
|
||||
|
||||
// set up the entry view
|
||||
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
|
||||
_entryListView.setListener(this);
|
||||
_entryListView.setCodeGroupSize(getPreferences().getCodeGroupSize());
|
||||
_entryListView.setShowAccountName(getPreferences().isAccountNameVisible());
|
||||
_entryListView.setSearchAccountName(getPreferences().isSearchAccountNameEnabled());
|
||||
_entryListView.setHighlightEntry(getPreferences().isEntryHighlightEnabled());
|
||||
_entryListView.setTapToReveal(getPreferences().isTapToRevealEnabled());
|
||||
_entryListView.setTapToRevealTime(getPreferences().getTapToRevealTime());
|
||||
@@ -121,27 +121,34 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.setViewMode(getPreferences().getCurrentViewMode());
|
||||
_entryListView.setIsCopyOnTapEnabled(getPreferences().isCopyOnTapEnabled());
|
||||
|
||||
// set up the floating action button
|
||||
_fabMenu = findViewById(R.id.fab);
|
||||
findViewById(R.id.fab_enter).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, null, true);
|
||||
});
|
||||
findViewById(R.id.fab_scan_image).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
startScanImageActivity();
|
||||
});
|
||||
findViewById(R.id.fab_scan).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
startScanActivity();
|
||||
});
|
||||
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.findViewById(R.id.fab_enter).setOnClickListener(v1 -> {
|
||||
dialog.dismiss();
|
||||
startEditEntryActivityForManual(CODE_ADD_ENTRY);
|
||||
});
|
||||
view.findViewById(R.id.fab_scan_image).setOnClickListener(v2 -> {
|
||||
dialog.dismiss();
|
||||
startScanImageActivity();
|
||||
});
|
||||
view.findViewById(R.id.fab_scan).setOnClickListener(v3 -> {
|
||||
dialog.dismiss();
|
||||
startScanActivity();
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
});
|
||||
|
||||
_btnBackupError = findViewById(R.id.btn_backup_error);
|
||||
_btnBackupError.setOnClickListener(view -> {
|
||||
startPreferencesActivity("pref_backups");
|
||||
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
|
||||
});
|
||||
|
||||
_fabScrollHelper = new FabScrollHelper(_fabMenu);
|
||||
_fabScrollHelper = new FabScrollHelper(fab);
|
||||
_selectedEntries = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -158,23 +165,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent event) {
|
||||
// collapse the fab menu on touch
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
if (_fabMenu.isExpanded()) {
|
||||
Rect rect = new Rect();
|
||||
_fabMenu.getGlobalVisibleRect(rect);
|
||||
|
||||
if (!rect.contains((int) event.getRawX(), (int) event.getRawY())) {
|
||||
_fabMenu.collapse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.dispatchTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
_isAuthenticating = false;
|
||||
@@ -235,7 +225,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
} else if (data.getBooleanExtra("needsRefresh", false)) {
|
||||
boolean showAccountName = getPreferences().isAccountNameVisible();
|
||||
int codeGroupSize = getPreferences().getCodeGroupSize();
|
||||
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
|
||||
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
|
||||
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
|
||||
int tapToRevealTime = getPreferences().getTapToRevealTime();
|
||||
@@ -243,7 +232,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
boolean copyOnTap = getPreferences().isCopyOnTapEnabled();
|
||||
_entryListView.setShowAccountName(showAccountName);
|
||||
_entryListView.setCodeGroupSize(codeGroupSize);
|
||||
_entryListView.setSearchAccountName(searchAccountName);
|
||||
_entryListView.setHighlightEntry(highlightEntry);
|
||||
_entryListView.setTapToReveal(tapToReveal);
|
||||
_entryListView.setTapToRevealTime(tapToRevealTime);
|
||||
@@ -254,21 +242,30 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private void startEditEntryActivity(int requestCode, VaultEntry entry, boolean isNew) {
|
||||
private void startEditEntryActivityForNew(int requestCode, VaultEntry entry) {
|
||||
Intent intent = new Intent(this, EditEntryActivity.class);
|
||||
if (isNew) {
|
||||
intent.putExtra("newEntry", entry != null ? entry : VaultEntry.getDefault());
|
||||
} else {
|
||||
intent.putExtra("entryUUID", entry.getUUID());
|
||||
}
|
||||
intent.putExtra("selectedGroup", _selectedGroup);
|
||||
intent.putExtra("newEntry", entry);
|
||||
intent.putExtra("isManual", false);
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
private void startEditEntryActivityForManual(int requestCode) {
|
||||
Intent intent = new Intent(this, EditEntryActivity.class);
|
||||
intent.putExtra("newEntry", VaultEntry.getDefault());
|
||||
intent.putExtra("isManual", true);
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
private void startEditEntryActivity(int requestCode, VaultEntry entry) {
|
||||
Intent intent = new Intent(this, EditEntryActivity.class);
|
||||
intent.putExtra("entryUUID", entry.getUUID());
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
private void onScanResult(Intent data) {
|
||||
List<VaultEntry> entries = (ArrayList<VaultEntry>) data.getSerializableExtra("entries");
|
||||
if (entries.size() == 1) {
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entries.get(0), true);
|
||||
startEditEntryActivityForNew(CODE_ADD_ENTRY, entries.get(0));
|
||||
} else {
|
||||
for (VaultEntry entry : entries) {
|
||||
_vault.addEntry(entry);
|
||||
@@ -303,7 +300,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
private void onScanImageResult(Intent intent) {
|
||||
Uri inputFile = (intent.getData());
|
||||
decodeQrCodeImage(intent.getData());
|
||||
}
|
||||
|
||||
private void decodeQrCodeImage(Uri inputFile) {
|
||||
Bitmap bitmap;
|
||||
|
||||
try {
|
||||
@@ -311,7 +311,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
|
||||
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
|
||||
bitmap = BitmapHelper.resize(bitmap, 640, 480);
|
||||
bitmap = BitmapHelper.resize(bitmap, QrCodeAnalyzer.RESOLUTION.getWidth(), QrCodeAnalyzer.RESOLUTION.getHeight());
|
||||
}
|
||||
|
||||
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
@@ -326,55 +326,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
|
||||
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
|
||||
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGroupFilterMenu() {
|
||||
SubMenu menu = _menu.findItem(R.id.action_filter).getSubMenu();
|
||||
for (int i = menu.size() - 1; i >= 0; i--) {
|
||||
MenuItem item = menu.getItem(i);
|
||||
if (item.getItemId() == R.id.menu_filter_all) {
|
||||
continue;
|
||||
}
|
||||
menu.removeItem(item.getItemId());
|
||||
}
|
||||
|
||||
// if the group no longer exists, switch back to 'All'
|
||||
TreeSet<String> groups = _vault.getGroups();
|
||||
if (_selectedGroup != null && !groups.contains(_selectedGroup)) {
|
||||
menu.findItem(R.id.menu_filter_all).setChecked(true);
|
||||
setGroupFilter(null);
|
||||
}
|
||||
|
||||
for (String group : groups) {
|
||||
MenuItem item = menu.add(R.id.action_filter_group, Menu.NONE, Menu.NONE, group);
|
||||
if (group.equals(_selectedGroup)) {
|
||||
item.setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (groups.size() > 0) {
|
||||
menu.add(R.id.action_filter_group, Menu.NONE, 10, R.string.filter_ungrouped);
|
||||
}
|
||||
|
||||
menu.setGroupCheckable(R.id.action_filter_group, true, true);
|
||||
}
|
||||
|
||||
private void updateSortCategoryMenu() {
|
||||
SortCategory category = getPreferences().getCurrentSortCategory();
|
||||
_menu.findItem(category.getMenuItem()).setChecked(true);
|
||||
}
|
||||
|
||||
private void setGroupFilter(String group) {
|
||||
getSupportActionBar().setSubtitle(group);
|
||||
_selectedGroup = group;
|
||||
_entryListView.setGroupFilter(group, true);
|
||||
}
|
||||
|
||||
private void onDoIntroResult() {
|
||||
_vault = _app.getVaultManager();
|
||||
loadEntries();
|
||||
@@ -418,8 +381,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
startActivityForResult(chooserIntent, CODE_SCAN_IMAGE);
|
||||
}
|
||||
|
||||
private void startPreferencesActivity(String preference) {
|
||||
private void startPreferencesActivity() {
|
||||
startPreferencesActivity(null, null);
|
||||
}
|
||||
|
||||
private void startPreferencesActivity(Class<? extends PreferencesFragment> fragmentType, String preference) {
|
||||
Intent intent = new Intent(this, PreferencesActivity.class);
|
||||
intent.putExtra("fragment", fragmentType);
|
||||
intent.putExtra("pref", preference);
|
||||
startActivityForResult(intent, CODE_PREFERENCES);
|
||||
}
|
||||
@@ -461,11 +429,28 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
if (info != null) {
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
|
||||
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSharedImage() {
|
||||
if (_app.isVaultLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getAction();
|
||||
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && uri != null) {
|
||||
intent.setAction(null);
|
||||
intent.removeExtra(Intent.EXTRA_STREAM);
|
||||
|
||||
decodeQrCodeImage(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
@@ -499,10 +484,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
if (_app.isVaultLocked()) {
|
||||
startAuthActivity(false);
|
||||
} else if (_loaded) {
|
||||
// update the list of groups in the filter menu
|
||||
if (_menu != null) {
|
||||
updateGroupFilterMenu();
|
||||
}
|
||||
// update the list of groups in the entry list view so that the chip gets updated
|
||||
_entryListView.setGroups(_vault.getGroups());
|
||||
|
||||
// refresh all codes to prevent showing old ones
|
||||
_entryListView.refresh(false);
|
||||
@@ -512,6 +495,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
handleDeeplink();
|
||||
handleSharedImage();
|
||||
updateLockIcon();
|
||||
doShortcutActions();
|
||||
updateBackupErrorBar();
|
||||
@@ -524,8 +508,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.setSearchFilter(null);
|
||||
|
||||
collapseSearchView();
|
||||
setTitle("Aegis");
|
||||
setGroupFilter(_selectedGroup);
|
||||
setTitle(R.string.app_name);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -552,7 +535,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
updateLockIcon();
|
||||
if (_loaded) {
|
||||
updateGroupFilterMenu();
|
||||
_entryListView.setGroups(_vault.getGroups());
|
||||
updateSortCategoryMenu();
|
||||
}
|
||||
|
||||
@@ -593,7 +576,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings: {
|
||||
startPreferencesActivity(null);
|
||||
startPreferencesActivity();
|
||||
return true;
|
||||
}
|
||||
case R.id.action_about: {
|
||||
@@ -605,16 +588,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_app.lock(true);
|
||||
return true;
|
||||
default:
|
||||
if (item.getGroupId() == R.id.action_filter_group) {
|
||||
item.setChecked(true);
|
||||
|
||||
String group = null;
|
||||
if (item.getItemId() != R.id.menu_filter_all) {
|
||||
group = item.getTitle().toString();
|
||||
}
|
||||
setGroupFilter(group);
|
||||
}
|
||||
|
||||
if (item.getGroupId() == R.id.action_sort_category) {
|
||||
item.setChecked(true);
|
||||
|
||||
@@ -795,7 +768,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
return true;
|
||||
|
||||
case R.id.action_edit:
|
||||
startEditEntryActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0), false);
|
||||
startEditEntryActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0));
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
@@ -814,19 +787,19 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
return true;
|
||||
|
||||
case R.id.action_delete:
|
||||
Dialogs.showDeleteEntriesDialog(MainActivity.this, (d, which) -> {
|
||||
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries.stream().map(VaultEntry::getIssuer).collect(Collectors.toList()), (d, which) -> {
|
||||
deleteEntries(_selectedEntries);
|
||||
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
if (entry.getGroup() != null) {
|
||||
if (!_vault.getGroups().contains(entry.getGroup())) {
|
||||
updateGroupFilterMenu();
|
||||
_entryListView.setGroups(_vault.getGroups());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mode.finish();
|
||||
}, _selectedEntries.size());
|
||||
});
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
public class PreferencesActivity extends AegisActivity {
|
||||
private PreferencesFragment _fragment;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.fragments.MainPreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.fragments.PreferencesFragment;
|
||||
|
||||
public class PreferencesActivity extends AegisActivity implements
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
private Fragment _fragment;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_preferences);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
@@ -17,59 +28,97 @@ public class PreferencesActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
_fragment = new PreferencesFragment();
|
||||
_fragment = new MainPreferencesFragment();
|
||||
_fragment.setArguments(getIntent().getExtras());
|
||||
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit();
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.content, _fragment)
|
||||
.commit();
|
||||
|
||||
PreferencesFragment requestedFragment = getRequestedFragment();
|
||||
if (requestedFragment != null) {
|
||||
_fragment = requestedFragment;
|
||||
showFragment(_fragment);
|
||||
}
|
||||
} else {
|
||||
_fragment = (PreferencesFragment) getSupportFragmentManager().findFragmentById(android.R.id.content);
|
||||
_fragment = getSupportFragmentManager().findFragmentById(R.id.content);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
Intent intent = getIntent();
|
||||
String preference = intent.getStringExtra("pref");
|
||||
if (preference != null) {
|
||||
_fragment.scrollToPreference(preference);
|
||||
intent.removeExtra("pref");
|
||||
}
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
setTitle(R.string.action_settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
// pass permission request results to the fragment
|
||||
_fragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(final Bundle inState) {
|
||||
// pass the stored result intent back to the fragment
|
||||
if (inState.containsKey("result")) {
|
||||
_fragment.setResult(inState.getParcelable("result"));
|
||||
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(final Bundle outState) {
|
||||
// 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", _fragment.getResult());
|
||||
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());
|
||||
}
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
|
||||
_fragment = getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment());
|
||||
_fragment.setArguments(pref.getExtras());
|
||||
_fragment.setTargetFragment(caller, 0);
|
||||
showFragment(_fragment);
|
||||
|
||||
setTitle(pref.getTitle());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showFragment(Fragment fragment) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right)
|
||||
.replace(R.id.content, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private PreferencesFragment getRequestedFragment() {
|
||||
Class<? extends PreferencesFragment> fragmentType = (Class<? extends PreferencesFragment>) getIntent().getSerializableExtra("fragment");
|
||||
if (fragmentType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return fragmentType.newInstance();
|
||||
} catch (IllegalAccessException | InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,9 +17,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.vault.VaultEntry;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.zxing.Result;
|
||||
@@ -27,6 +29,8 @@ import com.google.zxing.Result;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Listener {
|
||||
private ProcessCameraProvider _cameraProvider;
|
||||
@@ -36,7 +40,9 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
||||
private int _currentLens;
|
||||
|
||||
private Menu _menu;
|
||||
private ImageAnalysis _analysis;
|
||||
private PreviewView _previewView;
|
||||
private ExecutorService _executor;
|
||||
|
||||
private int _batchId = 0;
|
||||
private int _batchIndex = -1;
|
||||
@@ -46,10 +52,12 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_scanner);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
_entries = new ArrayList<>();
|
||||
_lenses = new ArrayList<>();
|
||||
_previewView = findViewById(R.id.preview_view);
|
||||
_executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
_cameraProviderFuture = ProcessCameraProvider.getInstance(this);
|
||||
_cameraProviderFuture.addListener(() -> {
|
||||
@@ -75,9 +83,15 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
_executor.shutdownNow();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
setTheme(R.style.AppTheme_Fullscreen);
|
||||
setTheme(ThemeMap.FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,7 +104,7 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_camera) {
|
||||
_cameraProvider.unbindAll();
|
||||
unbindPreview(_cameraProvider);
|
||||
_currentLens = _currentLens == CameraSelector.LENS_FACING_BACK ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
|
||||
bindPreview(_cameraProvider);
|
||||
updateCameraIcon();
|
||||
@@ -137,27 +151,38 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
||||
.requireLensFacing(_currentLens)
|
||||
.build();
|
||||
|
||||
ImageAnalysis analysis = new ImageAnalysis.Builder()
|
||||
_analysis = new ImageAnalysis.Builder()
|
||||
.setTargetResolution(QrCodeAnalyzer.RESOLUTION)
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build();
|
||||
analysis.setAnalyzer(ContextCompat.getMainExecutor(this), new QrCodeAnalyzer(this));
|
||||
_analysis.setAnalyzer(_executor, new QrCodeAnalyzer(this));
|
||||
|
||||
cameraProvider.bindToLifecycle(this, selector, preview, analysis);
|
||||
cameraProvider.bindToLifecycle(this, selector, preview, _analysis);
|
||||
}
|
||||
|
||||
private void unbindPreview(@NonNull ProcessCameraProvider cameraProvider) {
|
||||
_analysis = null;
|
||||
cameraProvider.unbindAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQrCodeDetected(Result result) {
|
||||
try {
|
||||
Uri uri = Uri.parse(result.getText().trim());
|
||||
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
|
||||
handleExportUri(uri);
|
||||
} else {
|
||||
handleUri(uri);
|
||||
if (_analysis != null) {
|
||||
try {
|
||||
Uri uri = Uri.parse(result.getText().trim());
|
||||
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
|
||||
handleExportUri(uri);
|
||||
} else {
|
||||
handleUri(uri);
|
||||
}
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
unbindPreview(_cameraProvider);
|
||||
Dialogs.showErrorDialog(this,
|
||||
e.isPhoneFactor() ? R.string.read_qr_error_phonefactor : R.string.read_qr_error,
|
||||
e, ((dialog, which) -> bindPreview(_cameraProvider)));
|
||||
}
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.read_qr_error, e, ((dialog, which) -> bindPreview(_cameraProvider)));
|
||||
_cameraProvider.unbindAll();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
|
||||
import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
|
||||
import com.getbase.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class SelectEntriesActivity extends AegisActivity {
|
||||
private ImportEntriesAdapter _adapter;
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
private boolean _vaultContainsEntries;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_select_entries);
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
_adapter = new ImportEntriesAdapter();
|
||||
RecyclerView 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);
|
||||
onScroll(dx, dy);
|
||||
}
|
||||
});
|
||||
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
entriesView.setLayoutManager(layoutManager);
|
||||
entriesView.setAdapter(_adapter);
|
||||
entriesView.setNestedScrollingEnabled(false);
|
||||
|
||||
Intent intent = getIntent();
|
||||
List<ImportEntry> entries = (ArrayList<ImportEntry>) intent.getSerializableExtra("entries");
|
||||
List<DatabaseImporterEntryException> errors = (ArrayList<DatabaseImporterEntryException>) intent.getSerializableExtra("errors");
|
||||
_vaultContainsEntries = intent.getBooleanExtra("vaultContainsEntries", false);
|
||||
|
||||
for (ImportEntry entry : entries) {
|
||||
_adapter.addEntry(entry);
|
||||
}
|
||||
|
||||
if (errors.size() > 0) {
|
||||
showErrorDialog(errors);
|
||||
}
|
||||
|
||||
FloatingActionButton fabMenu = findViewById(R.id.fab);
|
||||
fabMenu.setOnClickListener(v -> {
|
||||
if (_vaultContainsEntries) {
|
||||
showWipeEntriesDialog();
|
||||
} else {
|
||||
returnSelectedEntries(false);
|
||||
}
|
||||
});
|
||||
_fabScrollHelper = new FabScrollHelper(fabMenu);
|
||||
}
|
||||
|
||||
private void showErrorDialog(List<DatabaseImporterEntryException> errors) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.import_error_title)
|
||||
.setMessage(getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size()))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(getString(R.string.details), (dialog, which) -> showDetailedErrorDialog(errors))
|
||||
.create());
|
||||
}
|
||||
|
||||
private void showDetailedErrorDialog(List<DatabaseImporterEntryException> errors) {
|
||||
List<String> messages = new ArrayList<>();
|
||||
for (DatabaseImporterEntryException e : errors) {
|
||||
messages.add(e.getMessage());
|
||||
}
|
||||
|
||||
String message = TextUtils.join("\n\n", messages);
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.import_error_title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(android.R.string.copy, (dialog2, which2) -> {
|
||||
ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("text/plain", message);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(this, R.string.errors_copied, Toast.LENGTH_SHORT).show();
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
private void showWipeEntriesDialog() {
|
||||
Dialogs.showCheckboxDialog(this, R.string.dialog_wipe_entries_title,
|
||||
R.string.dialog_wipe_entries_message,
|
||||
R.string.dialog_wipe_entries_checkbox,
|
||||
this::returnSelectedEntries
|
||||
);
|
||||
}
|
||||
|
||||
private void returnSelectedEntries(boolean wipeEntries) {
|
||||
Intent intent = new Intent();
|
||||
List<ImportEntry> entries = _adapter.getCheckedEntries();
|
||||
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
|
||||
intent.putExtra("wipeEntries", wipeEntries);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
public void onScroll(int dx, int dy) {
|
||||
_fabScrollHelper.onScroll(dx, dy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_select_entries, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
case R.id.toggle_checkboxes:
|
||||
_adapter.toggleCheckboxes();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
@@ -39,6 +40,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_slots);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
_edited = false;
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.widget.TextView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.WriterException;
|
||||
@@ -37,6 +38,7 @@ public class TransferEntriesActivity extends AegisActivity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_share_entry);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
_vault = getApp().getVaultManager();
|
||||
|
||||
_qrImage = findViewById(R.id.ivQrCode);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.beemdevelopment.aegis.ui.dialogs;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class ChangelogDialog extends SimpleWebViewDialog {
|
||||
private ChangelogDialog() {
|
||||
super(R.string.changelog);
|
||||
}
|
||||
|
||||
public static ChangelogDialog create() {
|
||||
return new ChangelogDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getContent(Context context) {
|
||||
String content = readAssetAsString(context, "changelog.html");
|
||||
return String.format(content, getBackgroundColor(), getTextColor());
|
||||
}
|
||||
}
|
||||
+30
-20
@@ -1,4 +1,4 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
package com.beemdevelopment.aegis.ui.dialogs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
@@ -10,6 +10,7 @@ import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.PasswordTransformationMethod;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -38,6 +39,7 @@ import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.nulabinc.zxcvbn.Strength;
|
||||
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||
@@ -63,28 +65,25 @@ public class Dialogs {
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public static void showDeleteEntryDialog(Activity activity, DialogInterface.OnClickListener onDelete) {
|
||||
showSecureDialog(new AlertDialog.Builder(activity)
|
||||
.setTitle(activity.getString(R.string.delete_entry))
|
||||
.setMessage(activity.getString(R.string.delete_entry_description))
|
||||
.setPositiveButton(android.R.string.yes, onDelete)
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
}
|
||||
public static void showDeleteEntriesDialog(Activity activity, List<String> services, DialogInterface.OnClickListener onDelete) {
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_delete_entry, null);
|
||||
TextView textMessage = view.findViewById(R.id.text_message);
|
||||
TextView textExplanation = view.findViewById(R.id.text_explanation);
|
||||
textExplanation.setText(activity.getString(R.string.delete_entry_explanation, TextUtils.join(", ", services)));
|
||||
|
||||
public static void showDeleteEntriesDialog(Activity activity, DialogInterface.OnClickListener onDelete, int totalEntries) {
|
||||
String title, message;
|
||||
if (totalEntries > 1) {
|
||||
if (services.size() > 1) {
|
||||
title = activity.getString(R.string.delete_entries);
|
||||
message = activity.getResources().getQuantityString(R.plurals.delete_entries_description, totalEntries, totalEntries);
|
||||
message = activity.getResources().getQuantityString(R.plurals.delete_entries_description, services.size(), services.size());
|
||||
} else {
|
||||
title = activity.getString(R.string.delete_entry);
|
||||
message = activity.getString(R.string.delete_entry_description);
|
||||
}
|
||||
textMessage.setText(message);
|
||||
|
||||
showSecureDialog(new AlertDialog.Builder(activity)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.yes, onDelete)
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
@@ -188,13 +187,14 @@ public class Dialogs {
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener, boolean isSecret) {
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener, boolean isSecret) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
|
||||
EditText input = view.findViewById(R.id.text_input);
|
||||
TextInputEditText input = view.findViewById(R.id.text_input);
|
||||
TextInputLayout inputLayout = view.findViewById(R.id.text_input_layout);
|
||||
if (isSecret) {
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
}
|
||||
input.setHint(hintId);
|
||||
inputLayout.setHint(hintId);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
.setTitle(titleId)
|
||||
@@ -204,8 +204,8 @@ public class Dialogs {
|
||||
listener.onTextInputResult(text);
|
||||
});
|
||||
|
||||
if (dismissListener != null) {
|
||||
builder.setOnDismissListener(dismissListener);
|
||||
if (cancelListener != null) {
|
||||
builder.setOnCancelListener(cancelListener);
|
||||
}
|
||||
|
||||
if (messageId != 0) {
|
||||
@@ -213,6 +213,7 @@ public class Dialogs {
|
||||
}
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.setCanceledOnTouchOutside(true);
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
@@ -229,12 +230,20 @@ public class Dialogs {
|
||||
showTextInputDialog(context, R.string.set_password, R.string.password, listener, true);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
|
||||
showTextInputDialog(context, R.string.set_password, 0, R.string.password, listener, cancelListener, true);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
|
||||
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, null, true);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int setPasswordMessageId, @StringRes int messageId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener) {
|
||||
showTextInputDialog(context, setPasswordMessageId, messageId, R.string.password, listener, dismissListener, true);
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
|
||||
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, cancelListener, true);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
|
||||
showTextInputDialog(context, titleId, messageId, R.string.password, listener, cancelListener, true);
|
||||
}
|
||||
|
||||
public static void showCheckboxDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int checkboxMessageId, CheckboxInputListener listener) {
|
||||
@@ -333,6 +342,7 @@ public class Dialogs {
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog1, which);
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.beemdevelopment.aegis.ui.dialogs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||
import com.beemdevelopment.aegis.ui.views.IconRecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.ListPreloader;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IconPickerDialog {
|
||||
private IconPickerDialog() {
|
||||
|
||||
}
|
||||
|
||||
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, IconAdapter.Listener listener) {
|
||||
View view = LayoutInflater.from(activity).inflate(R.layout.dialog_icon_picker, null);
|
||||
TextView textIconPack = view.findViewById(R.id.text_icon_pack);
|
||||
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(activity);
|
||||
dialog.setContentView(view);
|
||||
dialog.create();
|
||||
|
||||
FrameLayout rootView = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet);
|
||||
rootView.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
|
||||
IconAdapter adapter = new IconAdapter(dialog.getContext(), issuer, new IconAdapter.Listener() {
|
||||
@Override
|
||||
public void onIconSelected(IconPack.Icon icon) {
|
||||
dialog.dismiss();
|
||||
listener.onIconSelected(icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomSelected() {
|
||||
dialog.dismiss();
|
||||
listener.onCustomSelected();
|
||||
}
|
||||
});
|
||||
|
||||
class IconPreloadProvider implements ListPreloader.PreloadModelProvider<IconPack.Icon> {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<IconPack.Icon> getPreloadItems(int position) {
|
||||
IconPack.Icon icon = adapter.getIconAt(position);
|
||||
return Collections.singletonList(icon);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
|
||||
return Glide.with(dialog.getContext())
|
||||
.asDrawable()
|
||||
.load(icon.getFile())
|
||||
.set(IconLoader.ICON_TYPE, icon.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false);
|
||||
}
|
||||
}
|
||||
|
||||
TextInputEditText iconSearch = view.findViewById(R.id.text_search_icon);
|
||||
iconSearch.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) {
|
||||
BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(rootView);
|
||||
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
}
|
||||
});
|
||||
iconSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
String query = s.toString();
|
||||
adapter.setQuery(query.isEmpty() ? null : query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
ViewPreloadSizeProvider<IconPack.Icon> preloadSizeProvider = new ViewPreloadSizeProvider<>();
|
||||
IconPreloadProvider modelProvider = new IconPreloadProvider();
|
||||
RecyclerViewPreloader<IconPack.Icon> preloader = new RecyclerViewPreloader<>(activity, modelProvider, preloadSizeProvider, 10);
|
||||
IconRecyclerView recyclerView = view.findViewById(R.id.list_icons);
|
||||
GridLayoutManager layoutManager = recyclerView.getGridLayoutManager();
|
||||
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
if (adapter.getItemViewType(position) == R.layout.card_icon) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return recyclerView.getSpanCount();
|
||||
}
|
||||
});
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.addOnScrollListener(preloader);
|
||||
adapter.loadIcons(iconPacks.get(0));
|
||||
textIconPack.setText(iconPacks.get(0).getName());
|
||||
|
||||
view.findViewById(R.id.btn_icon_pack).setOnClickListener(v -> {
|
||||
List<String> iconPackNames = iconPacks.stream()
|
||||
.map(IconPack::getName)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PopupMenu popupMenu = new PopupMenu(activity, v);
|
||||
popupMenu.setOnMenuItemClickListener(item -> {
|
||||
IconPack pack = iconPacks.get(iconPackNames.indexOf(item.getTitle().toString()));
|
||||
adapter.loadIcons(pack);
|
||||
|
||||
String query = iconSearch.getText().toString();
|
||||
if (!query.isEmpty()) {
|
||||
adapter.setQuery(query);
|
||||
}
|
||||
|
||||
textIconPack.setText(pack.getName());
|
||||
return true;
|
||||
});
|
||||
Menu menu = popupMenu.getMenu();
|
||||
for (String name : iconPackNames) {
|
||||
menu.add(name);
|
||||
}
|
||||
|
||||
popupMenu.show();
|
||||
});
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.beemdevelopment.aegis.ui.dialogs;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class LicenseDialog extends SimpleWebViewDialog {
|
||||
private LicenseDialog() {
|
||||
super(R.string.license);
|
||||
}
|
||||
|
||||
public static LicenseDialog create() {
|
||||
return new LicenseDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getContent(Context context) {
|
||||
String license = readAssetAsString(context, "LICENSE");
|
||||
String html = readAssetAsString(context, "license.html");
|
||||
return String.format(html, license, getBackgroundColor(), getTextColor());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.beemdevelopment.aegis.ui.dialogs;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.InflateException;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.google.common.io.CharStreams;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public abstract class SimpleWebViewDialog extends DialogFragment {
|
||||
private Theme _theme;
|
||||
private final @StringRes int _title;
|
||||
|
||||
protected SimpleWebViewDialog(@StringRes int title) {
|
||||
_title = title;
|
||||
}
|
||||
|
||||
protected abstract String getContent(Context context);
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final View view;
|
||||
try {
|
||||
view = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_web_view, null);
|
||||
} catch (InflateException e) {
|
||||
e.printStackTrace();
|
||||
return new AlertDialog.Builder(getActivity())
|
||||
.setTitle(android.R.string.dialog_alert_title)
|
||||
.setMessage(getString(R.string.webview_error))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(getActivity())
|
||||
.setTitle(_title)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
|
||||
String content = getContent(getContext());
|
||||
final WebView webView = view.findViewById(R.id.web_view);
|
||||
webView.loadData(content, "text/html", "UTF-8");
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public SimpleWebViewDialog setTheme(Theme theme) {
|
||||
_theme = theme;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected String getBackgroundColor() {
|
||||
int backgroundColorResource = _theme == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
return colorToCSS(ThemeHelper.getThemeColor(backgroundColorResource, getContext().getTheme()));
|
||||
}
|
||||
|
||||
protected String getTextColor() {
|
||||
return colorToCSS(0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getContext().getTheme()));
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private static String colorToCSS(int color) {
|
||||
return String.format("rgb(%d, %d, %d)", Color.red(color), Color.green(color), Color.blue(color));
|
||||
}
|
||||
|
||||
protected static String readAssetAsString(Context context, String name) {
|
||||
try (InputStream inStream = context.getAssets().open(name);
|
||||
InputStreamReader reader = new InputStreamReader(inStream, StandardCharsets.UTF_8)) {
|
||||
return CharStreams.toString(reader);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package com.beemdevelopment.aegis.ui.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.GroupManagerActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
||||
public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
private Preference _groupsPreference;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
addPreferencesFromResource(R.xml.preferences_appearance);
|
||||
Preferences prefs = getPreferences();
|
||||
|
||||
_groupsPreference = findPreference("pref_groups");
|
||||
_groupsPreference.setOnPreferenceClickListener(preference -> {
|
||||
Intent intent = new Intent(getActivity(), GroupManagerActivity.class);
|
||||
intent.putExtra("groups", new ArrayList<>(getVault().getGroups()));
|
||||
startActivityForResult(intent, CODE_GROUPS);
|
||||
return true;
|
||||
});
|
||||
|
||||
int currentTheme = prefs.getCurrentTheme().ordinal();
|
||||
Preference darkModePreference = findPreference("pref_dark_mode");
|
||||
darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme]));
|
||||
darkModePreference.setOnPreferenceClickListener(preference -> {
|
||||
int currentTheme1 = prefs.getCurrentTheme().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.choose_theme)
|
||||
.setSingleChoiceItems(R.array.theme_titles, currentTheme1, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
prefs.setCurrentTheme(Theme.fromInteger(i));
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
getActivity().recreate();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create());
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference langPreference = findPreference("pref_lang");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
getActivity().recreate();
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
// Setting locale doesn't work on Marshmallow or below
|
||||
langPreference.setVisible(false);
|
||||
}
|
||||
|
||||
int currentViewMode = prefs.getCurrentViewMode().ordinal();
|
||||
Preference viewModePreference = findPreference("pref_view_mode");
|
||||
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode]));
|
||||
viewModePreference.setOnPreferenceClickListener(preference -> {
|
||||
int currentViewMode1 = prefs.getCurrentViewMode().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.choose_view_mode)
|
||||
.setSingleChoiceItems(R.array.view_mode_titles, currentViewMode1, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
prefs.setCurrentViewMode(ViewMode.fromInteger(i));
|
||||
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i]));
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create());
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference codeDigitGroupingPreference = findPreference("pref_code_group_size");
|
||||
codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference issuerPreference = findPreference("pref_account_name");
|
||||
issuerPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (data != null && requestCode == CODE_GROUPS) {
|
||||
onGroupManagerResult(resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void onGroupManagerResult(int resultCode, Intent data) {
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<String> groups = new HashSet<>(data.getStringArrayListExtra("groups"));
|
||||
|
||||
for (VaultEntry entry : getVault().getEntries()) {
|
||||
if (!groups.contains(entry.getGroup())) {
|
||||
entry.setGroup(null);
|
||||
}
|
||||
}
|
||||
|
||||
saveVault();
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package com.beemdevelopment.aegis.ui.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
|
||||
public class BackupsPreferencesFragment extends PreferencesFragment {
|
||||
private SwitchPreferenceCompat _androidBackupsPreference;
|
||||
private SwitchPreferenceCompat _backupsPreference;
|
||||
private Preference _backupsLocationPreference;
|
||||
private Preference _backupsTriggerPreference;
|
||||
private Preference _backupsVersionsPreference;
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateBackupPreference();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
addPreferencesFromResource(R.xml.preferences_backups);
|
||||
Preferences prefs = getPreferences();
|
||||
|
||||
_backupsPreference = findPreference("pref_backups");
|
||||
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if ((boolean) newValue) {
|
||||
selectBackupsLocation();
|
||||
} else {
|
||||
prefs.setIsBackupsEnabled(false);
|
||||
updateBackupPreference();
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
_androidBackupsPreference = findPreference("pref_android_backups");
|
||||
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
prefs.setIsAndroidBackupsEnabled((boolean) newValue);
|
||||
updateBackupPreference();
|
||||
getVault().androidBackupDataChanged();
|
||||
return false;
|
||||
});
|
||||
|
||||
Uri backupLocation = prefs.getBackupsLocation();
|
||||
_backupsLocationPreference = findPreference("pref_backups_location");
|
||||
if (backupLocation != null) {
|
||||
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
|
||||
}
|
||||
_backupsLocationPreference.setOnPreferenceClickListener(preference -> {
|
||||
selectBackupsLocation();
|
||||
return false;
|
||||
});
|
||||
|
||||
_backupsTriggerPreference = findPreference("pref_backups_trigger");
|
||||
_backupsTriggerPreference.setOnPreferenceClickListener(preference -> {
|
||||
if (prefs.isBackupsEnabled()) {
|
||||
try {
|
||||
getVault().backup();
|
||||
Toast.makeText(getActivity(), R.string.backup_successful, Toast.LENGTH_LONG).show();
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.backup_error, e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
_backupsVersionsPreference = findPreference("pref_backups_versions");
|
||||
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, prefs.getBackupsVersionCount(), prefs.getBackupsVersionCount()));
|
||||
_backupsVersionsPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> {
|
||||
number = number * 5 + 5;
|
||||
prefs.setBackupsVersionCount(number);
|
||||
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, prefs.getBackupsVersionCount(), prefs.getBackupsVersionCount()));
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (data != null && requestCode == CODE_BACKUPS) {
|
||||
onSelectBackupsLocationResult(resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void onSelectBackupsLocationResult(int resultCode, Intent data) {
|
||||
Uri uri = data.getData();
|
||||
if (resultCode != Activity.RESULT_OK || uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
getContext().getContentResolver().takePersistableUriPermission(data.getData(), flags);
|
||||
|
||||
Preferences prefs = getPreferences();
|
||||
prefs.setBackupsLocation(uri);
|
||||
prefs.setIsBackupsEnabled(true);
|
||||
prefs.setBackupsError(null);
|
||||
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
|
||||
updateBackupPreference();
|
||||
}
|
||||
|
||||
private void updateBackupPreference() {
|
||||
boolean encrypted = getVault().isEncryptionEnabled();
|
||||
boolean androidBackupEnabled = getPreferences().isAndroidBackupsEnabled() && encrypted;
|
||||
boolean backupEnabled = getPreferences().isBackupsEnabled() && encrypted;
|
||||
_androidBackupsPreference.setChecked(androidBackupEnabled);
|
||||
_androidBackupsPreference.setEnabled(encrypted);
|
||||
_backupsPreference.setChecked(backupEnabled);
|
||||
_backupsPreference.setEnabled(encrypted);
|
||||
_backupsLocationPreference.setVisible(backupEnabled);
|
||||
_backupsTriggerPreference.setVisible(backupEnabled);
|
||||
_backupsVersionsPreference.setVisible(backupEnabled);
|
||||
}
|
||||
|
||||
private void selectBackupsLocation() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||
|
||||
getApp().setBlockAutoLock(true);
|
||||
startActivityForResult(intent, CODE_BACKUPS);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.beemdevelopment.aegis.ui.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class BehaviorPreferencesFragment extends PreferencesFragment {
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
addPreferencesFromResource(R.xml.preferences_behavior);
|
||||
|
||||
Preference copyOnTapPreference = findPreference("pref_copy_on_tap");
|
||||
copyOnTapPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
|
||||
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package com.beemdevelopment.aegis.ui.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconPackException;
|
||||
import com.beemdevelopment.aegis.icons.IconPackExistsException;
|
||||
import com.beemdevelopment.aegis.icons.IconPackManager;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ImportIconPackTask;
|
||||
import com.beemdevelopment.aegis.ui.views.IconPackAdapter;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
public class IconPacksManagerFragment extends Fragment implements IconPackAdapter.Listener {
|
||||
private static final int CODE_IMPORT = 0;
|
||||
|
||||
private IconPackAdapter _adapter;
|
||||
private IconPackManager _iconPackManager;
|
||||
|
||||
private View _iconPacksView;
|
||||
private RecyclerView _iconPacksRecyclerView;
|
||||
private LinearLayout _noIconPacksView;
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
public IconPacksManagerFragment() {
|
||||
super(R.layout.fragment_icon_packs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
|
||||
_iconPackManager = ((AegisApplication) getContext().getApplicationContext()).getIconPackManager();
|
||||
|
||||
FloatingActionButton fab = view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(v -> startImportIconPack());
|
||||
_fabScrollHelper = new FabScrollHelper(fab);
|
||||
|
||||
_noIconPacksView = view.findViewById(R.id.vEmptyList);
|
||||
((TextView) view.findViewById(R.id.txt_no_icon_packs)).setMovementMethod(LinkMovementMethod.getInstance());
|
||||
_iconPacksView = view.findViewById(R.id.view_icon_packs);
|
||||
_adapter = new IconPackAdapter(this);
|
||||
_iconPacksRecyclerView = view.findViewById(R.id.list_icon_packs);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
|
||||
_iconPacksRecyclerView.setLayoutManager(layoutManager);
|
||||
_iconPacksRecyclerView.setAdapter(_adapter);
|
||||
_iconPacksRecyclerView.setNestedScrollingEnabled(false);
|
||||
_iconPacksRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
_fabScrollHelper.onScroll(dx, dy);
|
||||
}
|
||||
});
|
||||
|
||||
for (IconPack pack : _iconPackManager.getIconPacks()) {
|
||||
_adapter.addIconPack(pack);
|
||||
}
|
||||
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveIconPack(IconPack pack) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.remove_icon_pack)
|
||||
.setMessage(R.string.remove_icon_pack_description)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
try {
|
||||
_iconPackManager.removeIconPack(pack);
|
||||
} catch (IconPackException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_delete_error, e);
|
||||
return;
|
||||
}
|
||||
_adapter.removeIconPack(pack);
|
||||
updateEmptyState();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == CODE_IMPORT && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
|
||||
importIconPack(data.getData());
|
||||
}
|
||||
}
|
||||
|
||||
private void importIconPack(Uri uri) {
|
||||
ImportIconPackTask task = new ImportIconPackTask(getContext(), result -> {
|
||||
Exception e = result.getException();
|
||||
if (e instanceof IconPackExistsException) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setMessage(R.string.icon_pack_import_exists_error)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
if (removeIconPack(((IconPackExistsException) e).getIconPack())) {
|
||||
importIconPack(uri);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.create());
|
||||
} else if (e != null) {
|
||||
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_import_error, e);
|
||||
} else {
|
||||
_adapter.addIconPack(result.getIconPack());
|
||||
updateEmptyState();
|
||||
}
|
||||
});
|
||||
task.execute(getLifecycle(), new ImportIconPackTask.Params(_iconPackManager, uri));
|
||||
}
|
||||
|
||||
private boolean removeIconPack(IconPack pack) {
|
||||
try {
|
||||
_iconPackManager.removeIconPack(pack);
|
||||
} catch (IconPackException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_delete_error, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
_adapter.removeIconPack(pack);
|
||||
updateEmptyState();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startImportIconPack() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("application/zip");
|
||||
startActivityForResult(intent, CODE_IMPORT);
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
if (_adapter.getItemCount() > 0) {
|
||||
_iconPacksView.setVisibility(View.VISIBLE);
|
||||
_noIconPacksView.setVisibility(View.GONE);
|
||||
} else {
|
||||
_iconPacksView.setVisibility(View.GONE);
|
||||
_noIconPacksView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
package com.beemdevelopment.aegis.ui.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.ui.ImportEntriesActivity;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ExportTask;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultBackupManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
// keep a reference to the type of database converter that was selected
|
||||
private Class<? extends DatabaseImporter> _importerType;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
addPreferencesFromResource(R.xml.preferences_import_export);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
_importerType = (Class<? extends DatabaseImporter>) savedInstanceState.getSerializable("importerType");
|
||||
}
|
||||
|
||||
Preference importPreference = findPreference("pref_import");
|
||||
importPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showImportersDialog(getContext(), false, definition -> {
|
||||
_importerType = definition.getType();
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, CODE_IMPORT_SELECT);
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference importAppPreference = findPreference("pref_import_app");
|
||||
importAppPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showImportersDialog(getContext(), true, definition -> {
|
||||
startImportEntriesActivity(definition.getType(), null);
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference exportPreference = findPreference("pref_export");
|
||||
exportPreference.setOnPreferenceClickListener(preference -> {
|
||||
startExport();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putSerializable("importerType", _importerType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == CODE_IMPORT) {
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
} else if (data != null) {
|
||||
switch (requestCode) {
|
||||
case CODE_IMPORT_SELECT:
|
||||
onImportSelectResult(resultCode, data);
|
||||
break;
|
||||
case CODE_EXPORT:
|
||||
// intentional fallthrough
|
||||
case CODE_EXPORT_PLAIN:
|
||||
// intentional fallthrough
|
||||
case CODE_EXPORT_GOOGLE_URI:
|
||||
onExportResult(requestCode, resultCode, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onImportSelectResult(int resultCode, Intent data) {
|
||||
Uri uri = data.getData();
|
||||
if (resultCode != Activity.RESULT_OK || uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImportFileTask task = new ImportFileTask(getContext(), result -> {
|
||||
if (result.getException() == null) {
|
||||
startImportEntriesActivity(_importerType, result.getFile());
|
||||
} else {
|
||||
Dialogs.showErrorDialog(getContext(), R.string.reading_file_error, result.getException());
|
||||
}
|
||||
});
|
||||
task.execute(getLifecycle(), uri);
|
||||
}
|
||||
|
||||
private void startImportEntriesActivity(Class<? extends DatabaseImporter> importerType, File file) {
|
||||
Intent intent = new Intent(getActivity(), ImportEntriesActivity.class);
|
||||
intent.putExtra("importerType", importerType);
|
||||
intent.putExtra("file", file);
|
||||
startActivityForResult(intent, CODE_IMPORT);
|
||||
}
|
||||
|
||||
private void startExport() {
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_export, null);
|
||||
TextView warningText = view.findViewById(R.id.text_export_warning);
|
||||
CheckBox checkBoxEncrypt = view.findViewById(R.id.checkbox_export_encrypt);
|
||||
CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept);
|
||||
AutoCompleteTextView dropdown = view.findViewById(R.id.dropdown_export_format);
|
||||
DropdownHelper.fillDropdown(getContext(), dropdown, R.array.export_formats);
|
||||
dropdown.setText(getString(R.string.export_format_aegis), false);
|
||||
dropdown.setOnItemClickListener((parent, view1, position, id) -> {
|
||||
checkBoxEncrypt.setChecked(position == 0);
|
||||
checkBoxEncrypt.setEnabled(position == 0);
|
||||
warningText.setVisibility(checkBoxEncrypt.isChecked() ? View.GONE : View.VISIBLE);
|
||||
});
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.pref_export_summary)
|
||||
.setView(view)
|
||||
.setNeutralButton(R.string.share, null)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
Button btnNeutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
|
||||
checkBoxEncrypt.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
warningText.setVisibility(isChecked ? View.GONE : View.VISIBLE);
|
||||
checkBoxAccept.setVisibility(isChecked ? View.GONE : View.VISIBLE);
|
||||
checkBoxAccept.setChecked(false);
|
||||
btnPos.setEnabled(isChecked);
|
||||
btnNeutral.setEnabled(isChecked);
|
||||
});
|
||||
|
||||
checkBoxAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
btnPos.setEnabled(isChecked);
|
||||
btnNeutral.setEnabled(isChecked);
|
||||
});
|
||||
|
||||
btnPos.setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
|
||||
if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int pos = getStringResourceIndex(R.array.export_formats, dropdown.getText().toString());
|
||||
int requestCode = getExportRequestCode(pos, checkBoxEncrypt.isChecked());
|
||||
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked());
|
||||
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType(getExportMimeType(requestCode))
|
||||
.putExtra(Intent.EXTRA_TITLE, fileInfo.toString());
|
||||
startActivityForResult(intent, requestCode);
|
||||
});
|
||||
|
||||
btnNeutral.setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
|
||||
int pos = getStringResourceIndex(R.array.export_formats, dropdown.getText().toString());
|
||||
if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
File file;
|
||||
try {
|
||||
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked());
|
||||
file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
int requestCode = getExportRequestCode(pos, checkBoxEncrypt.isChecked());
|
||||
startExportVault(requestCode, cb -> {
|
||||
try (OutputStream stream = new FileOutputStream(file)) {
|
||||
cb.exportVault(stream);
|
||||
} catch (IOException | VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = FileProvider.getUriForFile(getContext(), BuildConfig.FILE_PROVIDER_AUTHORITY, file);
|
||||
Intent intent = new Intent(Intent.ACTION_SEND)
|
||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setType(getExportMimeType(requestCode))
|
||||
.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
Intent chooser = Intent.createChooser(intent, getString(R.string.pref_export_summary));
|
||||
startActivity(chooser);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private static int getExportRequestCode(int spinnerPos, boolean encrypt) {
|
||||
if (spinnerPos == 0) {
|
||||
return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN;
|
||||
}
|
||||
|
||||
return CODE_EXPORT_GOOGLE_URI;
|
||||
}
|
||||
|
||||
private static VaultBackupManager.FileInfo getExportFileInfo(int spinnerPos, boolean encrypt) {
|
||||
if (spinnerPos == 0) {
|
||||
String filename = encrypt ? VaultManager.FILENAME_PREFIX_EXPORT : VaultManager.FILENAME_PREFIX_EXPORT_PLAIN;
|
||||
return new VaultBackupManager.FileInfo(filename);
|
||||
}
|
||||
|
||||
return new VaultBackupManager.FileInfo(VaultManager.FILENAME_PREFIX_EXPORT_URI, "txt");
|
||||
}
|
||||
|
||||
private static String getExportMimeType(int requestCode) {
|
||||
return requestCode == CODE_EXPORT_GOOGLE_URI ? "text/plain" : "application/json";
|
||||
}
|
||||
|
||||
private File getExportCacheDir() throws IOException {
|
||||
File dir = new File(getContext().getCacheDir(), "export");
|
||||
if (!dir.exists() && !dir.mkdir()) {
|
||||
throw new IOException(String.format("Unable to create directory %s", dir));
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
private void startExportVault(int requestCode, StartExportCallback cb) {
|
||||
switch (requestCode) {
|
||||
case CODE_EXPORT:
|
||||
if (getVault().isEncryptionEnabled()) {
|
||||
cb.exportVault(stream -> getVault().export(stream));
|
||||
} else {
|
||||
Dialogs.showSetPasswordDialog(getActivity(), new Dialogs.SlotListener() {
|
||||
@Override
|
||||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
VaultFileCredentials creds = new VaultFileCredentials();
|
||||
|
||||
try {
|
||||
slot.setKey(creds.getKey(), cipher);
|
||||
creds.getSlots().add(slot);
|
||||
} catch (SlotException e) {
|
||||
onException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
cb.exportVault(stream -> getVault().export(stream, creds));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case CODE_EXPORT_PLAIN:
|
||||
cb.exportVault((stream) -> getVault().export(stream, null));
|
||||
break;
|
||||
case CODE_EXPORT_GOOGLE_URI:
|
||||
cb.exportVault((stream) -> getVault().exportGoogleUris(stream));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onExportResult(int requestCode, int resultCode, Intent data) {
|
||||
Uri uri = data.getData();
|
||||
if (resultCode != Activity.RESULT_OK || uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
startExportVault(requestCode, cb -> {
|
||||
File file;
|
||||
OutputStream outStream = null;
|
||||
try {
|
||||
file = File.createTempFile(VaultManager.FILENAME_PREFIX_EXPORT + "-", ".json", getExportCacheDir());
|
||||
outStream = new FileOutputStream(file);
|
||||
cb.exportVault(outStream);
|
||||
|
||||
new ExportTask(getContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri));
|
||||
} catch (VaultManagerException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
|
||||
} finally {
|
||||
try {
|
||||
if (outStream != null) {
|
||||
outStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int getStringResourceIndex(@ArrayRes int id, String string) {
|
||||
String[] res = getResources().getStringArray(id);
|
||||
for (int i = 0; i < res.length; i++) {
|
||||
if (res[i].equalsIgnoreCase(string)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private class ExportResultListener implements ExportTask.Callback {
|
||||
@Override
|
||||
public void onTaskFinished(Exception e) {
|
||||
if (e != null) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
|
||||
} else {
|
||||
Toast.makeText(getContext(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface FinishExportCallback {
|
||||
void exportVault(OutputStream stream) throws IOException, VaultManagerException;
|
||||
}
|
||||
|
||||
private interface StartExportCallback {
|
||||
void exportVault(FinishExportCallback exportCb);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.beemdevelopment.aegis.ui.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.beemdevelopment.aegis.ui.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
|
||||
public abstract class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
// activity request codes
|
||||
public static final int CODE_IMPORT_SELECT = 0;
|
||||
public static final int CODE_SLOTS = 2;
|
||||
public static final int CODE_GROUPS = 3;
|
||||
public static final int CODE_IMPORT = 4;
|
||||
public static final int CODE_EXPORT = 5;
|
||||
public static final int CODE_EXPORT_PLAIN = 6;
|
||||
public static final int CODE_EXPORT_GOOGLE_URI = 7;
|
||||
public static final int CODE_BACKUPS = 8;
|
||||
|
||||
private AegisApplication _app;
|
||||
private Intent _result;
|
||||
private Preferences _prefs;
|
||||
private VaultManager _vault;
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
_app = (AegisApplication) getActivity().getApplication();
|
||||
_prefs = _app.getPreferences();
|
||||
_vault = _app.getVaultManager();
|
||||
|
||||
setResult(new Intent());
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
Intent intent = getActivity().getIntent();
|
||||
String preference = intent.getStringExtra("pref");
|
||||
if (preference != null) {
|
||||
scrollToPreference(preference);
|
||||
intent.removeExtra("pref");
|
||||
}
|
||||
}
|
||||
|
||||
public Intent getResult() {
|
||||
return _result;
|
||||
}
|
||||
|
||||
public void setResult(Intent result) {
|
||||
_result = result;
|
||||
getActivity().setResult(Activity.RESULT_OK, _result);
|
||||
}
|
||||
|
||||
protected AegisApplication getApp() {
|
||||
return _app;
|
||||
}
|
||||
|
||||
protected Preferences getPreferences() {
|
||||
return _prefs;
|
||||
}
|
||||
|
||||
protected VaultManager getVault() {
|
||||
return _vault;
|
||||
}
|
||||
|
||||
protected boolean saveVault() {
|
||||
try {
|
||||
_vault.save(true);
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.saving_error, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
package com.beemdevelopment.aegis.ui.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.SlotManagerActivity;
|
||||
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
import static android.text.TextUtils.isDigitsOnly;
|
||||
|
||||
public class SecurityPreferencesFragment extends PreferencesFragment {
|
||||
private SwitchPreference _encryptionPreference;
|
||||
private SwitchPreference _biometricsPreference;
|
||||
private Preference _autoLockPreference;
|
||||
private Preference _setPasswordPreference;
|
||||
private Preference _slotsPreference;
|
||||
private Preference _passwordReminderPreference;
|
||||
private SwitchPreferenceCompat _pinKeyboardPreference;
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
addPreferencesFromResource(R.xml.preferences_security);
|
||||
|
||||
Preference tapToRevealPreference = findPreference("pref_tap_to_reveal");
|
||||
tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference screenPreference = findPreference("pref_secure_screen");
|
||||
screenPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
Window window = getActivity().getWindow();
|
||||
if ((boolean) newValue) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference tapToRevealTimePreference = findPreference("pref_tap_to_reveal_time");
|
||||
tapToRevealTimePreference.setSummary(getPreferences().getTapToRevealTime() + " seconds");
|
||||
tapToRevealTimePreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showNumberPickerDialog(getActivity(), number -> {
|
||||
getPreferences().setTapToRevealTime(number);
|
||||
tapToRevealTimePreference.setSummary(number + " seconds");
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
_encryptionPreference = findPreference("pref_encryption");
|
||||
_encryptionPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if (!getVault().isEncryptionEnabled()) {
|
||||
Dialogs.showSetPasswordDialog(getActivity(), new EnableEncryptionListener());
|
||||
} else {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.disable_encryption)
|
||||
.setMessage(getText(R.string.disable_encryption_description))
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
try {
|
||||
getVault().disableEncryption();
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.disable_encryption_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// clear the KeyStore
|
||||
try {
|
||||
KeyStoreHandle handle = new KeyStoreHandle();
|
||||
handle.clear();
|
||||
} catch (KeyStoreHandleException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
getActivity().stopService(new Intent(getActivity(), NotificationService.class));
|
||||
updateEncryptionPreferences();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
_biometricsPreference = findPreference("pref_biometrics");
|
||||
_biometricsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
VaultFileCredentials creds = getVault().getCredentials();
|
||||
SlotList slots = creds.getSlots();
|
||||
|
||||
if (!slots.has(BiometricSlot.class)) {
|
||||
if (BiometricsHelper.isAvailable(getContext())) {
|
||||
BiometricSlotInitializer initializer = new BiometricSlotInitializer(SecurityPreferencesFragment.this, new RegisterBiometricsListener());
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.set_up_biometric))
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.build();
|
||||
initializer.authenticate(info);
|
||||
}
|
||||
} else {
|
||||
// remove the biometric slot
|
||||
BiometricSlot slot = slots.find(BiometricSlot.class);
|
||||
slots.remove(slot);
|
||||
getVault().setCredentials(creds);
|
||||
|
||||
// remove the KeyStore key
|
||||
try {
|
||||
KeyStoreHandle handle = new KeyStoreHandle();
|
||||
handle.deleteKey(slot.getUUID().toString());
|
||||
} catch (KeyStoreHandleException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
saveVault();
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
_setPasswordPreference = findPreference("pref_password");
|
||||
_setPasswordPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showSetPasswordDialog(getActivity(), new SetPasswordListener());
|
||||
return false;
|
||||
});
|
||||
|
||||
_slotsPreference = findPreference("pref_slots");
|
||||
_slotsPreference.setOnPreferenceClickListener(preference -> {
|
||||
Intent intent = new Intent(getActivity(), SlotManagerActivity.class);
|
||||
intent.putExtra("creds", getVault().getCredentials());
|
||||
startActivityForResult(intent, CODE_SLOTS);
|
||||
return true;
|
||||
});
|
||||
|
||||
_pinKeyboardPreference = findPreference("pref_pin_keyboard");
|
||||
_pinKeyboardPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if (!(boolean) newValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Dialogs.showPasswordInputDialog(getActivity(), R.string.set_password_confirm, R.string.pin_keyboard_description, password -> {
|
||||
if (isDigitsOnly(new String(password))) {
|
||||
List<PasswordSlot> slots = getVault().getCredentials().getSlots().findAll(PasswordSlot.class);
|
||||
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
|
||||
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(getActivity(), new PasswordConfirmationListener());
|
||||
task.execute(getLifecycle(), params);
|
||||
} else {
|
||||
_pinKeyboardPreference.setChecked(false);
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.pin_keyboard_error)
|
||||
.setMessage(R.string.pin_keyboard_error_description)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create());
|
||||
}
|
||||
}, dialog -> {
|
||||
_pinKeyboardPreference.setChecked(false);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
_autoLockPreference = findPreference("pref_auto_lock");
|
||||
_autoLockPreference.setSummary(getAutoLockSummary());
|
||||
_autoLockPreference.setOnPreferenceClickListener((preference) -> {
|
||||
final int[] items = Preferences.AUTO_LOCK_SETTINGS;
|
||||
final String[] textItems = getResources().getStringArray(R.array.pref_auto_lock_types);
|
||||
final boolean[] checkedItems = new boolean[items.length];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
checkedItems[i] = getPreferences().isAutoLockTypeEnabled(items[i]);
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.pref_auto_lock_prompt)
|
||||
.setMultiChoiceItems(textItems, checkedItems, (dialog, index, isChecked) -> checkedItems[index] = isChecked)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int autoLock = Preferences.AUTO_LOCK_OFF;
|
||||
for (int i = 0; i < checkedItems.length; i++) {
|
||||
if (checkedItems[i]) {
|
||||
autoLock |= items[i];
|
||||
}
|
||||
}
|
||||
|
||||
getPreferences().setAutoLockMask(autoLock);
|
||||
_autoLockPreference.setSummary(getAutoLockSummary());
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
Dialogs.showSecureDialog(builder.create());
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
_passwordReminderPreference = findPreference("pref_password_reminder");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (data != null && requestCode == CODE_SLOTS) {
|
||||
onSlotManagerResult(resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void onSlotManagerResult(int resultCode, Intent data) {
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
|
||||
getVault().setCredentials(creds);
|
||||
saveVault();
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
|
||||
private void updateEncryptionPreferences() {
|
||||
boolean encrypted = getVault().isEncryptionEnabled();
|
||||
_encryptionPreference.setChecked(encrypted, true);
|
||||
_setPasswordPreference.setVisible(encrypted);
|
||||
_biometricsPreference.setVisible(encrypted);
|
||||
_slotsPreference.setEnabled(encrypted);
|
||||
_autoLockPreference.setVisible(encrypted);
|
||||
_pinKeyboardPreference.setVisible(encrypted);
|
||||
|
||||
if (encrypted) {
|
||||
SlotList slots = getVault().getCredentials().getSlots();
|
||||
boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1;
|
||||
boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1;
|
||||
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiBio;
|
||||
boolean canUseBio = BiometricsHelper.isAvailable(getContext());
|
||||
_setPasswordPreference.setEnabled(!multiPassword);
|
||||
_biometricsPreference.setEnabled(canUseBio && !multiBio);
|
||||
_biometricsPreference.setChecked(slots.has(BiometricSlot.class), true);
|
||||
_slotsPreference.setVisible(showSlots);
|
||||
_passwordReminderPreference.setVisible(slots.has(BiometricSlot.class));
|
||||
} else {
|
||||
_setPasswordPreference.setEnabled(false);
|
||||
_biometricsPreference.setEnabled(false);
|
||||
_biometricsPreference.setChecked(false, true);
|
||||
_slotsPreference.setVisible(false);
|
||||
_passwordReminderPreference.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private String getAutoLockSummary() {
|
||||
final int[] settings = Preferences.AUTO_LOCK_SETTINGS;
|
||||
final String[] descriptions = getResources().getStringArray(R.array.pref_auto_lock_types);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < settings.length; i++) {
|
||||
if (getPreferences().isAutoLockTypeEnabled(settings[i])) {
|
||||
if (builder.length() != 0) {
|
||||
builder.append(", ");
|
||||
}
|
||||
|
||||
builder.append(descriptions[i].toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.length() == 0) {
|
||||
return getString(R.string.pref_auto_lock_summary_disabled);
|
||||
}
|
||||
|
||||
return getString(R.string.pref_auto_lock_summary, builder.toString());
|
||||
}
|
||||
|
||||
private class SetPasswordListener implements Dialogs.SlotListener {
|
||||
@Override
|
||||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
VaultFileCredentials creds = getVault().getCredentials();
|
||||
SlotList slots = creds.getSlots();
|
||||
|
||||
try {
|
||||
// encrypt the master key for this slot
|
||||
slot.setKey(creds.getKey(), cipher);
|
||||
|
||||
// remove the old master password slot
|
||||
PasswordSlot oldSlot = creds.getSlots().find(PasswordSlot.class);
|
||||
if (oldSlot != null) {
|
||||
slots.remove(oldSlot);
|
||||
}
|
||||
|
||||
// add the new master password slot
|
||||
slots.add(slot);
|
||||
} catch (SlotException e) {
|
||||
onException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
getVault().setCredentials(creds);
|
||||
saveVault();
|
||||
|
||||
if (getPreferences().isPinKeyboardEnabled()) {
|
||||
_pinKeyboardPreference.setChecked(false);
|
||||
Toast.makeText(getContext(), R.string.pin_keyboard_disabled, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
e.printStackTrace();
|
||||
updateEncryptionPreferences();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
|
||||
}
|
||||
}
|
||||
|
||||
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
|
||||
@Override
|
||||
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
|
||||
VaultFileCredentials creds = getVault().getCredentials();
|
||||
try {
|
||||
slot.setKey(creds.getKey(), cipher);
|
||||
} catch (SlotException e) {
|
||||
e.printStackTrace();
|
||||
onSlotInitializationFailed(0, e.toString());
|
||||
return;
|
||||
}
|
||||
creds.getSlots().add(slot);
|
||||
getVault().setCredentials(creds);
|
||||
|
||||
saveVault();
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class EnableEncryptionListener implements Dialogs.SlotListener {
|
||||
@Override
|
||||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
VaultFileCredentials creds = new VaultFileCredentials();
|
||||
|
||||
try {
|
||||
slot.setKey(creds.getKey(), cipher);
|
||||
creds.getSlots().add(slot);
|
||||
getVault().enableEncryption(creds);
|
||||
} catch (VaultManagerException | SlotException e) {
|
||||
onException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
getActivity().startService(new Intent(getActivity(), NotificationService.class));
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
|
||||
}
|
||||
}
|
||||
|
||||
private class PasswordConfirmationListener implements PasswordSlotDecryptTask.Callback {
|
||||
@Override
|
||||
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
|
||||
if (result != null) {
|
||||
_pinKeyboardPreference.setChecked(true);
|
||||
} else {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.pin_keyboard_error)
|
||||
.setMessage(R.string.invalid_password)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create());
|
||||
_pinKeyboardPreference.setChecked(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@@ -9,7 +10,9 @@ import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
@GlideModule
|
||||
@@ -17,5 +20,8 @@ public class AegisGlideModule extends AppGlideModule {
|
||||
@Override
|
||||
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
|
||||
registry.prepend(VaultEntry.class, ByteBuffer.class, new IconLoader.Factory());
|
||||
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
|
||||
.append(InputStream.class, SVG.class, new SvgDecoder())
|
||||
.append(ByteBuffer.class, SVG.class, new SvgBytesDecoder());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.Option;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
@@ -14,6 +16,8 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
|
||||
public static final Option<IconType> ICON_TYPE = Option.memory("ICON_TYPE", IconType.INVALID);
|
||||
|
||||
@Override
|
||||
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntry model, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model));
|
||||
@@ -25,7 +29,7 @@ public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
|
||||
}
|
||||
|
||||
public static class Fetcher implements DataFetcher<ByteBuffer> {
|
||||
private VaultEntry _model;
|
||||
private final VaultEntry _model;
|
||||
|
||||
private Fetcher(VaultEntry model) {
|
||||
_model = model;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class SvgBytesDecoder implements ResourceDecoder<ByteBuffer, SVG> {
|
||||
private SvgDecoder _decoder = new SvgDecoder();
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException {
|
||||
try (ByteArrayInputStream inStream = new ByteArrayInputStream(source.array())) {
|
||||
return _decoder.handles(inStream, options);
|
||||
}
|
||||
}
|
||||
|
||||
public Resource<SVG> decode(@NonNull ByteBuffer source, int width, int height, @NonNull Options options) throws IOException {
|
||||
try (ByteArrayInputStream inStream = new ByteArrayInputStream(source.array())) {
|
||||
return _decoder.decode(inStream, width, height, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.SimpleResource;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
import com.caverock.androidsvg.SVGParseException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static com.bumptech.glide.request.target.Target.SIZE_ORIGINAL;
|
||||
|
||||
// source: https://github.com/bumptech/glide/tree/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg
|
||||
|
||||
/** Decodes an SVG internal representation from an {@link InputStream}. */
|
||||
public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
|
||||
return options.get(IconLoader.ICON_TYPE) == IconType.SVG;
|
||||
}
|
||||
|
||||
public Resource<SVG> decode(
|
||||
@NonNull InputStream source, int width, int height, @NonNull Options options)
|
||||
throws IOException {
|
||||
try {
|
||||
SVG svg = SVG.getFromInputStream(source);
|
||||
if (width != SIZE_ORIGINAL) {
|
||||
svg.setDocumentWidth(width);
|
||||
}
|
||||
if (height != SIZE_ORIGINAL) {
|
||||
svg.setDocumentHeight(height);
|
||||
}
|
||||
return new SimpleResource<>(svg);
|
||||
} catch (SVGParseException ex) {
|
||||
throw new IOException("Cannot load SVG from stream", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import android.graphics.Picture;
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.SimpleResource;
|
||||
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
|
||||
// source: https://github.com/bumptech/glide/tree/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg
|
||||
|
||||
/**
|
||||
* Convert the {@link SVG}'s internal representation to an Android-compatible one ({@link Picture}).
|
||||
*/
|
||||
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
|
||||
@Nullable
|
||||
@Override
|
||||
public Resource<PictureDrawable> transcode(
|
||||
@NonNull Resource<SVG> toTranscode, @NonNull Options options) {
|
||||
SVG svg = toTranscode.get();
|
||||
Picture picture = svg.renderToPicture();
|
||||
PictureDrawable drawable = new PictureDrawable(picture);
|
||||
return new SimpleResource<>(drawable);
|
||||
}
|
||||
}
|
||||
@@ -3,32 +3,19 @@ package com.beemdevelopment.aegis.ui.models;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ImportEntry implements Serializable {
|
||||
private UUID _uuid;
|
||||
private String _name;
|
||||
private String _issuer;
|
||||
private final VaultEntry _entry;
|
||||
|
||||
private transient Listener _listener;
|
||||
private boolean _isChecked = true;
|
||||
|
||||
public ImportEntry(VaultEntry entry) {
|
||||
_uuid = entry.getUUID();
|
||||
_name = entry.getName();
|
||||
_issuer = entry.getIssuer();
|
||||
_entry = entry;
|
||||
}
|
||||
|
||||
public UUID getUUID() {
|
||||
return _uuid;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
public VaultEntry getEntry() {
|
||||
return _entry;
|
||||
}
|
||||
|
||||
public void setOnCheckedChangedListener(Listener listener) {
|
||||
|
||||
@@ -23,7 +23,7 @@ import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
|
||||
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.beemdevelopment.aegis.ui.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* ImportFileTask reads an SAF file from a background thread and
|
||||
* writes it to a temporary file in the cache directory.
|
||||
*/
|
||||
public class ImportFileTask extends ProgressDialogTask<Uri, ImportFileTask.Result> {
|
||||
private final Callback _cb;
|
||||
|
||||
public ImportFileTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.reading_file));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result doInBackground(Uri... uris) {
|
||||
Context context = getDialog().getContext();
|
||||
|
||||
try (InputStream inStream = context.getContentResolver().openInputStream(uris[0])) {
|
||||
File tempFile = File.createTempFile("import-", "", context.getCacheDir());
|
||||
try (FileOutputStream outStream = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
}
|
||||
|
||||
return new Result(tempFile, null);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return new Result(null, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Result result) {
|
||||
super.onPostExecute(result);
|
||||
_cb.onTaskFinished(result);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(Result result);
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private final File _file;
|
||||
private final Exception _e;
|
||||
|
||||
public Result(File file, Exception e) {
|
||||
_file = file;
|
||||
_e = e;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return _file;
|
||||
}
|
||||
|
||||
public Exception getException() {
|
||||
return _e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.beemdevelopment.aegis.ui.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconPackException;
|
||||
import com.beemdevelopment.aegis.icons.IconPackManager;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class ImportIconPackTask extends ProgressDialogTask<ImportIconPackTask.Params, ImportIconPackTask.Result> {
|
||||
private final ImportIconPackTask.Callback _cb;
|
||||
|
||||
public ImportIconPackTask(Context context, ImportIconPackTask.Callback cb) {
|
||||
super(context, context.getString(R.string.importing_icon_pack));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImportIconPackTask.Result doInBackground(ImportIconPackTask.Params... params) {
|
||||
Context context = getDialog().getContext();
|
||||
ImportIconPackTask.Params param = params[0];
|
||||
|
||||
File tempFile = null;
|
||||
try {
|
||||
tempFile = File.createTempFile("icon-pack-", "", context.getCacheDir());
|
||||
try (InputStream inStream = context.getContentResolver().openInputStream(param.getUri());
|
||||
FileOutputStream outStream = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
}
|
||||
|
||||
IconPack pack = param.getManager().importPack(tempFile);
|
||||
return new Result(pack, null);
|
||||
} catch (IOException | IconPackException e) {
|
||||
e.printStackTrace();
|
||||
return new ImportIconPackTask.Result(null, e);
|
||||
} finally {
|
||||
if (tempFile != null) {
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(ImportIconPackTask.Result result) {
|
||||
super.onPostExecute(result);
|
||||
_cb.onTaskFinished(result);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(ImportIconPackTask.Result result);
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
private final IconPackManager _manager;
|
||||
private final Uri _uri;
|
||||
|
||||
public Params(IconPackManager manager, Uri uri) {
|
||||
_manager = manager;
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
public IconPackManager getManager() {
|
||||
return _manager;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return _uri;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private final IconPack _pack;
|
||||
private final Exception _e;
|
||||
|
||||
public Result(IconPack pack, Exception e) {
|
||||
_pack = pack;
|
||||
_e = e;
|
||||
}
|
||||
|
||||
public IconPack getIconPack() {
|
||||
return _pack;
|
||||
}
|
||||
|
||||
public Exception getException() {
|
||||
return _e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,13 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
|
||||
setPriority();
|
||||
|
||||
Params params = args[0];
|
||||
for (PasswordSlot slot : params.getSlots()) {
|
||||
return decrypt(params.getSlots(), params.getPassword());
|
||||
}
|
||||
|
||||
public static Result decrypt(List<PasswordSlot> slots, char[] password) {
|
||||
for (PasswordSlot slot : slots) {
|
||||
try {
|
||||
return decryptPasswordSlot(slot, params.getPassword());
|
||||
return decryptPasswordSlot(slot, password);
|
||||
} catch (SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (SlotIntegrityException ignored) {
|
||||
@@ -41,7 +45,7 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
|
||||
return null;
|
||||
}
|
||||
|
||||
private Result decryptPasswordSlot(PasswordSlot slot, char[] password)
|
||||
public static Result decryptPasswordSlot(PasswordSlot slot, char[] password)
|
||||
throws SlotIntegrityException, SlotException {
|
||||
MasterKey masterKey;
|
||||
SecretKey key = slot.deriveKey(password);
|
||||
@@ -56,8 +60,6 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
|
||||
throw e;
|
||||
}
|
||||
|
||||
publishProgress(getDialog().getContext().getString(R.string.unlocking_vault_repair));
|
||||
|
||||
// try to decrypt the password slot with the old key
|
||||
SecretKey oldKey = slot.deriveKey(oldPasswordBytes);
|
||||
masterKey = decryptPasswordSlot(slot, oldKey);
|
||||
@@ -75,7 +77,7 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
|
||||
return new Result(masterKey, slot, repaired);
|
||||
}
|
||||
|
||||
private MasterKey decryptPasswordSlot(PasswordSlot slot, SecretKey key)
|
||||
public static MasterKey decryptPasswordSlot(PasswordSlot slot, SecretKey key)
|
||||
throws SlotException, SlotIntegrityException {
|
||||
Cipher cipher = slot.createDecryptCipher(key);
|
||||
return slot.getKey(cipher);
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.OnLifecycleEvent;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
|
||||
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, String, Result> {
|
||||
private ProgressDialog _dialog;
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
|
||||
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
|
||||
@@ -35,13 +36,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
private VaultEntry _focusedEntry;
|
||||
private int _codeGroupSize;
|
||||
private boolean _showAccountName;
|
||||
private boolean _searchAccountName;
|
||||
private boolean _highlightEntry;
|
||||
private boolean _tempHighlightEntry;
|
||||
private boolean _tapToReveal;
|
||||
private int _tapToRevealTime;
|
||||
private boolean _copyOnTap;
|
||||
private String _groupFilter;
|
||||
private List<String> _groupFilter;
|
||||
private SortCategory _sortCategory;
|
||||
private ViewMode _viewMode;
|
||||
private String _searchFilter;
|
||||
@@ -57,6 +57,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
_entries = new ArrayList<>();
|
||||
_shownEntries = new ArrayList<>();
|
||||
_selectedEntries = new ArrayList<>();
|
||||
_groupFilter = new ArrayList<>();
|
||||
_holders = new ArrayList<>();
|
||||
_dimHandler = new Handler();
|
||||
_view = view;
|
||||
@@ -85,10 +86,6 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
_tapToRevealTime = number;
|
||||
}
|
||||
|
||||
public void setSearchAccountName(boolean searchAccountName) {
|
||||
_searchAccountName = searchAccountName;
|
||||
}
|
||||
|
||||
public void setHighlightEntry(boolean highlightEntry) {
|
||||
_highlightEntry = highlightEntry;
|
||||
}
|
||||
@@ -212,12 +209,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
String issuer = entry.getIssuer().toLowerCase();
|
||||
String name = entry.getName().toLowerCase();
|
||||
|
||||
if (_groupFilter != null) {
|
||||
if (group == null && _groupFilter.equals(_view.getContext().getString(R.string.filter_ungrouped))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (group == null || !group.equals(_groupFilter)) {
|
||||
if (!_groupFilter.isEmpty()) {
|
||||
if (group == null || !_groupFilter.contains(group)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -226,7 +219,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
return false;
|
||||
}
|
||||
|
||||
return !issuer.contains(_searchFilter) && !(_searchAccountName && name.contains(_searchFilter));
|
||||
return !issuer.contains(_searchFilter) && !name.contains(_searchFilter);
|
||||
}
|
||||
|
||||
public void refresh(boolean hard) {
|
||||
@@ -239,11 +232,16 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
}
|
||||
}
|
||||
|
||||
public void setGroupFilter(String group, boolean apply) {
|
||||
if (_groupFilter != null && _groupFilter.equals(group)) {
|
||||
public void setGroupFilter(List<String> groups, boolean apply) {
|
||||
if (groups == null) {
|
||||
groups = new ArrayList<>();
|
||||
}
|
||||
|
||||
if (_groupFilter.equals(groups)) {
|
||||
return;
|
||||
}
|
||||
_groupFilter = group;
|
||||
|
||||
_groupFilter = groups;
|
||||
if (apply) {
|
||||
updateShownEntries();
|
||||
checkPeriodUniformity();
|
||||
@@ -291,6 +289,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
_viewMode = viewMode;
|
||||
}
|
||||
|
||||
public void setGroups(TreeSet<String> groups) {
|
||||
_view.setGroups(groups);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDismiss(int position) {
|
||||
|
||||
@@ -299,7 +301,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
@Override
|
||||
public void onItemDrop(int position) {
|
||||
// moving entries is not allowed when a filter is applied
|
||||
if (_groupFilter != null) {
|
||||
if (!_groupFilter.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -309,7 +311,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
@Override
|
||||
public void onItemMove(int firstPosition, int secondPosition) {
|
||||
// moving entries is not allowed when a filter is applied
|
||||
if (_groupFilter != null) {
|
||||
if (!_groupFilter.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -596,7 +598,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
}
|
||||
|
||||
public boolean isDragAndDropAllowed() {
|
||||
return _sortCategory == SortCategory.CUSTOM && _groupFilter == null && _searchFilter == null;
|
||||
return _sortCategory == SortCategory.CUSTOM && _groupFilter.isEmpty() && _searchFilter == null;
|
||||
}
|
||||
|
||||
public boolean isPeriodUniform() {
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
@@ -21,10 +22,10 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.google.zxing.common.StringUtils;
|
||||
|
||||
public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private static final float DEFAULT_ALPHA = 1.0f;
|
||||
@@ -141,9 +142,11 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public void loadIcon(Fragment fragment) {
|
||||
if (_entry.hasIcon()) {
|
||||
IconViewHelper.setLayerType(_profileDrawable, _entry.getIconType());
|
||||
Glide.with(fragment)
|
||||
.asDrawable()
|
||||
.load(_entry)
|
||||
.set(IconLoader.ICON_TYPE, _entry.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_profileDrawable);
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
@@ -10,6 +12,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.animation.LayoutAnimationController;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -24,9 +27,12 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.helpers.MetricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.ListPreloader;
|
||||
@@ -34,11 +40,16 @@ import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private EntryAdapter _adapter;
|
||||
@@ -52,7 +63,10 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private TotpProgressBar _progressBar;
|
||||
private boolean _showProgress;
|
||||
private ViewMode _viewMode;
|
||||
private TreeSet<String> _groups;
|
||||
private LinearLayout _emptyStateView;
|
||||
private Chip _groupChip;
|
||||
private List<String> _groupFilter;
|
||||
|
||||
private UiRefresher _refresher;
|
||||
|
||||
@@ -73,6 +87,8 @@ 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();
|
||||
|
||||
// set up the recycler view
|
||||
_recyclerView = view.findViewById(R.id.rvKeyProfiles);
|
||||
@@ -128,8 +144,9 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
public void setGroupFilter(String group, boolean apply) {
|
||||
_adapter.setGroupFilter(group, apply);
|
||||
public void setGroupFilter(List<String> groups, boolean apply) {
|
||||
_groupFilter = groups;
|
||||
_adapter.setGroupFilter(groups, apply);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
|
||||
if (apply) {
|
||||
@@ -258,10 +275,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_adapter.setShowAccountName(showAccountName);
|
||||
}
|
||||
|
||||
public void setSearchAccountName(boolean searchAccountName) {
|
||||
_adapter.setSearchAccountName(searchAccountName);
|
||||
}
|
||||
|
||||
public void setHighlightEntry(boolean highlightEntry) {
|
||||
_adapter.setHighlightEntry(highlightEntry);
|
||||
}
|
||||
@@ -283,33 +296,44 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
int position = _adapter.addEntry(entry);
|
||||
updateEmptyState();
|
||||
|
||||
LinearLayoutManager layoutManager = (LinearLayoutManager) _recyclerView.getLayoutManager();
|
||||
if (focusEntry && position >= 0) {
|
||||
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
_recyclerView.removeOnScrollListener(this);
|
||||
_adapter.setTempHighlightEntry(true);
|
||||
|
||||
final int secondsToFocus = 3;
|
||||
_adapter.focusEntry(entry, secondsToFocus);
|
||||
if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition())
|
||||
|| (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) {
|
||||
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
_recyclerView.removeOnScrollListener(this);
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
tempHighlightEntry(entry);
|
||||
}
|
||||
}
|
||||
};
|
||||
_recyclerView.addOnScrollListener(scrollListener);
|
||||
_recyclerView.setOnTouchListener((v, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
_recyclerView.removeOnScrollListener(scrollListener);
|
||||
_recyclerView.stopScroll();
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
_recyclerView.addOnScrollListener(scrollListener);
|
||||
_recyclerView.setOnTouchListener((v, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
_recyclerView.removeOnScrollListener(scrollListener);
|
||||
_recyclerView.stopScroll();
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
_recyclerView.smoothScrollToPosition(position);
|
||||
return false;
|
||||
});
|
||||
_recyclerView.smoothScrollToPosition(position);
|
||||
} else {
|
||||
tempHighlightEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void tempHighlightEntry(VaultEntry entry) {
|
||||
_adapter.setTempHighlightEntry(true);
|
||||
|
||||
final int secondsToFocus = 3;
|
||||
_adapter.focusEntry(entry, secondsToFocus);
|
||||
}
|
||||
|
||||
public void addEntries(Collection<VaultEntry> entries) {
|
||||
_adapter.addEntries(entries);
|
||||
updateEmptyState();
|
||||
@@ -342,11 +366,62 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_recyclerView.scheduleLayoutAnimation();
|
||||
}
|
||||
|
||||
private void initializeGroupChip() {
|
||||
View view = getLayoutInflater().inflate(R.layout.dialog_select_groups, null);
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(getContext());
|
||||
dialog.setContentView(view);
|
||||
|
||||
ChipGroup chipGroup = view.findViewById(R.id.groupChipGroup);
|
||||
Button clearButton = view.findViewById(R.id.btnClear);
|
||||
clearButton.setOnClickListener(v -> {
|
||||
chipGroup.clearCheck();
|
||||
setGroupFilter(null, true);
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
_groupChip.setOnClickListener(v -> {
|
||||
ColorStateList colorStateList = ContextCompat.getColorStateList(getContext(), R.color.bg_chip_text_color);
|
||||
chipGroup.removeAllViews();
|
||||
|
||||
for (String group : _groups) {
|
||||
Chip chip = new Chip(getContext());
|
||||
chip.setText(group);
|
||||
chip.setCheckable(true);
|
||||
chip.setChecked(_groupFilter != null && _groupFilter.contains(group));
|
||||
chip.setCheckedIconVisible(false);
|
||||
chip.setChipBackgroundColorResource(R.color.bg_chip_color);
|
||||
chip.setTextColor(colorStateList);
|
||||
chip.setOnCheckedChangeListener((group1, checkedId) -> {
|
||||
List<String> groupFilter = chipGroup.getCheckedChipIds().stream()
|
||||
.map(i -> ((Chip) view.findViewById(i)).getText().toString())
|
||||
.collect(Collectors.toList());
|
||||
setGroupFilter(groupFilter, true);
|
||||
|
||||
if (groupFilter.isEmpty()) {
|
||||
_groupChip.setText(R.string.groups);
|
||||
} else {
|
||||
_groupChip.setText(String.format("%s (%d)", getString(R.string.groups), groupFilter.size()));
|
||||
}
|
||||
});
|
||||
|
||||
chipGroup.addView(chip);
|
||||
}
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
private void setShowProgress(boolean showProgress) {
|
||||
_showProgress = showProgress;
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setGroups(TreeSet<String> groups) {
|
||||
_groups = groups;
|
||||
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
private void updateDividerDecoration() {
|
||||
if (_dividerDecoration != null) {
|
||||
_recyclerView.removeItemDecoration(_dividerDecoration);
|
||||
@@ -392,12 +467,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
|
||||
private VerticalSpaceItemDecoration(float dp) {
|
||||
// convert dp to pixels
|
||||
_height = (int) (dp * (getContext().getResources().getDisplayMetrics().densityDpi / 160f));
|
||||
_height = MetricsHelper.convertDpToPixels(getContext(), dp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
|
||||
if (parent.getChildAdapterPosition(view) == 0) {
|
||||
if (parent.getChildAdapterPosition(view) == 0 && (_groups == null || _groups.isEmpty())) {
|
||||
// the first item should also have a top margin
|
||||
outRect.top = _height;
|
||||
}
|
||||
@@ -418,10 +493,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder getPreloadRequestBuilder(@NonNull VaultEntry entry) {
|
||||
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) {
|
||||
return Glide.with(EntryListView.this)
|
||||
.asDrawable()
|
||||
.load(entry)
|
||||
.set(IconLoader.ICON_TYPE, entry.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IconAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
private final Context _context;
|
||||
private final String _issuer;
|
||||
private final Listener _listener;
|
||||
private IconPack _pack;
|
||||
private List<IconPack.Icon> _icons;
|
||||
private final List<CategoryHeader> _categories;
|
||||
private String _query;
|
||||
|
||||
public IconAdapter(@NonNull Context context, String issuer, @NonNull Listener listener) {
|
||||
_context = context;
|
||||
_issuer = issuer;
|
||||
_listener = listener;
|
||||
_icons = new ArrayList<>();
|
||||
_categories = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all icons from the given icon pack into this adapter. Any icons added before this call will be overwritten.
|
||||
*/
|
||||
public void loadIcons(IconPack pack) {
|
||||
_pack = pack;
|
||||
_query = null;
|
||||
_icons = new ArrayList<>(_pack.getIcons());
|
||||
_categories.clear();
|
||||
|
||||
Comparator<IconPack.Icon> iconCategoryComparator = (i1, i2) -> {
|
||||
String c1 = getCategoryString(i1.getCategory());
|
||||
String c2 = getCategoryString(i2.getCategory());
|
||||
return c1.compareTo(c2);
|
||||
};
|
||||
Collections.sort(_icons, iconCategoryComparator.thenComparing(IconPack.Icon::getName));
|
||||
|
||||
long categoryCount = _icons.stream()
|
||||
.map(IconPack.Icon::getCategory)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.count();
|
||||
|
||||
List<IconPack.Icon> suggested = pack.getSuggestedIcons(_issuer);
|
||||
suggested.add(0, new DummyIcon(_context.getString(R.string.icon_custom)));
|
||||
if (suggested.size() > 0) {
|
||||
CategoryHeader category = new CategoryHeader(_context.getString(R.string.suggested));
|
||||
category.setIsCollapsed(false);
|
||||
category.getIcons().addAll(suggested);
|
||||
_categories.add(category);
|
||||
}
|
||||
|
||||
CategoryHeader category = null;
|
||||
for (IconPack.Icon icon : _icons) {
|
||||
String iconCategory = getCategoryString(icon.getCategory());
|
||||
if (category == null || !getCategoryString(category.getCategory()).equals(iconCategory)) {
|
||||
boolean collapsed = !(categoryCount == 0 && category == null);
|
||||
category = new CategoryHeader(iconCategory);
|
||||
category.setIsCollapsed(collapsed);
|
||||
_categories.add(category);
|
||||
}
|
||||
|
||||
category.getIcons().add(icon);
|
||||
}
|
||||
|
||||
_icons.addAll(0, suggested);
|
||||
updateCategoryPositions();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setQuery(@Nullable String query) {
|
||||
_query = query;
|
||||
|
||||
if (_query == null) {
|
||||
loadIcons(_pack);
|
||||
} else {
|
||||
_icons = _pack.getIcons().stream()
|
||||
.filter(i -> i.isSuggestedFor(query))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Collections.sort(_icons, Comparator.comparing(IconPack.Icon::getName));
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public IconPack.Icon getIconAt(int position) {
|
||||
if (isQueryActive()) {
|
||||
return _icons.get(position);
|
||||
}
|
||||
|
||||
position = translateIconPosition(position);
|
||||
return _icons.get(position);
|
||||
}
|
||||
|
||||
public CategoryHeader getCategoryAt(int position) {
|
||||
return _categories.stream()
|
||||
.filter(c -> c.getPosition() == position)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private String getCategoryString(String category) {
|
||||
return category == null ? _context.getString(R.string.uncategorized) : category;
|
||||
}
|
||||
|
||||
private boolean isCategoryPosition(int position) {
|
||||
if (isQueryActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getCategoryAt(position) != null;
|
||||
}
|
||||
|
||||
private int translateIconPosition(int position) {
|
||||
int offset = 0;
|
||||
for (CategoryHeader category : _categories) {
|
||||
if (category.getPosition() < position) {
|
||||
offset++;
|
||||
if (category.isCollapsed()) {
|
||||
offset -= category.getIcons().size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return position - offset;
|
||||
}
|
||||
|
||||
private void updateCategoryPositions() {
|
||||
int i = 0;
|
||||
for (CategoryHeader category : _categories) {
|
||||
category.setPosition(i);
|
||||
|
||||
int icons = 0;
|
||||
if (!category.isCollapsed()) {
|
||||
icons = category.getIcons().size();
|
||||
}
|
||||
|
||||
i += 1 + icons;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
|
||||
return viewType == R.layout.card_icon ? new IconHolder(view) : new IconCategoryHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (!isCategoryPosition(position)) {
|
||||
IconHolder iconHolder = (IconHolder) holder;
|
||||
IconPack.Icon icon = getIconAt(position);
|
||||
iconHolder.setData(icon);
|
||||
iconHolder.loadIcon(_context);
|
||||
iconHolder.itemView.setOnClickListener(v -> {
|
||||
if (icon instanceof DummyIcon) {
|
||||
_listener.onCustomSelected();
|
||||
} else {
|
||||
_listener.onIconSelected(icon);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
IconCategoryHolder categoryHolder = (IconCategoryHolder) holder;
|
||||
CategoryHeader category = getCategoryAt(position);
|
||||
categoryHolder.setData(category);
|
||||
categoryHolder.itemView.setOnClickListener(v -> {
|
||||
boolean collapsed = !category.isCollapsed();
|
||||
categoryHolder.setIsCollapsed(collapsed);
|
||||
category.setIsCollapsed(collapsed);
|
||||
|
||||
int startPosition = category.getPosition() + 1;
|
||||
if (category.isCollapsed()) {
|
||||
notifyItemRangeRemoved(startPosition, category.getIcons().size());
|
||||
} else {
|
||||
notifyItemRangeInserted(startPosition, category.getIcons().size());
|
||||
}
|
||||
|
||||
updateCategoryPositions();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (isQueryActive()) {
|
||||
return _icons.size();
|
||||
}
|
||||
|
||||
int items = _categories.stream()
|
||||
.filter(c -> !c.isCollapsed())
|
||||
.mapToInt(c -> c.getIcons().size())
|
||||
.sum();
|
||||
|
||||
return items + _categories.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (isCategoryPosition(position)) {
|
||||
return R.layout.card_icon_category;
|
||||
}
|
||||
|
||||
return R.layout.card_icon;
|
||||
}
|
||||
|
||||
private boolean isQueryActive() {
|
||||
return _query != null;
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onIconSelected(IconPack.Icon icon);
|
||||
void onCustomSelected();
|
||||
}
|
||||
|
||||
public static class DummyIcon extends IconPack.Icon {
|
||||
private final String _name;
|
||||
|
||||
protected DummyIcon(String name) {
|
||||
super(null, null, null);
|
||||
_name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IconType getIconType() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CategoryHeader {
|
||||
private final String _category;
|
||||
private int _position = -1;
|
||||
private final List<IconPack.Icon> _icons;
|
||||
private boolean _collapsed = true;
|
||||
|
||||
public CategoryHeader(String category) {
|
||||
_category = category;
|
||||
_icons = new ArrayList<>();
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return _category;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return _position;
|
||||
}
|
||||
|
||||
public void setPosition(int position) {
|
||||
_position = position;
|
||||
}
|
||||
|
||||
public List<IconPack.Icon> getIcons() {
|
||||
return _icons;
|
||||
}
|
||||
|
||||
public boolean isCollapsed() {
|
||||
return _collapsed;
|
||||
}
|
||||
|
||||
public void setIsCollapsed(boolean collapsed) {
|
||||
_collapsed = collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class IconCategoryHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView _textView;
|
||||
private final ImageView _imgView;
|
||||
|
||||
public IconCategoryHolder(final View view) {
|
||||
super(view);
|
||||
_textView = view.findViewById(R.id.icon_category);
|
||||
_imgView = view.findViewById(R.id.icon_category_indicator);
|
||||
}
|
||||
|
||||
public void setData(IconAdapter.CategoryHeader header) {
|
||||
_textView.setText(header.getCategory());
|
||||
_imgView.setRotation(getRotation(header.isCollapsed()));
|
||||
}
|
||||
|
||||
public void setIsCollapsed(boolean collapsed) {
|
||||
_imgView.animate()
|
||||
.setDuration(200)
|
||||
.rotation(getRotation(collapsed))
|
||||
.start();
|
||||
}
|
||||
|
||||
private static int getRotation(boolean collapsed) {
|
||||
return collapsed ? 90 : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class IconHolder extends RecyclerView.ViewHolder {
|
||||
private File _iconFile;
|
||||
private IconType _iconType;
|
||||
private boolean _isCustom;
|
||||
|
||||
private final ImageView _imageView;
|
||||
private final TextView _textView;
|
||||
|
||||
public IconHolder(final View view) {
|
||||
super(view);
|
||||
_imageView = view.findViewById(R.id.icon);
|
||||
_textView = view.findViewById(R.id.icon_name);
|
||||
}
|
||||
|
||||
public void setData(IconPack.Icon icon) {
|
||||
_iconFile = icon.getFile();
|
||||
_iconType = icon.getIconType();
|
||||
_isCustom = icon instanceof IconAdapter.DummyIcon;
|
||||
_textView.setText(icon.getName());
|
||||
}
|
||||
|
||||
public void loadIcon(Context context) {
|
||||
if (_isCustom) {
|
||||
int tint = ThemeHelper.getThemeColor(R.attr.iconColorPrimary, context.getTheme());
|
||||
_imageView.setColorFilter(tint);
|
||||
_imageView.setImageResource(R.drawable.ic_plus_black_24dp);
|
||||
} else {
|
||||
_imageView.setImageTintList(null);
|
||||
IconViewHelper.setLayerType(_imageView, _iconType);
|
||||
Glide.with(context)
|
||||
.asDrawable()
|
||||
.load(_iconFile)
|
||||
.set(IconLoader.ICON_TYPE, _iconType)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_imageView);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class IconPackAdapter extends RecyclerView.Adapter<IconPackHolder> {
|
||||
private IconPackAdapter.Listener _listener;
|
||||
private List<IconPack> _iconPacks;
|
||||
|
||||
public IconPackAdapter(IconPackAdapter.Listener listener) {
|
||||
_listener = listener;
|
||||
_iconPacks = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void addIconPack(IconPack pack) {
|
||||
_iconPacks.add(pack);
|
||||
|
||||
int position = getItemCount() - 1;
|
||||
if (position == 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeIconPack(IconPack pack) {
|
||||
int position = _iconPacks.indexOf(pack);
|
||||
_iconPacks.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public IconPackHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_icon_pack, parent, false);
|
||||
return new IconPackHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull IconPackHolder holder, int position) {
|
||||
holder.setData(_iconPacks.get(position));
|
||||
holder.setOnDeleteClickListener(v -> {
|
||||
int position12 = holder.getAdapterPosition();
|
||||
_listener.onRemoveIconPack(_iconPacks.get(position12));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return _iconPacks.size();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onRemoveIconPack(IconPack pack);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
|
||||
public class IconPackHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView _iconPackName;
|
||||
private final TextView _iconPackInfo;
|
||||
private final ImageView _buttonDelete;
|
||||
|
||||
public IconPackHolder(final View view) {
|
||||
super(view);
|
||||
_iconPackName = view.findViewById(R.id.text_icon_pack_name);
|
||||
_iconPackInfo = view.findViewById(R.id.text_icon_pack_info);
|
||||
_buttonDelete = view.findViewById(R.id.button_delete);
|
||||
}
|
||||
|
||||
public void setData(IconPack pack) {
|
||||
_iconPackName.setText(String.format("%s (v%d)", pack.getName(), pack.getVersion()));
|
||||
_iconPackInfo.setText(itemView.getResources().getQuantityString(R.plurals.icon_pack_info, pack.getIcons().size(), pack.getIcons().size()));
|
||||
}
|
||||
|
||||
public void setOnDeleteClickListener(View.OnClickListener listener) {
|
||||
_buttonDelete.setOnClickListener(listener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
// source (slightly modified for Aegis): https://github.com/chiuki/android-recyclerview/blob/745dc88/app/src/main/java/com/sqisland/android/recyclerview/AutofitRecyclerView.java
|
||||
public class IconRecyclerView extends RecyclerView {
|
||||
private GridLayoutManager _manager;
|
||||
private int _columnWidth = -1;
|
||||
private int _spanCount;
|
||||
|
||||
public IconRecyclerView(@NonNull Context context) {
|
||||
super(context);
|
||||
init(context, null);
|
||||
}
|
||||
|
||||
public IconRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public IconRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private void init(Context context, AttributeSet attrs) {
|
||||
if (attrs != null) {
|
||||
int[] attrsArray = {
|
||||
android.R.attr.columnWidth
|
||||
};
|
||||
TypedArray array = context.obtainStyledAttributes(attrs, attrsArray);
|
||||
_columnWidth = array.getDimensionPixelSize(0, -1);
|
||||
array.recycle();
|
||||
}
|
||||
|
||||
_manager = new GridLayoutManager(getContext(), 1);
|
||||
setLayoutManager(_manager);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthSpec, int heightSpec) {
|
||||
super.onMeasure(widthSpec, heightSpec);
|
||||
if (_columnWidth > 0) {
|
||||
_spanCount = Math.max(1, getMeasuredWidth() / _columnWidth);
|
||||
_manager.setSpanCount(_spanCount);
|
||||
}
|
||||
}
|
||||
|
||||
public GridLayoutManager getGridLayoutManager() {
|
||||
return _manager;
|
||||
}
|
||||
|
||||
public int getSpanCount() {
|
||||
return _spanCount;
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,15 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class ImportEntriesAdapter extends RecyclerView.Adapter<ImportEntryHolder> {
|
||||
private List<ImportEntry> _entries;
|
||||
|
||||
@@ -47,7 +47,7 @@ public class ImportEntriesAdapter extends RecyclerView.Adapter<ImportEntryHolder
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull ImportEntryHolder holder) {
|
||||
holder.getEntry().setOnCheckedChangedListener(null);
|
||||
holder.getData().setOnCheckedChangedListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,7 +15,7 @@ public class ImportEntryHolder extends RecyclerView.ViewHolder implements Import
|
||||
private TextView _accountName;
|
||||
private CheckBox _checkbox;
|
||||
|
||||
private ImportEntry _entry;
|
||||
private ImportEntry _data;
|
||||
|
||||
public ImportEntryHolder(final View view) {
|
||||
super(view);
|
||||
@@ -23,20 +23,20 @@ public class ImportEntryHolder extends RecyclerView.ViewHolder implements Import
|
||||
_issuer = view.findViewById(R.id.profile_issuer);
|
||||
_accountName = view.findViewById(R.id.profile_account_name);
|
||||
_checkbox = view.findViewById(R.id.checkbox_import_entry);
|
||||
view.setOnClickListener(v -> _entry.setIsChecked(!_entry.isChecked()));
|
||||
view.setOnClickListener(v -> _data.setIsChecked(!_data.isChecked()));
|
||||
}
|
||||
|
||||
public void setData(ImportEntry entry) {
|
||||
_entry = entry;
|
||||
public void setData(ImportEntry data) {
|
||||
_data = data;
|
||||
|
||||
Context context = itemView.getContext();
|
||||
_issuer.setText(!entry.getIssuer().isEmpty() ? entry.getIssuer() : context.getString(R.string.unknown_issuer));
|
||||
_accountName.setText(!entry.getName().isEmpty() ? entry.getName() : context.getString(R.string.unknown_account_name));
|
||||
_checkbox.setChecked(entry.isChecked());
|
||||
_issuer.setText(!_data.getEntry().getIssuer().isEmpty() ? _data.getEntry().getIssuer() : context.getString(R.string.unknown_issuer));
|
||||
_accountName.setText(!_data.getEntry().getName().isEmpty() ? _data.getEntry().getName() : context.getString(R.string.unknown_account_name));
|
||||
_checkbox.setChecked(_data.isChecked());
|
||||
}
|
||||
|
||||
public ImportEntry getEntry() {
|
||||
return _entry;
|
||||
public ImportEntry getData() {
|
||||
return _data;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.annotation.RequiresApi;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
|
||||
public class TotpProgressBar extends ProgressBar {
|
||||
private int _period = 30;
|
||||
private int _period = TotpInfo.DEFAULT_PERIOD;
|
||||
private Handler _handler;
|
||||
private float _animDurationScale;
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
|
||||
import org.json.JSONArray;
|
||||
@@ -9,7 +7,7 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class Vault {
|
||||
private static final int VERSION = 1;
|
||||
private static final int VERSION = 2;
|
||||
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||
|
||||
public JSONObject toJson() {
|
||||
@@ -34,7 +32,7 @@ public class Vault {
|
||||
|
||||
try {
|
||||
int ver = obj.getInt("version");
|
||||
if (ver != VERSION) {
|
||||
if (ver > VERSION) {
|
||||
throw new VaultException("Unsupported version");
|
||||
}
|
||||
|
||||
@@ -43,7 +41,7 @@ public class Vault {
|
||||
VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(i));
|
||||
entries.add(entry);
|
||||
}
|
||||
} catch (EncodingException | OtpInfoException | JSONException e) {
|
||||
} catch (VaultEntryException | JSONException e) {
|
||||
throw new VaultException(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
|
||||
import org.json.JSONException;
|
||||
@@ -21,6 +23,7 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
private String _group;
|
||||
private OtpInfo _info;
|
||||
private byte[] _icon;
|
||||
private IconType _iconType = IconType.INVALID;
|
||||
|
||||
private VaultEntry(UUID uuid, OtpInfo info) {
|
||||
super(uuid);
|
||||
@@ -53,12 +56,13 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try {
|
||||
obj.put("type", _info.getType());
|
||||
obj.put("type", _info.getTypeId());
|
||||
obj.put("uuid", getUUID().toString());
|
||||
obj.put("name", _name);
|
||||
obj.put("issuer", _issuer);
|
||||
obj.put("group", _group);
|
||||
obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon));
|
||||
obj.put("icon_mime", _icon == null ? null : _iconType.toMimeType());
|
||||
obj.put("info", _info.toJson());
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -67,27 +71,39 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static VaultEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, EncodingException {
|
||||
// if there is no uuid, generate a new one
|
||||
UUID uuid;
|
||||
if (!obj.has("uuid")) {
|
||||
uuid = UUID.randomUUID();
|
||||
} else {
|
||||
uuid = UUID.fromString(obj.getString("uuid"));
|
||||
public static VaultEntry fromJson(JSONObject obj) throws VaultEntryException {
|
||||
try {
|
||||
// if there is no uuid, generate a new one
|
||||
UUID uuid;
|
||||
if (!obj.has("uuid")) {
|
||||
uuid = UUID.randomUUID();
|
||||
} else {
|
||||
uuid = UUID.fromString(obj.getString("uuid"));
|
||||
}
|
||||
|
||||
OtpInfo info = OtpInfo.fromJson(obj.getString("type"), obj.getJSONObject("info"));
|
||||
VaultEntry entry = new VaultEntry(uuid, info);
|
||||
entry.setName(obj.getString("name"));
|
||||
entry.setIssuer(obj.getString("issuer"));
|
||||
entry.setGroup(obj.optString("group", null));
|
||||
|
||||
Object icon = obj.get("icon");
|
||||
if (icon != JSONObject.NULL) {
|
||||
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));
|
||||
}
|
||||
|
||||
byte[] iconBytes = Base64.decode((String) icon);
|
||||
entry.setIcon(iconBytes, iconType);
|
||||
}
|
||||
|
||||
return entry;
|
||||
} catch (OtpInfoException | JSONException | EncodingException e) {
|
||||
throw new VaultEntryException(e);
|
||||
}
|
||||
|
||||
OtpInfo info = OtpInfo.fromJson(obj.getString("type"), obj.getJSONObject("info"));
|
||||
VaultEntry entry = new VaultEntry(uuid, info);
|
||||
entry.setName(obj.getString("name"));
|
||||
entry.setIssuer(obj.getString("issuer"));
|
||||
entry.setGroup(obj.optString("group", null));
|
||||
|
||||
Object icon = obj.get("icon");
|
||||
if (icon != JSONObject.NULL) {
|
||||
entry.setIcon(Base64.decode((String) icon));
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@@ -106,6 +122,10 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
return _icon;
|
||||
}
|
||||
|
||||
public IconType getIconType() {
|
||||
return _iconType;
|
||||
}
|
||||
|
||||
public OtpInfo getInfo() {
|
||||
return _info;
|
||||
}
|
||||
@@ -126,8 +146,9 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
_info = info;
|
||||
}
|
||||
|
||||
public void setIcon(byte[] icon) {
|
||||
public void setIcon(byte[] icon, IconType iconType) {
|
||||
_icon = icon;
|
||||
_iconType = iconType;
|
||||
}
|
||||
|
||||
public boolean hasIcon() {
|
||||
@@ -154,7 +175,8 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
&& getIssuer().equals(entry.getIssuer())
|
||||
&& Objects.equals(getGroup(), entry.getGroup())
|
||||
&& getInfo().equals(entry.getInfo())
|
||||
&& Arrays.equals(getIcon(), entry.getIcon());
|
||||
&& Arrays.equals(getIcon(), entry.getIcon())
|
||||
&& getIconType().equals(entry.getIconType());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user