Compare commits
107
Commits
v2.2-beta1
...
v3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9f37d30b2 | ||
|
|
4c4acf0cd0 | ||
|
|
3a66851df5 | ||
|
|
09c789b250 | ||
|
|
60c72d48ee | ||
|
|
8001ecb482 | ||
|
|
8912d75870 | ||
|
|
e70499bcff | ||
|
|
4c28bf2a12 | ||
|
|
584b0acb2b | ||
|
|
45ced0de60 | ||
|
|
7ec786231e | ||
|
|
9737c85f86 | ||
|
|
bf7c60d620 | ||
|
|
6fc9cd5a71 | ||
|
|
9bae4d6bbc | ||
|
|
7ce43a0afd | ||
|
|
2b2cac1ada | ||
|
|
2f18907ce1 | ||
|
|
ec01a4a96d | ||
|
|
76f6ebd216 | ||
|
|
f1e14e6645 | ||
|
|
52ecf12576 | ||
|
|
dfd720b406 | ||
|
|
8995626d16 | ||
|
|
cffe15735c | ||
|
|
8512986a6b | ||
|
|
c5b8ee9215 | ||
|
|
2e44a81c69 | ||
|
|
f76d84ef87 | ||
|
|
3d59114230 | ||
|
|
22c9ab7c03 | ||
|
|
f8ad3d16fc | ||
|
|
fbd3bf3ff5 | ||
|
|
8bbbe3611a | ||
|
|
2d0e201060 | ||
|
|
b59350337f | ||
|
|
6d73e5101c | ||
|
|
d16d56c4b0 | ||
|
|
59bae27556 | ||
|
|
fcde086ae3 | ||
|
|
0e2fa929e6 | ||
|
|
8951c19581 | ||
|
|
32e462bdce | ||
|
|
6bd8521661 | ||
|
|
f7bac4331e | ||
|
|
243a52ebed | ||
|
|
f91b6f0466 | ||
|
|
57ec695718 | ||
|
|
224ec2553c | ||
|
|
5acacf63e1 | ||
|
|
bfbb3ef2c4 | ||
|
|
f1c9c6c5fc | ||
|
|
566bcac3e0 | ||
|
|
4d729d1bef | ||
|
|
1acb9db489 | ||
|
|
98bcdc7615 | ||
|
|
7c1a954e4d | ||
|
|
a1d00b47fe | ||
|
|
ca530f229b | ||
|
|
b86bb286e8 | ||
|
|
52abb08201 | ||
|
|
ff233090f8 | ||
|
|
adaae9e6d6 | ||
|
|
3dd70de5df | ||
|
|
da2244f511 | ||
|
|
08c73922cc | ||
|
|
88caafd61c | ||
|
|
45220241aa | ||
|
|
60e93559c3 | ||
|
|
e1f4696115 | ||
|
|
1c86c5fd51 | ||
|
|
92e9e047a7 | ||
|
|
c13d4e7f8d | ||
|
|
d09e81232a | ||
|
|
03f1a0e8ab | ||
|
|
305e157fc5 | ||
|
|
5c86e5c099 | ||
|
|
0760bfc618 | ||
|
|
9414b5c420 | ||
|
|
e7a1058618 | ||
|
|
1a6f85ccb6 | ||
|
|
b84ecf15da | ||
|
|
31b8162ab4 | ||
|
|
7def7eb4f7 | ||
|
|
8ca45d2322 | ||
|
|
1c1dee560c | ||
|
|
ca4a3e2f74 | ||
|
|
37964da4a5 | ||
|
|
dd9c307dea | ||
|
|
c65ecd9c54 | ||
|
|
72511fc02b | ||
|
|
79ade74c0c | ||
|
|
8164e91dd0 | ||
|
|
1ccbe88ce6 | ||
|
|
9b3e7136bd | ||
|
|
9cabd9f309 | ||
|
|
27e56d60b5 | ||
|
|
aac77442bf | ||
|
|
c9cf6729e0 | ||
|
|
b916697391 | ||
|
|
b205438982 | ||
|
|
4f83706f73 | ||
|
|
44bb11e2d8 | ||
|
|
92b31c3eda | ||
|
|
2793b65786 | ||
|
|
5e35cc203f |
@@ -5,36 +5,37 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/wrapper-validation-action@55e685c48d84285a5b0418cd094606e199cca3b6
|
||||
- uses: actions/setup-java@v3
|
||||
uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: 'gradle'
|
||||
- name: Build the app
|
||||
run: ./gradlew build
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: apk
|
||||
path: app/build/outputs/apk/debug/app-debug.apk
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
# This is probably pretty expensive for GitHub, so restrict the repositories that this job runs on
|
||||
if: github.repository == 'beemdevelopment/Aegis' || github.repository == 'alexbakker/Aegis'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: 'gradle'
|
||||
- name: Install HAXM
|
||||
run: brew install --cask intel-haxm
|
||||
- name: Enable KVM group perms
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
- name: Tests
|
||||
uses: reactivecircus/android-emulator-runner@50986b1464923454c95e261820bc626f38490ec0
|
||||
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2
|
||||
with:
|
||||
api-level: 31
|
||||
arch: x86_64
|
||||
@@ -52,8 +53,8 @@ jobs:
|
||||
adb logcat -d > artifacts/logcat.txt
|
||||
cp -r app/build/reports/androidTests/connected/* artifacts/report/
|
||||
if adb shell '[ -e /sdcard/Pictures/screenshots ]'; then adb pull /sdcard/Pictures/screenshots artifacts/; fi
|
||||
# test ! -f tests_failing
|
||||
- uses: actions/upload-artifact@v3
|
||||
test ! -f tests_failing
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: instrumented-test-report
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Exclude paths
|
||||
# The importers are excluded from analysis, because some of the apps Aegis
|
||||
# can import from don't have such great crypto, which will cause false
|
||||
@@ -24,18 +24,18 @@ jobs:
|
||||
run: |
|
||||
find app/src/main/java/com/beemdevelopment/aegis/importers ! \( -name AegisImporter.java -o -name "DatabaseImporter*" \) -type f -exec rm -f {} +
|
||||
sed -i '/Importer.class/d' app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
|
||||
- uses: actions/setup-java@v3
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: 'gradle'
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: java
|
||||
- name: Build
|
||||
run: ./gradlew assembleDebug
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'beemdevelopment/Aegis'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install crowdin-cli
|
||||
run: |
|
||||
wget https://github.com/crowdin/crowdin-cli/releases/download/3.7.2/crowdin-cli.zip
|
||||
|
||||
@@ -125,10 +125,20 @@ documentation](docs/iconpacks.md).
|
||||
[<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)
|
||||
- [delta-aegis-icons](https://github.com/Delta-Icons/aegis-icons)
|
||||
|
||||
Delta version of the unofficial monochrome-styled 2FA icon pack aegis-icons.
|
||||
|
||||
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons) *
|
||||
|
||||
This project periodically generates an icon pack for Aegis based on [Simple
|
||||
Icons](https://simpleicons.org/). The icons are automatically generated, so
|
||||
Icons](https://simpleicons.org/).
|
||||
|
||||
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) *
|
||||
|
||||
This is a variant on the aegis-simple-icons pack where the icons contain no solid background and just the outlines are being used.
|
||||
|
||||
\* 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).
|
||||
|
||||
|
||||
+40
-31
@@ -1,6 +1,7 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
apply plugin: 'dagger.hilt.android.plugin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
|
||||
def getCmdOutput = { cmd ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
@@ -19,16 +20,16 @@ def fileProviderAuthority = "${packageName}.fileprovider"
|
||||
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
|
||||
|
||||
android {
|
||||
compileSdk 33
|
||||
compileSdk 34
|
||||
|
||||
namespace packageName
|
||||
|
||||
defaultConfig {
|
||||
applicationId "${packageName}"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 59
|
||||
versionName "2.2-beta1"
|
||||
targetSdkVersion 34
|
||||
versionCode 64
|
||||
versionName "3.0"
|
||||
multiDexEnabled true
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
|
||||
@@ -107,7 +108,7 @@ android {
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.8.0'
|
||||
artifact = 'com.google.protobuf:protoc:3.25.1'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
@@ -120,35 +121,42 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def cameraxVersion = '1.2.3'
|
||||
def glideVersion = '4.16.0'
|
||||
def guavaVersion = '32.1.2'
|
||||
def hiltVersion = '2.47'
|
||||
def junitVersion = '4.13.2'
|
||||
def libsuVersion = '5.2.0'
|
||||
aboutLibraries {
|
||||
// Tasks for aboutLibraries are not run automatically to keep the build reproducible
|
||||
// To update manually: ./gradlew app:exportLibraryDefinitions -PaboutLibraries.exportPath=src/main/res/raw
|
||||
prettyPrint = true
|
||||
configPath = "app/config"
|
||||
fetchRemoteFunding = false
|
||||
registerAndroidTasks = false
|
||||
exclusionPatterns = [~"javax.annotation.*"]
|
||||
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||
}
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.6.0'
|
||||
dependencies {
|
||||
def cameraxVersion = '1.3.1'
|
||||
def glideVersion = '4.16.0'
|
||||
def guavaVersion = '33.0.0'
|
||||
def hiltVersion = '2.50'
|
||||
def junitVersion = '4.13.2'
|
||||
def libsuVersion = '5.2.2'
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.7.1'
|
||||
annotationProcessor "com.google.dagger:hilt-compiler:$hiltVersion"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
||||
|
||||
// Ridiculous fix for a bunch of "Duplicate class" build errors:
|
||||
implementation (platform("org.jetbrains.kotlin:kotlin-bom:1.9.10"))
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.activity:activity:1.7.2'
|
||||
implementation 'androidx.activity:activity:1.8.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.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.2.3'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation "androidx.core:core:1.10.1"
|
||||
implementation "androidx.camera:camera-view:$cameraxVersion"
|
||||
implementation "androidx.core:core:1.12.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.6.1"
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.6.2"
|
||||
implementation "androidx.preference:preference:1.2.1"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
@@ -162,17 +170,18 @@ dependencies {
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
implementation "com.google.guava:guava:${guavaVersion}-android"
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.22.0'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
|
||||
implementation 'com.google.zxing:core:3.5.2'
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
|
||||
implementation("com.mikepenz:aboutlibraries:11.1.0") {
|
||||
exclude group: 'com.mikepenz', module: 'aboutlibraries-core'
|
||||
}
|
||||
implementation "com.mikepenz:aboutlibraries-core-android:11.1.0"
|
||||
implementation 'com.nulab-inc:zxcvbn:1.8.2'
|
||||
implementation 'de.hdodenhof:circleimageview:3.1.0'
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.2.0'
|
||||
implementation 'net.lingala.zip4j:zip4j:2.11.5'
|
||||
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
|
||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.76'
|
||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
|
||||
implementation "org.simpleflatmapper:sfm-csv:8.2.3"
|
||||
|
||||
androidTestAnnotationProcessor "com.google.dagger:hilt-android-compiler:$hiltVersion"
|
||||
@@ -190,8 +199,8 @@ dependencies {
|
||||
testImplementation 'androidx.test:core:1.5.0'
|
||||
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
|
||||
testImplementation "junit:junit:${junitVersion}"
|
||||
testImplementation 'org.json:json:20230618'
|
||||
testImplementation 'org.robolectric:robolectric:4.10.3'
|
||||
testImplementation 'org.json:json:20231013'
|
||||
testImplementation 'org.robolectric:robolectric:4.11.1'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"uniqueId": "com.github.avito-tech:krop",
|
||||
"licenses": [
|
||||
"MIT"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"uniqueId": "com.github.topjohnwu.libsu:.*::regex",
|
||||
"licenses": [
|
||||
"Apache-2.0"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"uniqueId": "com.amulyakhare:com.amulyakhare.textdrawable",
|
||||
"licenses": [
|
||||
"MIT"
|
||||
]
|
||||
}
|
||||
@@ -2,15 +2,19 @@ package com.beemdevelopment.aegis;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.matcher.BoundedMatcher;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.SCryptParameters;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryHolder;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
@@ -20,6 +24,7 @@ import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vectors.VaultEntries;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -178,4 +183,21 @@ public abstract class AegisTest {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected static Matcher<RecyclerView.ViewHolder> withOtpType(Class<? extends OtpInfo> otpClass) {
|
||||
return new BoundedMatcher<RecyclerView.ViewHolder, EntryHolder>(EntryHolder.class) {
|
||||
@Override
|
||||
public boolean matchesSafely(EntryHolder holder) {
|
||||
return holder != null
|
||||
&& holder.getEntry() != null
|
||||
&& holder.getEntry().getInfo().getClass().equals(otpClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText(String.format("with otp type '%s'", otpClass.getSimpleName()));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
|
||||
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
|
||||
import static androidx.test.espresso.action.ViewActions.clearText;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
@@ -18,19 +17,17 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.espresso.matcher.RootMatchers;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
@@ -41,6 +38,7 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.YandexInfo;
|
||||
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
@@ -55,6 +53,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest;
|
||||
|
||||
@@ -104,13 +103,19 @@ public class OverallTest extends AegisTest {
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, clickChildViewWithId(R.id.buttonRefresh)));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnHolderItem(withOtpType(HotpInfo.class), clickChildViewWithId(R.id.buttonRefresh)));
|
||||
}
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
AtomicBoolean isErrorCardShown = new AtomicBoolean(false);
|
||||
_activityRule.getScenario().onActivity(activity -> {
|
||||
isErrorCardShown.set(((EntryAdapter)((RecyclerView) activity.findViewById(R.id.rvKeyProfiles)).getAdapter()).isErrorCardShown());
|
||||
});
|
||||
|
||||
int entryPosOffset = isErrorCardShown.get() ? 1 : 0;
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
|
||||
onView(withId(R.id.action_copy)).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 1, longClick()));
|
||||
onView(withId(R.id.action_edit)).perform(click());
|
||||
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
|
||||
onView(withId(R.id.dropdown_group)).perform(click());
|
||||
@@ -129,13 +134,13 @@ public class OverallTest extends AegisTest {
|
||||
changeGroupFilter(_groupName);
|
||||
changeGroupFilter(null);
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(4, click()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 2, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 3, click()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 4, click()));
|
||||
onView(withId(R.id.action_share_qr)).perform(click());
|
||||
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
|
||||
onView(allOf(isDescendantOfA(withClassName(containsString("ActionBarContextView"))), withClassName(containsString("OverflowMenuButton")))).perform(click());
|
||||
onView(withText(R.string.action_delete)).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -32,11 +31,10 @@ public class PanicTriggerTest extends AegisTest {
|
||||
@Test
|
||||
public void testPanicTriggerDisabled() {
|
||||
assertFalse(_prefs.isPanicTriggerEnabled());
|
||||
assertTrue(_vaultManager.isVaultLoaded());
|
||||
launchPanic();
|
||||
assertTrue(_vaultManager.isVaultLoaded());
|
||||
_vaultManager.getVault();
|
||||
assertFalse(_vaultManager.isVaultFileLoaded());
|
||||
assertNull(_vaultManager.getVaultFileError());
|
||||
assertTrue(VaultRepository.fileExists(getApp()));
|
||||
}
|
||||
|
||||
@@ -44,11 +42,10 @@ public class PanicTriggerTest extends AegisTest {
|
||||
public void testPanicTriggerEnabled() {
|
||||
_prefs.setIsPanicTriggerEnabled(true);
|
||||
assertTrue(_prefs.isPanicTriggerEnabled());
|
||||
assertTrue(_vaultManager.isVaultLoaded());
|
||||
launchPanic();
|
||||
assertFalse(_vaultManager.isVaultLoaded());
|
||||
assertThrows(IllegalStateException.class, () -> _vaultManager.getVault());
|
||||
assertFalse(_vaultManager.isVaultFileLoaded());
|
||||
assertNull(_vaultManager.getVaultFileError());
|
||||
assertFalse(VaultRepository.fileExists(getApp()));
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@
|
||||
android:icon="@mipmap/${iconName}"
|
||||
android:label="Aegis"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Aegis.Launch"
|
||||
tools:replace="android:theme"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity android:name=".ui.TransferEntriesActivity"
|
||||
android:label="@string/title_activity_transfer" />
|
||||
@@ -82,6 +80,10 @@
|
||||
<activity
|
||||
android:name=".ui.GroupManagerActivity"
|
||||
android:label="@string/title_activity_manage_groups" />
|
||||
<activity android:name=".ui.AssignIconsActivity"
|
||||
android:label="@string/title_activity_assign_icons"/>
|
||||
<activity android:name=".ui.LicensesActivity"
|
||||
android:label="@string/title_activity_licenses"/>
|
||||
<activity
|
||||
android:name=".ui.PanicResponderActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -31,6 +31,44 @@
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 3.0</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Material 3 (and Material You)</li>
|
||||
<li>Automatic assignment of icons to entries</li>
|
||||
<li>Ability to select all entries in one go</li>
|
||||
<li>Support for importing 2FAS schema v4 backups</li>
|
||||
<li>Sort entries based on the last time they were used</li>
|
||||
<li>Some clarifications related to importing and backup permission errors</li>
|
||||
<li>Preparations for the ability to assign a single entry to multiple groups</li>
|
||||
<li>Performance improvements when scrolling through an entry list with lots of icons</li>
|
||||
<li>A new look for the third-party licenses list</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Directly importing from Authy using root would fail</li>
|
||||
<li>Minor glitches related to animation duration scale settings</li>
|
||||
<li>Various stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 2.2.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>An optional name field for icon packs to bypass filename character restrictions</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The Authenticator Pro importer only supported the legacy backup format</li>
|
||||
<li>A crash could occur in the tile service</li>
|
||||
</ul>
|
||||
<h3>Version 2.2.1</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Ability to automatically skip potential duplicates when importing entries</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Biometrics button on the unlock screen was unresponsive</li>
|
||||
</ul>
|
||||
<h3>Version 2.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
@@ -46,6 +84,7 @@
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Deleting an entry while a search filter is active now shows the correct state</li>
|
||||
<li>Aegis now fully respects system animation settings</li>
|
||||
</ul>
|
||||
<h3>Version 2.1.3</h3>
|
||||
<h4>New</h4>
|
||||
|
||||
@@ -22,8 +22,6 @@ import com.beemdevelopment.aegis.receivers.VaultLockReceiver;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -48,9 +46,6 @@ public abstract class AegisApplicationBase extends Application {
|
||||
super.onCreate();
|
||||
_vaultManager = EarlyEntryPoints.get(this, EntryPoint.class).getVaultManager();
|
||||
|
||||
Iconics.init(this);
|
||||
Iconics.registerFont(new MaterialDesignIconic());
|
||||
|
||||
VaultLockReceiver lockReceiver = new VaultLockReceiver();
|
||||
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
|
||||
ContextCompat.registerReceiver(this, lockReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.util.TimeUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultBackupPermissionException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -20,9 +21,11 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Preferences {
|
||||
@@ -142,6 +145,10 @@ public class Preferences {
|
||||
return CodeGrouping.valueOf(value);
|
||||
}
|
||||
|
||||
public void setCodeGroupSize(CodeGrouping codeGroupSize) {
|
||||
_prefs.edit().putString("pref_code_group_size_string", codeGroupSize.name()).apply();
|
||||
}
|
||||
|
||||
public boolean isIntroDone() {
|
||||
return _prefs.getBoolean("pref_intro", false);
|
||||
}
|
||||
@@ -195,6 +202,10 @@ public class Preferences {
|
||||
_prefs.edit().putInt("pref_current_theme", theme.ordinal()).apply();
|
||||
}
|
||||
|
||||
public boolean isDynamicColorsEnabled() {
|
||||
return _prefs.getBoolean("pref_dynamic_colors", false);
|
||||
}
|
||||
|
||||
public ViewMode getCurrentViewMode() {
|
||||
return ViewMode.fromInteger(_prefs.getInt("pref_current_view_mode", 0));
|
||||
}
|
||||
@@ -224,10 +235,51 @@ public class Preferences {
|
||||
setUsageCount(usageCounts);
|
||||
}
|
||||
|
||||
public long getLastUsedTimestamp(UUID uuid) {
|
||||
Map<UUID, Long> timestamps = getLastUsedTimestamps();
|
||||
if (timestamps != null && timestamps.size() > 0){
|
||||
Long timestamp = timestamps.get(uuid);
|
||||
return timestamp != null ? timestamp : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void clearUsageCount() {
|
||||
_prefs.edit().remove("pref_usage_count").apply();
|
||||
}
|
||||
|
||||
public Map<UUID, Long> getLastUsedTimestamps() {
|
||||
Map<UUID, Long> lastUsedTimestamps = new HashMap<>();
|
||||
String lastUsedTimestamp = _prefs.getString("pref_last_used_timestamps", "");
|
||||
try {
|
||||
JSONArray arr = new JSONArray(lastUsedTimestamp);
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
JSONObject json = arr.getJSONObject(i);
|
||||
lastUsedTimestamps.put(UUID.fromString(json.getString("uuid")), json.getLong("timestamp"));
|
||||
}
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
|
||||
return lastUsedTimestamps;
|
||||
}
|
||||
|
||||
public void setLastUsedTimestamps(Map<UUID, Long> lastUsedTimestamps) {
|
||||
JSONArray lastUsedTimestampJson = new JSONArray();
|
||||
for (Map.Entry<UUID, Long> entry : lastUsedTimestamps.entrySet()) {
|
||||
JSONObject entryJson = new JSONObject();
|
||||
try {
|
||||
entryJson.put("uuid", entry.getKey());
|
||||
entryJson.put("timestamp", entry.getValue());
|
||||
lastUsedTimestampJson.put(entryJson);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
_prefs.edit().putString("pref_last_used_timestamps", lastUsedTimestampJson.toString()).apply();
|
||||
}
|
||||
|
||||
public Map<UUID, Integer> getUsageCounts() {
|
||||
Map<UUID, Integer> usageCounts = new HashMap<>();
|
||||
String usageCount = _prefs.getString("pref_usage_count", "");
|
||||
@@ -263,8 +315,16 @@ public class Preferences {
|
||||
return _prefs.getInt("pref_timeout", -1);
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return _prefs.getString("pref_lang", "system");
|
||||
}
|
||||
|
||||
public void setLanguage(String lang) {
|
||||
_prefs.edit().putString("pref_lang", lang).apply();
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
String lang = _prefs.getString("pref_lang", "system");
|
||||
String lang = getLanguage();
|
||||
|
||||
if (lang.equals("system")) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
@@ -478,26 +538,26 @@ public class Preferences {
|
||||
return _prefs.getBoolean("pref_minimize_on_copy", false);
|
||||
}
|
||||
|
||||
public void setGroupFilter(List<String> groupFilter) {
|
||||
public void setGroupFilter(Set<UUID> groupFilter) {
|
||||
JSONArray json = new JSONArray(groupFilter);
|
||||
_prefs.edit().putString("pref_group_filter", json.toString()).apply();
|
||||
_prefs.edit().putString("pref_group_filter_uuids", json.toString()).apply();
|
||||
}
|
||||
|
||||
public List<String> getGroupFilter() {
|
||||
String raw = _prefs.getString("pref_group_filter", null);
|
||||
public Set<UUID> getGroupFilter() {
|
||||
String raw = _prefs.getString("pref_group_filter_uuids", null);
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
try {
|
||||
JSONArray json = new JSONArray(raw);
|
||||
List<String> filter = new ArrayList<>();
|
||||
Set<UUID> filter = new HashSet<>();
|
||||
for (int i = 0; i < json.length(); i++) {
|
||||
filter.add(json.isNull(i) ? null : json.optString(i));
|
||||
filter.add(json.isNull(i) ? null : UUID.fromString(json.getString(i)));
|
||||
}
|
||||
return filter;
|
||||
} catch (JSONException e) {
|
||||
return Collections.emptyList();
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,14 +565,16 @@ public class Preferences {
|
||||
private final Date _time;
|
||||
private boolean _isBuiltIn;
|
||||
private final String _error;
|
||||
private final boolean _isPermissionError;
|
||||
|
||||
public BackupResult(@Nullable Exception e) {
|
||||
this(new Date(), e == null ? null : e.toString());
|
||||
this(new Date(), e == null ? null : e.toString(), e instanceof VaultBackupPermissionException);
|
||||
}
|
||||
|
||||
private BackupResult(Date time, @Nullable String error) {
|
||||
private BackupResult(Date time, @Nullable String error, boolean isPermissionError) {
|
||||
_time = time;
|
||||
_error = error;
|
||||
_isPermissionError = isPermissionError;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -540,12 +602,17 @@ public class Preferences {
|
||||
_isBuiltIn = isBuiltIn;
|
||||
}
|
||||
|
||||
public boolean isPermissionError() {
|
||||
return _isPermissionError;
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try {
|
||||
obj.put("time", _time.getTime());
|
||||
obj.put("error", _error == null ? JSONObject.NULL : _error);
|
||||
obj.put("isPermissionError", _isPermissionError);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -557,7 +624,8 @@ public class Preferences {
|
||||
JSONObject obj = new JSONObject(json);
|
||||
long time = obj.getLong("time");
|
||||
String error = JsonUtils.optString(obj, "error");
|
||||
return new BackupResult(new Date(time), error);
|
||||
boolean isPermissionError = obj.optBoolean("isPermissionError");
|
||||
return new BackupResult(new Date(time), error, isPermissionError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import com.beemdevelopment.aegis.helpers.comparators.LastUsedComparator;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.UsageCountComparator;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
|
||||
@@ -14,7 +15,8 @@ public enum SortCategory {
|
||||
ACCOUNT_REVERSED,
|
||||
ISSUER,
|
||||
ISSUER_REVERSED,
|
||||
USAGE_COUNT;
|
||||
USAGE_COUNT,
|
||||
LAST_USED;
|
||||
|
||||
private static SortCategory[] _values;
|
||||
|
||||
@@ -31,20 +33,22 @@ public enum SortCategory {
|
||||
|
||||
switch (this) {
|
||||
case ACCOUNT:
|
||||
comparator = new AccountNameComparator();
|
||||
comparator = new AccountNameComparator().thenComparing(new IssuerNameComparator());
|
||||
break;
|
||||
case ACCOUNT_REVERSED:
|
||||
comparator = Collections.reverseOrder(new AccountNameComparator());
|
||||
comparator = Collections.reverseOrder(new AccountNameComparator().thenComparing(new IssuerNameComparator()));
|
||||
break;
|
||||
case ISSUER:
|
||||
comparator = new IssuerNameComparator();
|
||||
comparator = new IssuerNameComparator().thenComparing(new AccountNameComparator());
|
||||
break;
|
||||
case ISSUER_REVERSED:
|
||||
comparator = Collections.reverseOrder(new IssuerNameComparator());
|
||||
comparator = Collections.reverseOrder(new IssuerNameComparator().thenComparing(new AccountNameComparator()));
|
||||
break;
|
||||
case USAGE_COUNT:
|
||||
comparator = Collections.reverseOrder(new UsageCountComparator());
|
||||
break;
|
||||
case LAST_USED:
|
||||
comparator = Collections.reverseOrder(new LastUsedComparator());
|
||||
}
|
||||
|
||||
return comparator;
|
||||
@@ -64,6 +68,8 @@ public enum SortCategory {
|
||||
return R.id.menu_sort_alphabetically_reverse;
|
||||
case USAGE_COUNT:
|
||||
return R.id.menu_sort_usage_count;
|
||||
case LAST_USED:
|
||||
return R.id.menu_sort_last_used;
|
||||
default:
|
||||
return R.id.menu_sort_custom;
|
||||
}
|
||||
|
||||
@@ -10,20 +10,14 @@ public class ThemeMap {
|
||||
}
|
||||
|
||||
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_Default,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_Default,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Default
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> NO_ACTION_BAR = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_NoActionBar,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_NoActionBar,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_NoActionBar
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_Amoled
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_Fullscreen,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_Fullscreen,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Fullscreen
|
||||
Theme.AMOLED, R.style.Theme_Aegis_Amoled_Fullscreen
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,19 +35,19 @@ public enum ViewMode {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the height (in dp) that the divider between entries should have in this view mode.
|
||||
* Retrieves the offset (in dp) that should exist between entries in this view mode.
|
||||
*/
|
||||
public float getDividerHeight() {
|
||||
public float getItemOffset() {
|
||||
if (this == ViewMode.COMPACT) {
|
||||
return 0;
|
||||
return 1;
|
||||
} else if (this == ViewMode.TILES) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
return 20;
|
||||
return 8;
|
||||
}
|
||||
|
||||
public int getColumnSpan() {
|
||||
public int getSpanCount() {
|
||||
if (this == ViewMode.TILES) {
|
||||
return 2;
|
||||
}
|
||||
@@ -55,14 +55,6 @@ public enum ViewMode {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public float getDividerWidth() {
|
||||
if (this == ViewMode.TILES) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public String getFormattedAccountName(String accountName) {
|
||||
if (this == ViewMode.TILES) {
|
||||
return accountName;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.os.Build;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
|
||||
public class IconViewHelper {
|
||||
private IconViewHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layer type of the given ImageView based on the given IconType. If the
|
||||
* icon type is SVG and SDK <= 27, the layer type is set to software. Otherwise, it
|
||||
* is set to hardware.
|
||||
*/
|
||||
public static void setLayerType(ImageView view, IconType iconType) {
|
||||
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
|
||||
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
|
||||
return;
|
||||
}
|
||||
|
||||
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
}
|
||||
+13
-8
@@ -56,16 +56,20 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
// It's not clear when this can happen, but sometimes the ViewHolder
|
||||
// that's passed to this function has a position of -1, leading
|
||||
// to a crash down the line.
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
int position = viewHolder.getBindingAdapterPosition();
|
||||
if (position == NO_POSITION) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int swipeFlags = 0;
|
||||
|
||||
EntryAdapter adapter = (EntryAdapter) recyclerView.getAdapter();
|
||||
if (adapter == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int swipeFlags = 0;
|
||||
if (adapter.isPositionFooter(position)
|
||||
|| adapter.getEntryAt(position) != _selectedEntry
|
||||
|| adapter.isPositionErrorCard(position)
|
||||
|| adapter.getEntryAtPos(position) != _selectedEntry
|
||||
|| !isLongPressDragEnabled()) {
|
||||
return makeMovementFlags(0, swipeFlags);
|
||||
}
|
||||
@@ -76,12 +80,13 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
|
||||
RecyclerView.ViewHolder target) {
|
||||
if (target.getAdapterPosition() < _adapter.getShownFavoritesCount()){
|
||||
int targetIndex = _adapter.translateEntryPosToIndex(target.getBindingAdapterPosition());
|
||||
if (targetIndex < _adapter.getShownFavoritesCount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int firstPosition = viewHolder.getLayoutPosition();
|
||||
int secondPosition = target.getAdapterPosition();
|
||||
int secondPosition = target.getBindingAdapterPosition();
|
||||
|
||||
_adapter.onItemMove(firstPosition, secondPosition);
|
||||
_positionChanged = true;
|
||||
@@ -90,7 +95,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
_adapter.onItemDismiss(viewHolder.getAdapterPosition());
|
||||
_adapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,7 +103,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
|
||||
if (_positionChanged) {
|
||||
_adapter.onItemDrop(viewHolder.getAdapterPosition());
|
||||
_adapter.onItemDrop(viewHolder.getBindingAdapterPosition());
|
||||
_positionChanged = false;
|
||||
_adapter.refresh(false);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,55 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.util.TypedValue;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.google.android.material.color.DynamicColors;
|
||||
import com.google.android.material.color.DynamicColorsOptions;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ThemeHelper {
|
||||
private ThemeHelper() {
|
||||
private final AppCompatActivity _activity;
|
||||
private final Preferences _prefs;
|
||||
|
||||
public ThemeHelper(AppCompatActivity activity, Preferences prefs) {
|
||||
_activity = activity;
|
||||
_prefs = prefs;
|
||||
}
|
||||
|
||||
public static int getThemeColor(int attributeId, Resources.Theme currentTheme) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
currentTheme.resolveAttribute(attributeId, typedValue, true);
|
||||
@ColorInt int color = typedValue.data;
|
||||
/**
|
||||
* Sets the theme of the activity. The actual style that is set is picked from the
|
||||
* given map, based on the theme configured by the user.
|
||||
*/
|
||||
public void setTheme(Map<Theme, Integer> themeMap) {
|
||||
int theme = themeMap.get(getConfiguredTheme());
|
||||
_activity.setTheme(theme);
|
||||
|
||||
return color;
|
||||
if (_prefs.isDynamicColorsEnabled()) {
|
||||
DynamicColorsOptions.Builder optsBuilder = new DynamicColorsOptions.Builder();
|
||||
if (getConfiguredTheme().equals(Theme.AMOLED)) {
|
||||
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Amoled);
|
||||
}
|
||||
DynamicColors.applyToActivityIfAvailable(_activity, optsBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
public Theme getConfiguredTheme() {
|
||||
Theme theme = _prefs.getCurrentTheme();
|
||||
|
||||
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
|
||||
int currentNightMode = _activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
|
||||
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
|
||||
} else {
|
||||
theme = Theme.LIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.beemdevelopment.aegis.helpers.comparators;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class LastUsedComparator implements Comparator<VaultEntry> {
|
||||
@Override
|
||||
public int compare(VaultEntry a, VaultEntry b) {
|
||||
return Long.compare(a.getLastUsedTimestamp(), b.getLastUsedTimestamp());
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.beemdevelopment.aegis.icons;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
@@ -17,7 +18,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IconPack {
|
||||
private UUID _uuid;
|
||||
@@ -58,9 +58,21 @@ public class IconPack {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return _icons.stream()
|
||||
.filter(i -> i.isSuggestedFor(issuer))
|
||||
.collect(Collectors.toList());
|
||||
List<Icon> icons = new ArrayList<>();
|
||||
for (Icon icon : _icons) {
|
||||
MatchType matchType = icon.getMatchFor(issuer);
|
||||
if (matchType != null) {
|
||||
// Inverse matches (entry issuer contains icon name) are less likely
|
||||
// to be good, so position them at the end of the list.
|
||||
if (matchType.equals(MatchType.NORMAL)) {
|
||||
icons.add(0, icon);
|
||||
} else if (matchType.equals(MatchType.INVERSE)) {
|
||||
icons.add(icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return icons;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -120,13 +132,15 @@ public class IconPack {
|
||||
|
||||
public static class Icon implements Serializable {
|
||||
private final String _relFilename;
|
||||
private final String _name;
|
||||
private final String _category;
|
||||
private final List<String> _issuers;
|
||||
|
||||
private File _file;
|
||||
|
||||
protected Icon(String filename, String category, List<String> issuers) {
|
||||
protected Icon(String filename, String name, String category, List<String> issuers) {
|
||||
_relFilename = filename;
|
||||
_name = name;
|
||||
_category = category;
|
||||
_issuers = issuers;
|
||||
}
|
||||
@@ -149,6 +163,9 @@ public class IconPack {
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
if (_name != null) {
|
||||
return _name;
|
||||
}
|
||||
return Files.getNameWithoutExtension(new File(_relFilename).getName());
|
||||
}
|
||||
|
||||
@@ -156,19 +173,29 @@ public class IconPack {
|
||||
return _category;
|
||||
}
|
||||
|
||||
public List<String> getIssuers() {
|
||||
return Collections.unmodifiableList(_issuers);
|
||||
}
|
||||
private MatchType getMatchFor(String issuer) {
|
||||
String lowerEntryIssuer = issuer.toLowerCase();
|
||||
|
||||
public boolean isSuggestedFor(String issuer) {
|
||||
String lowerIssuer = issuer.toLowerCase();
|
||||
return getIssuers().stream()
|
||||
.map(String::toLowerCase)
|
||||
.anyMatch(is -> is.contains(lowerIssuer) || lowerIssuer.contains(is));
|
||||
boolean inverseMatch = false;
|
||||
for (String is : _issuers) {
|
||||
String lowerIconIssuer = is.toLowerCase();
|
||||
if (lowerIconIssuer.contains(lowerEntryIssuer)) {
|
||||
return MatchType.NORMAL;
|
||||
}
|
||||
if (lowerEntryIssuer.contains(lowerIconIssuer)) {
|
||||
inverseMatch = true;
|
||||
}
|
||||
}
|
||||
if (inverseMatch) {
|
||||
return MatchType.INVERSE;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Icon fromJson(JSONObject obj) throws JSONException {
|
||||
String filename = obj.getString("filename");
|
||||
String name = JsonUtils.optString(obj, "name");
|
||||
String category = obj.isNull("category") ? null : obj.getString("category");
|
||||
JSONArray array = obj.getJSONArray("issuer");
|
||||
|
||||
@@ -178,7 +205,12 @@ public class IconPack {
|
||||
issuers.add(issuer);
|
||||
}
|
||||
|
||||
return new Icon(filename, category, issuers);
|
||||
return new Icon(filename, name, category, issuers);
|
||||
}
|
||||
}
|
||||
|
||||
private enum MatchType {
|
||||
NORMAL,
|
||||
INVERSE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ public class IconPackManager {
|
||||
return packs.get(0);
|
||||
}
|
||||
|
||||
public boolean hasIconPack() {
|
||||
return _iconPacks.size() > 0;
|
||||
}
|
||||
|
||||
public List<IconPack> getIconPacks() {
|
||||
return new ArrayList<>(_iconPacks);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.beemdevelopment.aegis.vault.VaultEntryException;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
@@ -27,6 +28,7 @@ import org.json.JSONObject;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AegisImporter extends DatabaseImporter {
|
||||
|
||||
@@ -132,11 +134,31 @@ public class AegisImporter extends DatabaseImporter {
|
||||
Result result = new Result();
|
||||
|
||||
try {
|
||||
JSONArray array = _obj.getJSONArray("entries");
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject entryObj = array.getJSONObject(i);
|
||||
if (_obj.has("groups")) {
|
||||
JSONArray groupArray = _obj.getJSONArray("groups");
|
||||
for (int i = 0; i < groupArray.length(); i++) {
|
||||
JSONObject groupObj = groupArray.getJSONObject(i);
|
||||
try {
|
||||
VaultGroup group = convertGroup(groupObj);
|
||||
if (!result.getGroups().has(group)) {
|
||||
result.addGroup(group);
|
||||
}
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray entryArray = _obj.getJSONArray("entries");
|
||||
for (int i = 0; i < entryArray.length(); i++) {
|
||||
JSONObject entryObj = entryArray.getJSONObject(i);
|
||||
try {
|
||||
VaultEntry entry = convertEntry(entryObj);
|
||||
for (UUID groupUuid : entry.getGroups()) {
|
||||
if (!result.getGroups().has(groupUuid)) {
|
||||
entry.getGroups().remove(groupUuid);
|
||||
}
|
||||
}
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
@@ -156,5 +178,13 @@ public class AegisImporter extends DatabaseImporter {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static VaultGroup convertGroup(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
return VaultGroup.fromJson(obj);
|
||||
} catch (VaultEntryException e) {
|
||||
throw new DatabaseImporterEntryException(e, obj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
@@ -182,7 +183,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
context.getResources().getString(R.string.andotp_old_format)
|
||||
};
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.choose_andotp_importer)
|
||||
.setSingleChoiceItems(choices, 0, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
|
||||
+106
-16
@@ -16,11 +16,13 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.Argon2Task;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.bouncycastle.crypto.params.Argon2Parameters;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -44,9 +46,8 @@ import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
public class AuthenticatorProImporter extends DatabaseImporter {
|
||||
private static final String HEADER = "AuthenticatorPro";
|
||||
private static final int ITERATIONS = 64000;
|
||||
private static final int KEY_SIZE = 32 * Byte.SIZE;
|
||||
private static final String HEADER = "AUTHENTICATORPRO";
|
||||
private static final String HEADER_LEGACY = "AuthenticatorPro";
|
||||
private static final String PKG_NAME = "me.jmh.authenticatorpro";
|
||||
private static final String PKG_DB_PATH = "files/proauth.db3";
|
||||
|
||||
@@ -90,24 +91,19 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
private static EncryptedState readEncrypted(DataInputStream stream) throws DatabaseImporterException {
|
||||
private static State readEncrypted(DataInputStream stream) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] headerBytes = new byte[HEADER.getBytes(StandardCharsets.UTF_8).length];
|
||||
stream.readFully(headerBytes);
|
||||
String header = new String(headerBytes, StandardCharsets.UTF_8);
|
||||
if (!header.equals(HEADER)) {
|
||||
throw new DatabaseImporterException("Invalid file header");
|
||||
switch (header) {
|
||||
case HEADER:
|
||||
return EncryptedState.parseHeader(stream);
|
||||
case HEADER_LEGACY:
|
||||
return LegacyEncryptedState.parseHeader(stream);
|
||||
default:
|
||||
throw new DatabaseImporterException("Invalid file header");
|
||||
}
|
||||
|
||||
int saltSize = 20;
|
||||
byte[] salt = new byte[saltSize];
|
||||
stream.readFully(salt);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
int ivSize = cipher.getBlockSize();
|
||||
byte[] iv = new byte[ivSize];
|
||||
stream.readFully(iv);
|
||||
return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
|
||||
} catch (UTFDataFormatException e) {
|
||||
throw new DatabaseImporterException("Invalid file header");
|
||||
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException e) {
|
||||
@@ -130,6 +126,13 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
static class EncryptedState extends State {
|
||||
private static final int KEY_SIZE = 32;
|
||||
private static final int MEMORY_COST = 16; // 2^16 KiB = 64 MiB
|
||||
private static final int PARALLELISM = 4;
|
||||
private static final int ITERATIONS = 3;
|
||||
private static final int SALT_SIZE = 16;
|
||||
private static final int IV_SIZE = 12;
|
||||
|
||||
private final Cipher _cipher;
|
||||
private final byte[] _salt;
|
||||
private final byte[] _iv;
|
||||
@@ -143,6 +146,81 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public JsonState decrypt(char[] password) throws DatabaseImporterException {
|
||||
Argon2Task.Params params = getKeyDerivationParams(password);
|
||||
SecretKey key = Argon2Task.deriveKey(params);
|
||||
return decrypt(key);
|
||||
}
|
||||
|
||||
public JsonState decrypt(SecretKey key) throws DatabaseImporterException {
|
||||
try {
|
||||
_cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv));
|
||||
byte[] decrypted = _cipher.doFinal(_data);
|
||||
return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8)));
|
||||
} catch (InvalidAlgorithmParameterException | IllegalBlockSizeException
|
||||
| JSONException | InvalidKeyException | BadPaddingException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
|
||||
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
|
||||
Argon2Task.Params params = getKeyDerivationParams(password);
|
||||
Argon2Task task = new Argon2Task(context, key -> {
|
||||
try {
|
||||
AuthenticatorProImporter.JsonState state = decrypt(key);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
|
||||
task.execute(lifecycle, params);
|
||||
}, dialog -> listener.onCanceled());
|
||||
}
|
||||
|
||||
private Argon2Task.Params getKeyDerivationParams(char[] password) {
|
||||
Argon2Parameters argon2Params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
|
||||
.withIterations(ITERATIONS)
|
||||
.withParallelism(PARALLELISM)
|
||||
.withMemoryPowOfTwo(MEMORY_COST)
|
||||
.withSalt(_salt)
|
||||
.build();
|
||||
return new Argon2Task.Params(password, argon2Params, KEY_SIZE);
|
||||
}
|
||||
|
||||
private static EncryptedState parseHeader(DataInputStream stream)
|
||||
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
|
||||
byte[] salt = new byte[SALT_SIZE];
|
||||
stream.readFully(salt);
|
||||
|
||||
byte[] iv = new byte[IV_SIZE];
|
||||
stream.readFully(iv);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
|
||||
}
|
||||
}
|
||||
|
||||
static class LegacyEncryptedState extends State {
|
||||
private static final int ITERATIONS = 64000;
|
||||
private static final int KEY_SIZE = 32 * Byte.SIZE;
|
||||
private static final int SALT_SIZE = 20;
|
||||
|
||||
private final Cipher _cipher;
|
||||
private final byte[] _salt;
|
||||
private final byte[] _iv;
|
||||
private final byte[] _data;
|
||||
|
||||
public LegacyEncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
|
||||
super(true);
|
||||
_cipher = cipher;
|
||||
_salt = salt;
|
||||
_iv = iv;
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public JsonState decrypt(char[] password) throws DatabaseImporterException {
|
||||
PBKDFTask.Params params = getKeyDerivationParams(password);
|
||||
SecretKey key = PBKDFTask.deriveKey(params);
|
||||
@@ -180,6 +258,18 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
||||
private PBKDFTask.Params getKeyDerivationParams(char[] password) {
|
||||
return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, _salt, ITERATIONS);
|
||||
}
|
||||
|
||||
private static LegacyEncryptedState parseHeader(DataInputStream stream)
|
||||
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
|
||||
byte[] salt = new byte[SALT_SIZE];
|
||||
stream.readFully(salt);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
int ivSize = cipher.getBlockSize();
|
||||
byte[] iv = new byte[ivSize];
|
||||
stream.readFully(iv);
|
||||
return new LegacyEncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
|
||||
}
|
||||
}
|
||||
|
||||
private static class JsonState extends State {
|
||||
|
||||
@@ -75,8 +75,13 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
JSONArray array;
|
||||
JSONArray authyArray;
|
||||
try {
|
||||
array = readFile(new SuFile(path, String.format("%s.xml", _authFilename)), String.format("%s.key", _authFilename));
|
||||
authyArray = readFile(new SuFile(path, String.format("%s.xml", _authyFilename)), String.format("%s.key", _authyFilename));
|
||||
SuFile file1 = new SuFile(path, String.format("%s.xml", _authFilename));
|
||||
file1.setShell(shell);
|
||||
SuFile file2 = new SuFile(path, String.format("%s.xml", _authyFilename));
|
||||
file2.setShell(shell);
|
||||
|
||||
array = readFile(file1, String.format("%s.key", _authFilename));
|
||||
authyArray = readFile(file2, String.format("%s.key", _authyFilename));
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.annotation.StringRes;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
@@ -168,12 +169,17 @@ public abstract class DatabaseImporter {
|
||||
|
||||
public static class Result {
|
||||
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||
private UUIDMap<VaultGroup> _groups = new UUIDMap<>();
|
||||
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
public void addGroup(VaultGroup group) {
|
||||
_groups.add(group);
|
||||
}
|
||||
|
||||
public void addError(DatabaseImporterEntryException error) {
|
||||
_errors.add(error);
|
||||
}
|
||||
@@ -182,6 +188,10 @@ public abstract class DatabaseImporter {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
public UUIDMap<VaultGroup> getGroups() {
|
||||
return _groups;
|
||||
}
|
||||
|
||||
public List<DatabaseImporterEntryException> getErrors() {
|
||||
return _errors;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ import org.json.JSONObject;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class SteamImporter extends DatabaseImporter {
|
||||
private static final String _subDir = "files";
|
||||
@@ -57,29 +61,43 @@ public class SteamImporter extends DatabaseImporter {
|
||||
try {
|
||||
byte[] bytes = IOUtils.readAll(stream);
|
||||
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
||||
return new State(obj);
|
||||
|
||||
List<JSONObject> objs = new ArrayList<>();
|
||||
if (obj.has("accounts")) {
|
||||
JSONObject accounts = obj.getJSONObject("accounts");
|
||||
Iterator<String> keys = accounts.keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
objs.add(accounts.getJSONObject(key));
|
||||
}
|
||||
} else {
|
||||
objs.add(obj);
|
||||
}
|
||||
return new State(objs);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private JSONObject _obj;
|
||||
private final List<JSONObject> _objs;
|
||||
|
||||
private State(JSONObject obj) {
|
||||
private State(List<JSONObject> objs) {
|
||||
super(false);
|
||||
_obj = obj;
|
||||
_objs = objs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
Result result = new Result();
|
||||
|
||||
try {
|
||||
VaultEntry entry = convertEntry(_obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
for (JSONObject obj : _objs) {
|
||||
try {
|
||||
VaultEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
+2
-3
@@ -4,8 +4,6 @@ import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
@@ -18,6 +16,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
@@ -154,7 +153,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
|
||||
.setMessage(R.string.choose_totpauth_importer)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
@@ -60,7 +61,7 @@ public class TwoFASImporter extends DatabaseImporter {
|
||||
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
|
||||
JSONObject obj = new JSONObject(json);
|
||||
int version = obj.getInt("schemaVersion");
|
||||
if (version > 3) {
|
||||
if (version > 4) {
|
||||
throw new DatabaseImporterException(String.format("Unsupported schema version: %d", version));
|
||||
}
|
||||
|
||||
@@ -187,6 +188,9 @@ public class TwoFASImporter extends DatabaseImporter {
|
||||
} else if (tokenType.equals("HOTP")) {
|
||||
long counter = info.optLong("counter", 0);
|
||||
otp = new HotpInfo(secret, algorithm, digits, counter);
|
||||
} else if (tokenType.equals("STEAM")) {
|
||||
int period = info.optInt("period", TotpInfo.DEFAULT_PERIOD);
|
||||
otp = new SteamInfo(secret, algorithm, digits, period);
|
||||
} else {
|
||||
throw new DatabaseImporterEntryException(String.format("Unrecognized tokenType: %s", tokenType), obj.toString());
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.beemdevelopment.aegis.licenses;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import de.psdev.licensesdialog.licenses.License;
|
||||
|
||||
public class GlideLicense extends License {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Glide License";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSummaryTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.glide_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readFullTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.glide_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return "https://github.com/bumptech/glide/blob/master/LICENSE";
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.beemdevelopment.aegis.licenses;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import de.psdev.licensesdialog.licenses.License;
|
||||
|
||||
public class ProtobufLicense extends License {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Protocol Buffers License";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSummaryTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.protobuf_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readFullTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.protobuf_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return "https://raw.githubusercontent.com/protocolbuffers/protobuf/master/LICENSE";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.beemdevelopment.aegis.services;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.service.quicksettings.Tile;
|
||||
@@ -16,10 +18,13 @@ public class LaunchAppTileService extends TileService {
|
||||
public void onStartListening() {
|
||||
super.onStartListening();
|
||||
Tile tile = getQsTile();
|
||||
tile.setState(Tile.STATE_INACTIVE);
|
||||
tile.updateTile();
|
||||
if (tile != null) {
|
||||
tile.setState(Tile.STATE_INACTIVE);
|
||||
tile.updateTile();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
@Override
|
||||
public void onClick() {
|
||||
super.onClick();
|
||||
@@ -28,6 +33,12 @@ public class LaunchAppTileService extends TileService {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
|
||||
startActivityAndCollapse(intent);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, flags);
|
||||
startActivityAndCollapse(pendingIntent);
|
||||
} else {
|
||||
startActivityAndCollapse(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.beemdevelopment.aegis.services;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.service.quicksettings.Tile;
|
||||
@@ -16,10 +18,13 @@ public class LaunchScannerTileService extends TileService {
|
||||
public void onStartListening() {
|
||||
super.onStartListening();
|
||||
Tile tile = getQsTile();
|
||||
tile.setState(Tile.STATE_INACTIVE);
|
||||
tile.updateTile();
|
||||
if (tile != null) {
|
||||
tile.setState(Tile.STATE_INACTIVE);
|
||||
tile.updateTile();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
@Override
|
||||
public void onClick() {
|
||||
super.onClick();
|
||||
@@ -29,6 +34,12 @@ public class LaunchScannerTileService extends TileService {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
|
||||
startActivityAndCollapse(intent);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, flags);
|
||||
startActivityAndCollapse(pendingIntent);
|
||||
} else {
|
||||
startActivityAndCollapse(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ public class NotificationService extends Service {
|
||||
.setOngoing(true)
|
||||
.setContentIntent(pendingIntent);
|
||||
|
||||
startForeground(NOTIFICATION_VAULT_UNLOCKED, builder.build());
|
||||
// NOTE: Disabled for now. See issue: #1047
|
||||
//startForeground(NOTIFICATION_VAULT_UNLOCKED, builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,20 +13,12 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.view.LayoutInflaterCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.licenses.GlideLicense;
|
||||
import com.beemdevelopment.aegis.licenses.ProtobufLicense;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.ChangelogDialog;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.LicenseDialog;
|
||||
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
|
||||
|
||||
import de.psdev.licensesdialog.LicenseResolver;
|
||||
import de.psdev.licensesdialog.LicensesDialog;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
public class AboutActivity extends AegisActivity {
|
||||
|
||||
@@ -40,7 +32,6 @@ public class AboutActivity extends AegisActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new IconicsLayoutInflater2(getDelegate()));
|
||||
super.onCreate(savedInstanceState);
|
||||
if (abortIfOrphan(savedInstanceState)) {
|
||||
return;
|
||||
@@ -57,12 +48,15 @@ public class AboutActivity extends AegisActivity {
|
||||
View btnLicense = findViewById(R.id.btn_license);
|
||||
btnLicense.setOnClickListener(v -> {
|
||||
LicenseDialog.create()
|
||||
.setTheme(getConfiguredTheme())
|
||||
.setTheme(_themeHelper.getConfiguredTheme())
|
||||
.show(getSupportFragmentManager(), null);
|
||||
});
|
||||
|
||||
View btnThirdPartyLicenses = findViewById(R.id.btn_third_party_licenses);
|
||||
btnThirdPartyLicenses.setOnClickListener(v -> showThirdPartyLicenseDialog());
|
||||
btnThirdPartyLicenses.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(this, LicensesActivity.class);
|
||||
startActivity(intent);
|
||||
});
|
||||
|
||||
TextView appVersion = findViewById(R.id.app_version);
|
||||
appVersion.setText(getCurrentAppVersion());
|
||||
@@ -93,7 +87,7 @@ public class AboutActivity extends AegisActivity {
|
||||
View btnChangelog = findViewById(R.id.btn_changelog);
|
||||
btnChangelog.setOnClickListener(v -> {
|
||||
ChangelogDialog.create()
|
||||
.setTheme(getConfiguredTheme())
|
||||
.setTheme(_themeHelper.getConfiguredTheme())
|
||||
.show(getSupportFragmentManager(), null);
|
||||
});
|
||||
}
|
||||
@@ -130,30 +124,9 @@ public class AboutActivity extends AegisActivity {
|
||||
startActivity(Intent.createChooser(mailIntent, getString(R.string.email)));
|
||||
}
|
||||
|
||||
private void showThirdPartyLicenseDialog() {
|
||||
String stylesheet = getString(R.string.custom_notices_format_style);
|
||||
int backgroundColorResource = getConfiguredTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
String backgroundColor = getThemeColorAsHex(backgroundColorResource);
|
||||
String textColor = getThemeColorAsHex(R.attr.primaryText);
|
||||
String licenseColor = getThemeColorAsHex(R.attr.cardBackgroundFocused);
|
||||
String linkColor = getThemeColorAsHex(R.attr.colorAccent);
|
||||
|
||||
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor, linkColor);
|
||||
|
||||
LicenseResolver.registerLicense(new GlideLicense());
|
||||
LicenseResolver.registerLicense(new ProtobufLicense());
|
||||
|
||||
new LicensesDialog.Builder(this)
|
||||
.setNotices(R.raw.notices)
|
||||
.setTitle(R.string.third_party_licenses)
|
||||
.setNoticesCssStyle(stylesheet)
|
||||
.setIncludeOwnLicense(true)
|
||||
.build()
|
||||
.show();
|
||||
}
|
||||
|
||||
private String getThemeColorAsHex(@AttrRes int attributeId) {
|
||||
return String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(attributeId, getTheme())));
|
||||
int color = MaterialColors.getColor(this, attributeId, getClass().getCanonicalName());
|
||||
return String.format("%06X", 0xFFFFFF & color);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,25 +4,33 @@ import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.core.view.ViewPropertyAnimatorCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPackManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -35,6 +43,7 @@ import dagger.hilt.components.SingletonComponent;
|
||||
@AndroidEntryPoint
|
||||
public abstract class AegisActivity extends AppCompatActivity implements VaultManager.LockListener {
|
||||
protected Preferences _prefs;
|
||||
protected ThemeHelper _themeHelper;
|
||||
|
||||
@Inject
|
||||
protected VaultManager _vaultManager;
|
||||
@@ -42,14 +51,19 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
|
||||
@Inject
|
||||
protected IconPackManager _iconPackManager;
|
||||
|
||||
private ActionModeStatusGuardHack _statusGuardHack;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// set the theme and locale before creating the activity
|
||||
_prefs = EarlyEntryPoints.get(getApplicationContext(), PrefEntryPoint.class).getPreferences();
|
||||
_themeHelper = new ThemeHelper(this, _prefs);
|
||||
onSetTheme();
|
||||
setLocale(_prefs.getLocale());
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
_statusGuardHack = new ActionModeStatusGuardHack();
|
||||
|
||||
// set FLAG_SECURE on the window of every AegisActivity
|
||||
if (_prefs.isSecureScreenEnabled()) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
@@ -96,31 +110,7 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
|
||||
* Called when the activity is expected to set its theme.
|
||||
*/
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme of the activity. The actual style that is set is picked from the
|
||||
* given map, based on the theme configured by the user.
|
||||
*/
|
||||
protected void setTheme(Map<Theme, Integer> themeMap) {
|
||||
int theme = themeMap.get(getConfiguredTheme());
|
||||
setTheme(theme);
|
||||
}
|
||||
|
||||
protected Theme getConfiguredTheme() {
|
||||
Theme theme = _prefs.getCurrentTheme();
|
||||
|
||||
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
|
||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
|
||||
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
|
||||
} else {
|
||||
theme = Theme.LIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
return theme;
|
||||
_themeHelper.setTheme(ThemeMap.DEFAULT);
|
||||
}
|
||||
|
||||
protected void setLocale(Locale locale) {
|
||||
@@ -169,6 +159,77 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSupportActionModeStarted(@NonNull ActionMode mode) {
|
||||
super.onSupportActionModeStarted(mode);
|
||||
_statusGuardHack.apply(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSupportActionModeFinished(@NonNull ActionMode mode) {
|
||||
super.onSupportActionModeFinished(mode);
|
||||
_statusGuardHack.apply(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* When starting/finishing an action mode, forcefully cancel the fade in/out animation and
|
||||
* set the status bar color. This requires the abc_decor_view_status_guard colors to be set
|
||||
* to transparent.
|
||||
*
|
||||
* This should fix any inconsistencies between the color of the action bar and the status bar
|
||||
* when an action mode is active.
|
||||
*/
|
||||
private class ActionModeStatusGuardHack {
|
||||
private Field _fadeAnimField;
|
||||
private Field _actionModeViewField;
|
||||
|
||||
@ColorInt
|
||||
private final int _statusBarColor;
|
||||
|
||||
private ActionModeStatusGuardHack() {
|
||||
_statusBarColor = getWindow().getStatusBarColor();
|
||||
|
||||
try {
|
||||
_fadeAnimField = getDelegate().getClass().getDeclaredField("mFadeAnim");
|
||||
_fadeAnimField.setAccessible(true);
|
||||
_actionModeViewField = getDelegate().getClass().getDeclaredField("mActionModeView");
|
||||
_actionModeViewField.setAccessible(true);
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void apply(int visibility) {
|
||||
if (_fadeAnimField == null || _actionModeViewField == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ViewPropertyAnimatorCompat fadeAnim;
|
||||
ViewGroup actionModeView;
|
||||
try {
|
||||
fadeAnim = (ViewPropertyAnimatorCompat) _fadeAnimField.get(getDelegate());
|
||||
actionModeView = (ViewGroup) _actionModeViewField.get(getDelegate());
|
||||
} catch (IllegalAccessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fadeAnim == null || actionModeView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fadeAnim.cancel();
|
||||
|
||||
actionModeView.setVisibility(visibility);
|
||||
actionModeView.setAlpha(visibility == View.VISIBLE ? 1f : 0f);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
int statusBarColor = visibility == View.VISIBLE
|
||||
? MaterialColors.getColor(actionModeView, com.google.android.material.R.attr.colorSurfaceContainer)
|
||||
: _statusBarColor;
|
||||
getWindow().setStatusBarColor(statusBarColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this Activity instance has become an orphan. This can happen if
|
||||
* the vault was killed/locked by an external trigger while the Activity was still open.
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.MetricsHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
|
||||
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
|
||||
import com.beemdevelopment.aegis.ui.views.AssignIconAdapter;
|
||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.ListPreloader;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AssignIconsActivity extends AegisActivity implements AssignIconAdapter.Listener {
|
||||
private AssignIconAdapter _adapter;
|
||||
private ArrayList<AssignIconEntry> _entries = new ArrayList<>();
|
||||
private RecyclerView _entriesView;
|
||||
private AssignIconsActivity.BackPressHandler _backPressHandler;
|
||||
private ViewPreloadSizeProvider<AssignIconEntry> _preloadSizeProvider;
|
||||
private IconPack _favoriteIconPack;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (abortIfOrphan(savedInstanceState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_assign_icons);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
ArrayList<UUID> assignIconEntriesIds = (ArrayList<UUID>) getIntent().getSerializableExtra("entries");
|
||||
for (UUID entryId: assignIconEntriesIds) {
|
||||
VaultEntry vaultEntry = _vaultManager.getVault().getEntryByUUID(entryId);
|
||||
_entries.add(new AssignIconEntry(vaultEntry));
|
||||
}
|
||||
|
||||
_backPressHandler = new AssignIconsActivity.BackPressHandler();
|
||||
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
|
||||
|
||||
IconPreloadProvider modelProvider1 = new IconPreloadProvider();
|
||||
EntryIconPreloadProvider modelProvider2 = new EntryIconPreloadProvider();
|
||||
_preloadSizeProvider = new ViewPreloadSizeProvider<>();
|
||||
RecyclerViewPreloader<IconPack.Icon> preloader1 = new RecyclerViewPreloader(this, modelProvider1, _preloadSizeProvider, 10);
|
||||
RecyclerViewPreloader<VaultEntry> preloader2 = new RecyclerViewPreloader(this, modelProvider2, _preloadSizeProvider, 10);
|
||||
|
||||
_adapter = new AssignIconAdapter(this);
|
||||
_entriesView = findViewById(R.id.list_assign_icons);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
_entriesView.setLayoutManager(layoutManager);
|
||||
_entriesView.setAdapter(_adapter);
|
||||
_entriesView.setNestedScrollingEnabled(false);
|
||||
_entriesView.addItemDecoration(new SpacesItemDecoration(8));
|
||||
_entriesView.addOnScrollListener(preloader1);
|
||||
_entriesView.addOnScrollListener(preloader2);
|
||||
|
||||
Optional<IconPack> favoriteIconPack = _iconPackManager.getIconPacks().stream()
|
||||
.sorted(Comparator.comparing(IconPack::getName))
|
||||
.findFirst();
|
||||
|
||||
if (!favoriteIconPack.isPresent()) {
|
||||
throw new RuntimeException(String.format("Started %s without any icon packs present", AssignIconsActivity.class.getName()));
|
||||
}
|
||||
|
||||
_favoriteIconPack = favoriteIconPack.get();
|
||||
|
||||
for (AssignIconEntry entry : _entries) {
|
||||
IconPack.Icon suggestedIcon = findSuggestedIcon(entry);
|
||||
if (suggestedIcon != null) {
|
||||
entry.setNewIcon(suggestedIcon);
|
||||
}
|
||||
}
|
||||
|
||||
_adapter.addEntries(_entries);
|
||||
}
|
||||
|
||||
private IconPack.Icon findSuggestedIcon(AssignIconEntry entry) {
|
||||
List<IconPack.Icon> suggestedIcons = _favoriteIconPack.getSuggestedIcons(entry.getEntry().getIssuer());
|
||||
if (suggestedIcons.size() > 0) {
|
||||
return suggestedIcons.get(0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void saveAndFinish() throws IOException {
|
||||
ArrayList<UUID> uuids = new ArrayList<>();
|
||||
for (AssignIconEntry selectedEntry : _entries) {
|
||||
VaultEntry entry = selectedEntry.getEntry();
|
||||
if (selectedEntry.getNewIcon() != null) {
|
||||
byte[] iconBytes;
|
||||
try (FileInputStream inStream = new FileInputStream(selectedEntry.getNewIcon().getFile())){
|
||||
iconBytes = IOUtils.readFile(inStream);
|
||||
}
|
||||
|
||||
VaultEntryIcon icon = new VaultEntryIcon(iconBytes, selectedEntry.getNewIcon().getIconType());
|
||||
entry.setIcon(icon);
|
||||
uuids.add(entry.getUUID());
|
||||
|
||||
_vaultManager.getVault().replaceEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entryUUIDs", uuids);
|
||||
|
||||
if (saveAndBackupVault()) {
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void discardAndFinish() {
|
||||
Dialogs.showDiscardDialog(this,
|
||||
(dialog, which) -> {
|
||||
try {
|
||||
saveAndFinish();
|
||||
} catch (IOException e) {
|
||||
Toast.makeText(this, R.string.saving_assign_icons_error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
},
|
||||
(dialog, which) -> finish());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_assign_icons, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
discardAndFinish();
|
||||
} else if (itemId == R.id.action_save) {
|
||||
try {
|
||||
saveAndFinish();
|
||||
} catch (IOException e) {
|
||||
Toast.makeText(this, R.string.saving_assign_icons_error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAssignIconEntryClick(AssignIconEntry entry) {
|
||||
BottomSheetDialog dialog = IconPickerDialog.create(this, Collections.singletonList(_favoriteIconPack), entry.getEntry().getIssuer(), false, new IconAdapter.Listener() {
|
||||
@Override
|
||||
public void onIconSelected(IconPack.Icon icon) {
|
||||
entry.setNewIcon(icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomSelected() { }
|
||||
});
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetPreloadView(View view) {
|
||||
_preloadSizeProvider.setView(view);
|
||||
}
|
||||
|
||||
private class BackPressHandler extends OnBackPressedCallback {
|
||||
public BackPressHandler() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
discardAndFinish();
|
||||
}
|
||||
}
|
||||
|
||||
private class EntryIconPreloadProvider implements ListPreloader.PreloadModelProvider<VaultEntry> {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<VaultEntry> getPreloadItems(int position) {
|
||||
VaultEntry entry = _entries.get(position).getEntry();
|
||||
if (entry.hasIcon()) {
|
||||
return Collections.singletonList(entry);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) {
|
||||
RequestBuilder<Drawable> rb = Glide.with(AssignIconsActivity.this)
|
||||
.load(entry.getIcon());
|
||||
return GlideHelper.setCommonOptions(rb, entry.getIcon().getType());
|
||||
}
|
||||
}
|
||||
|
||||
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<IconPack.Icon> {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<IconPack.Icon> getPreloadItems(int position) {
|
||||
AssignIconEntry entry = _entries.get(position);
|
||||
if (entry.getNewIcon() != null) {
|
||||
return Collections.singletonList(entry.getNewIcon());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
|
||||
RequestBuilder<Drawable> rb = Glide.with(AssignIconsActivity.this)
|
||||
.load(icon.getFile());
|
||||
return GlideHelper.setCommonOptions(rb, icon.getIconType());
|
||||
}
|
||||
}
|
||||
|
||||
private class SpacesItemDecoration extends RecyclerView.ItemDecoration {
|
||||
private final int _space;
|
||||
|
||||
public SpacesItemDecoration(int dpSpace) {
|
||||
|
||||
this._space = MetricsHelper.convertDpToPixels(AssignIconsActivity.this, dpSpace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
|
||||
outRect.left = _space;
|
||||
outRect.right = _space;
|
||||
outRect.bottom = _space;
|
||||
|
||||
if (parent.getChildLayoutPosition(view) == 0) {
|
||||
outRect.top = _space;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.KeyEvent;
|
||||
@@ -20,11 +19,9 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
@@ -36,6 +33,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
@@ -43,6 +41,7 @@ import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -55,10 +54,13 @@ public class AuthActivity extends AegisActivity {
|
||||
|
||||
private EditText _textPassword;
|
||||
|
||||
private VaultFile _vaultFile;
|
||||
private SlotList _slots;
|
||||
|
||||
private SecretKey _bioKey;
|
||||
private BiometricSlot _bioSlot;
|
||||
private BiometricPrompt _bioPrompt;
|
||||
private Button _decryptButton;
|
||||
|
||||
private int _failedUnlockAttempts;
|
||||
|
||||
@@ -72,14 +74,14 @@ public class AuthActivity extends AegisActivity {
|
||||
setContentView(R.layout.activity_auth);
|
||||
_textPassword = findViewById(R.id.text_password);
|
||||
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
|
||||
Button decryptButton = findViewById(R.id.button_decrypt);
|
||||
_decryptButton = findViewById(R.id.button_decrypt);
|
||||
TextView biometricsButton = findViewById(R.id.button_biometrics);
|
||||
|
||||
getOnBackPressedDispatcher().addCallback(this, new BackPressHandler());
|
||||
|
||||
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
decryptButton.performClick();
|
||||
_decryptButton.performClick();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -104,17 +106,17 @@ public class AuthActivity extends AegisActivity {
|
||||
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
|
||||
}
|
||||
|
||||
if (_vaultManager.getVaultFileError() != null) {
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, _vaultManager.getVaultFileError(), (dialog, which) -> {
|
||||
try {
|
||||
_vaultFile = VaultRepository.readVaultFile(this);
|
||||
} catch (VaultRepositoryException e) {
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
VaultFile vaultFile = _vaultManager.getVaultFile();
|
||||
_slots = vaultFile.getHeader().getSlots();
|
||||
|
||||
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
|
||||
_slots = _vaultFile.getHeader().getSlots();
|
||||
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
|
||||
boolean invalidated = false;
|
||||
|
||||
@@ -150,7 +152,7 @@ public class AuthActivity extends AegisActivity {
|
||||
}
|
||||
}
|
||||
|
||||
decryptButton.setOnClickListener(v -> {
|
||||
_decryptButton.setOnClickListener(v -> {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
|
||||
|
||||
@@ -159,18 +161,23 @@ public class AuthActivity extends AegisActivity {
|
||||
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
|
||||
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener());
|
||||
task.execute(getLifecycle(), params);
|
||||
|
||||
_decryptButton.setEnabled(false);
|
||||
});
|
||||
|
||||
biometricsButton.setOnClickListener(v -> {
|
||||
if (_prefs.isPasswordReminderNeeded()) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(getString(R.string.password_reminder_dialog_title))
|
||||
.setMessage(getString(R.string.password_reminder_dialog_message))
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
showBiometricPrompt();
|
||||
})
|
||||
.create());
|
||||
} else {
|
||||
showBiometricPrompt();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -181,11 +188,6 @@ public class AuthActivity extends AegisActivity {
|
||||
outState.putBoolean("inhibitBioPrompt", _inhibitBioPrompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.NO_ACTION_BAR);
|
||||
}
|
||||
|
||||
private void selectPassword() {
|
||||
_textPassword.selectAll();
|
||||
|
||||
@@ -238,9 +240,6 @@ public class AuthActivity extends AegisActivity {
|
||||
PopupWindow popup = new PopupWindow(popupLayout, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
popup.setFocusable(false);
|
||||
popup.setOutsideTouchable(true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
|
||||
popup.setElevation(5.0f);
|
||||
}
|
||||
_textPassword.post(() -> {
|
||||
if (isFinishing()) {
|
||||
return;
|
||||
@@ -285,7 +284,7 @@ public class AuthActivity extends AegisActivity {
|
||||
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
|
||||
|
||||
try {
|
||||
_vaultManager.unlock(creds);
|
||||
_vaultManager.loadFrom(_vaultFile, creds);
|
||||
if (isSlotRepaired) {
|
||||
saveAndBackupVault();
|
||||
}
|
||||
@@ -300,10 +299,11 @@ public class AuthActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
private void onInvalidPassword() {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(AuthActivity.this, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(getString(R.string.unlock_vault_error))
|
||||
.setMessage(getString(R.string.unlock_vault_error_description))
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
|
||||
.create());
|
||||
|
||||
@@ -343,6 +343,7 @@ public class AuthActivity extends AegisActivity {
|
||||
|
||||
finish(result.getKey(), result.isSlotRepaired());
|
||||
} else {
|
||||
_decryptButton.setEnabled(true);
|
||||
onInvalidPassword();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
@@ -15,17 +15,18 @@ import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.avito.android.krop.KropView;
|
||||
@@ -36,7 +37,6 @@ import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SafHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleTextWatcher;
|
||||
@@ -53,18 +53,22 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.YandexInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
|
||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
@@ -72,12 +76,17 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TreeSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -85,12 +94,10 @@ import java.util.stream.Collectors;
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
public class EditEntryActivity extends AegisActivity {
|
||||
private static final int PICK_IMAGE_REQUEST = 0;
|
||||
|
||||
private boolean _isNew = false;
|
||||
private boolean _isManual = false;
|
||||
private VaultEntry _origEntry;
|
||||
private TreeSet<String> _groups;
|
||||
private Collection<VaultGroup> _groups;
|
||||
private boolean _hasCustomIcon = false;
|
||||
// keep track of icon changes separately as the generated jpeg's are not deterministic
|
||||
private boolean _hasChangedIcon = false;
|
||||
@@ -109,12 +116,13 @@ public class EditEntryActivity extends AegisActivity {
|
||||
private LinearLayout _textPinLayout;
|
||||
private TextInputEditText _textUsageCount;
|
||||
private TextInputEditText _textNote;
|
||||
private TextView _textLastUsed;
|
||||
|
||||
private AutoCompleteTextView _dropdownType;
|
||||
private AutoCompleteTextView _dropdownAlgo;
|
||||
private TextInputLayout _dropdownAlgoLayout;
|
||||
private AutoCompleteTextView _dropdownGroup;
|
||||
private List<String> _dropdownGroupList = new ArrayList<>();
|
||||
private List<VaultGroupModel> _dropdownGroupList = new ArrayList<>();
|
||||
|
||||
private KropView _kropView;
|
||||
|
||||
@@ -124,6 +132,29 @@ public class EditEntryActivity extends AegisActivity {
|
||||
private BackPressHandler _backPressHandler;
|
||||
private IconBackPressHandler _iconBackPressHandler;
|
||||
|
||||
private final ActivityResultLauncher<Intent> pickImageResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
Intent data = activityResult.getData();
|
||||
if (activityResult.getResultCode() != RESULT_OK || data == null || data.getData() == null) {
|
||||
return;
|
||||
}
|
||||
String fileType = SafHelper.getMimeType(this, data.getData());
|
||||
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
|
||||
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
|
||||
ImportFileTask task = new ImportFileTask(this, result -> {
|
||||
if (result.getError() == null) {
|
||||
CustomSvgIcon icon = new CustomSvgIcon(result.getFile());
|
||||
selectIcon(icon);
|
||||
} else {
|
||||
Dialogs.showErrorDialog(this, R.string.reading_file_error, result.getError());
|
||||
}
|
||||
});
|
||||
task.execute(getLifecycle(), params);
|
||||
} else {
|
||||
startEditingIcon(data.getData());
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -137,7 +168,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
if (bar != null) {
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_outline_close_24);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
@@ -173,6 +204,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_textPinLayout = findViewById(R.id.layout_pin);
|
||||
_textUsageCount = findViewById(R.id.text_usage_count);
|
||||
_textNote = findViewById(R.id.text_note);
|
||||
_textLastUsed = findViewById(R.id.text_last_used);
|
||||
_dropdownType = findViewById(R.id.dropdown_type);
|
||||
DropdownHelper.fillDropdown(this, _dropdownType, R.array.otp_types_array);
|
||||
_dropdownAlgoLayout = findViewById(R.id.dropdown_algo_layout);
|
||||
@@ -212,19 +244,9 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_advancedSettings = findViewById(R.id.expandableLayout);
|
||||
|
||||
// fill the fields with values if possible
|
||||
GlideHelper.loadEntryIcon(Glide.with(this), _origEntry, _iconView);
|
||||
if (_origEntry.hasIcon()) {
|
||||
IconViewHelper.setLayerType(_iconView, _origEntry.getIconType());
|
||||
Glide.with(this)
|
||||
.asDrawable()
|
||||
.load(_origEntry)
|
||||
.set(IconLoader.ICON_TYPE, _origEntry.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_iconView);
|
||||
_hasCustomIcon = true;
|
||||
} else {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
_textName.setText(_origEntry.getName());
|
||||
@@ -262,8 +284,13 @@ public class EditEntryActivity extends AegisActivity {
|
||||
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
|
||||
updatePinFieldVisibility(_origEntry.getInfo().getTypeId());
|
||||
|
||||
String group = _origEntry.getGroup();
|
||||
setGroup(group);
|
||||
Set<UUID> groups = _origEntry.getGroups();
|
||||
if (groups.isEmpty()) {
|
||||
setGroup(new VaultGroupModel(getString(R.string.no_group)));
|
||||
} else {
|
||||
VaultGroup group = _vaultManager.getVault().getGroupByUUID(groups.iterator().next());
|
||||
setGroup(new VaultGroupModel(group));
|
||||
}
|
||||
|
||||
// Update the icon if the issuer or name has changed
|
||||
_textIssuer.addTextChangedListener(_nameChangeListener);
|
||||
@@ -327,28 +354,36 @@ public class EditEntryActivity extends AegisActivity {
|
||||
startIconSelection();
|
||||
});
|
||||
|
||||
_dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
private int prevPosition = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (position == _dropdownGroupList.size() - 1) {
|
||||
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, text -> {
|
||||
String groupName = new String(text);
|
||||
if (!groupName.isEmpty()) {
|
||||
_groups.add(groupName);
|
||||
updateGroupDropdownList();
|
||||
_dropdownGroup.setText(groupName, false);
|
||||
_dropdownGroup.setOnItemClickListener((parent, view, position, id) -> {
|
||||
VaultGroupModel selectedGroup = _dropdownGroupList.get(position);
|
||||
if (selectedGroup.isPlaceholder() && Objects.equals(selectedGroup.getName(), getString(R.string.new_group))) {
|
||||
Dialogs.TextInputListener onAddGroup = text -> {
|
||||
String groupName = new String(text).trim();
|
||||
if (!groupName.isEmpty()) {
|
||||
VaultGroup group = _vaultManager.getVault().findGroupByName(groupName);
|
||||
if (group == null) {
|
||||
group = new VaultGroup(groupName);
|
||||
_vaultManager.getVault().addGroup(group);
|
||||
}
|
||||
});
|
||||
_dropdownGroup.setText(_dropdownGroupList.get(prevPosition), false);
|
||||
} else {
|
||||
prevPosition = position;
|
||||
}
|
||||
|
||||
updateGroupDropdownList();
|
||||
setGroup(new VaultGroupModel(group));
|
||||
}
|
||||
};
|
||||
|
||||
DialogInterface.OnCancelListener onCancel = dialogInterface -> {
|
||||
VaultGroupModel previous = (VaultGroupModel) _dropdownGroup.getTag();
|
||||
_dropdownGroup.setText(previous.getName(), false);
|
||||
};
|
||||
|
||||
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, onAddGroup, onCancel);
|
||||
} else {
|
||||
setGroup(_dropdownGroupList.get(position));
|
||||
}
|
||||
});
|
||||
|
||||
_textUsageCount.setText(_prefs.getUsageCount(entryUUID).toString());
|
||||
setLastUsedTimestamp(_prefs.getLastUsedTimestamp(entryUUID));
|
||||
}
|
||||
|
||||
private void updateAdvancedFieldStatus(String otpType) {
|
||||
@@ -365,24 +400,20 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin);
|
||||
}
|
||||
|
||||
private void setGroup(String groupName) {
|
||||
int pos = 0;
|
||||
if (groupName != null) {
|
||||
pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() + 1 : 0;
|
||||
}
|
||||
|
||||
_dropdownGroup.setText(_dropdownGroupList.get(pos), false);
|
||||
private void setGroup(VaultGroupModel group) {
|
||||
_dropdownGroup.setText(group.getName(), false);
|
||||
_dropdownGroup.setTag(group);
|
||||
}
|
||||
|
||||
private void openAdvancedSettings() {
|
||||
Animation fadeOut = new AlphaAnimation(1, 0);
|
||||
fadeOut.setInterpolator(new AccelerateInterpolator());
|
||||
fadeOut.setDuration(220 * (long) AnimationsHelper.Scale.ANIMATOR.getValue(this));
|
||||
fadeOut.setDuration((long) (220 * AnimationsHelper.Scale.ANIMATOR.getValue(this)));
|
||||
_advancedSettingsHeader.startAnimation(fadeOut);
|
||||
|
||||
Animation fadeIn = new AlphaAnimation(0, 1);
|
||||
fadeIn.setInterpolator(new AccelerateInterpolator());
|
||||
fadeIn.setDuration(250 * (long) AnimationsHelper.Scale.ANIMATOR.getValue(this));
|
||||
fadeIn.setDuration((long) (250 * AnimationsHelper.Scale.ANIMATOR.getValue(this)));
|
||||
|
||||
fadeOut.setAnimationListener(new SimpleAnimationEndListener((a) -> {
|
||||
_advancedSettingsHeader.setVisibility(View.GONE);
|
||||
@@ -395,11 +426,10 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
private void updateGroupDropdownList() {
|
||||
Resources res = getResources();
|
||||
_dropdownGroupList.clear();
|
||||
_dropdownGroupList.add(res.getString(R.string.no_group));
|
||||
_dropdownGroupList.addAll(_groups);
|
||||
_dropdownGroupList.add(res.getString(R.string.new_group));
|
||||
_dropdownGroupList.add(new VaultGroupModel(getString(R.string.new_group)));
|
||||
_dropdownGroupList.addAll(_groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
|
||||
_dropdownGroupList.add(new VaultGroupModel(getString(R.string.no_group)));
|
||||
}
|
||||
|
||||
private boolean hasUnsavedChanges(VaultEntry newEntry) {
|
||||
@@ -437,38 +467,33 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
discardAndFinish();
|
||||
break;
|
||||
case R.id.action_save:
|
||||
onSave();
|
||||
break;
|
||||
case R.id.action_delete:
|
||||
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry), (dialog, which) -> {
|
||||
deleteAndFinish(_origEntry);
|
||||
});
|
||||
break;
|
||||
case R.id.action_edit_icon:
|
||||
startIconSelection();
|
||||
break;
|
||||
case R.id.action_reset_usage_count:
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.action_reset_usage_count)
|
||||
.setMessage(R.string.action_reset_usage_count_dialog)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> resetUsageCount())
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
break;
|
||||
case R.id.action_default_icon:
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
discardAndFinish();
|
||||
} else if (itemId == R.id.action_save) {
|
||||
onSave();
|
||||
} else if (itemId == R.id.action_delete) {
|
||||
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry), (dialog, which) -> {
|
||||
deleteAndFinish(_origEntry);
|
||||
});
|
||||
} else if (itemId == R.id.action_edit_icon) {
|
||||
startIconSelection();
|
||||
} else if (itemId == R.id.action_reset_usage_count) {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.action_reset_usage_count)
|
||||
.setMessage(R.string.action_reset_usage_count_dialog)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> resetUsageCount())
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
} else if (itemId == R.id.action_default_icon) {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
|
||||
_selectedIcon = null;
|
||||
_hasCustomIcon = false;
|
||||
_hasChangedIcon = true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
_selectedIcon = null;
|
||||
_hasCustomIcon = false;
|
||||
_hasChangedIcon = true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -483,7 +508,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
|
||||
_vaultManager.startActivityForResult(this, chooserIntent, PICK_IMAGE_REQUEST);
|
||||
_vaultManager.fireIntentLauncher(this, chooserIntent, pickImageResultLauncher);
|
||||
}
|
||||
|
||||
private void resetUsageCount() {
|
||||
@@ -500,7 +525,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), new IconAdapter.Listener() {
|
||||
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), true, new IconAdapter.Listener() {
|
||||
@Override
|
||||
public void onIconSelected(IconPack.Icon icon) {
|
||||
selectIcon(icon);
|
||||
@@ -519,14 +544,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
_hasCustomIcon = true;
|
||||
_hasChangedIcon = true;
|
||||
|
||||
IconViewHelper.setLayerType(_iconView, icon.getIconType());
|
||||
Glide.with(EditEntryActivity.this)
|
||||
.asDrawable()
|
||||
.load(icon.getFile())
|
||||
.set(IconLoader.ICON_TYPE, icon.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_iconView);
|
||||
GlideHelper.loadIcon(Glide.with(EditEntryActivity.this), icon, _iconView);
|
||||
}
|
||||
|
||||
private void startEditingIcon(Uri data) {
|
||||
@@ -596,6 +614,16 @@ public class EditEntryActivity extends AegisActivity {
|
||||
saveAndFinish(entry, false);
|
||||
}
|
||||
|
||||
private void setLastUsedTimestamp(long timestamp) {
|
||||
String readableDate = getString(R.string.last_used_never);
|
||||
if (timestamp != 0) {
|
||||
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.getDefault());
|
||||
readableDate = dateFormat.format(new Date(timestamp));
|
||||
}
|
||||
|
||||
_textLastUsed.setText(String.format("%s: %s", getString(R.string.last_used), readableDate));
|
||||
}
|
||||
|
||||
private void deleteAndFinish(VaultEntry entry) {
|
||||
_vaultManager.getVault().removeEntry(entry);
|
||||
saveAndFinish(entry, true);
|
||||
@@ -612,29 +640,6 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
|
||||
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
|
||||
String fileType = SafHelper.getMimeType(this, data.getData());
|
||||
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
|
||||
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
|
||||
ImportFileTask task = new ImportFileTask(this, result -> {
|
||||
if (result.getError() == null) {
|
||||
CustomSvgIcon icon = new CustomSvgIcon(result.getFile());
|
||||
selectIcon(icon);
|
||||
} else {
|
||||
Dialogs.showErrorDialog(this, R.string.reading_file_error, result.getError());
|
||||
}
|
||||
});
|
||||
task.execute(getLifecycle(), params);
|
||||
} else {
|
||||
startEditingIcon(data.getData());
|
||||
}
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
private int parsePeriod() throws ParseException {
|
||||
try {
|
||||
return Integer.parseInt(_textPeriodCounter.getText().toString());
|
||||
@@ -726,23 +731,25 @@ public class EditEntryActivity extends AegisActivity {
|
||||
entry.setName(_textName.getText().toString());
|
||||
entry.setNote(_textNote.getText().toString());
|
||||
|
||||
int groupPos = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
|
||||
if (groupPos != 0) {
|
||||
String group = _dropdownGroupList.get(groupPos);
|
||||
entry.setGroup(group);
|
||||
VaultGroupModel group = (VaultGroupModel) _dropdownGroup.getTag();
|
||||
if (group.isPlaceholder()) {
|
||||
entry.setGroups(new HashSet<>());
|
||||
} else {
|
||||
entry.setGroup(null);
|
||||
Set<UUID> groups = new HashSet<>();
|
||||
groups.add(group.getUUID());
|
||||
entry.setGroups(groups);
|
||||
}
|
||||
|
||||
if (_hasChangedIcon) {
|
||||
if (_hasCustomIcon) {
|
||||
VaultEntryIcon icon;
|
||||
if (_selectedIcon == null) {
|
||||
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
// the quality parameter is ignored for PNG
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
byte[] data = stream.toByteArray();
|
||||
entry.setIcon(data, IconType.PNG);
|
||||
icon = new VaultEntryIcon(data, IconType.PNG);
|
||||
} else {
|
||||
byte[] iconBytes;
|
||||
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
|
||||
@@ -750,11 +757,12 @@ public class EditEntryActivity extends AegisActivity {
|
||||
} catch (IOException e) {
|
||||
throw new ParseException(e.getMessage());
|
||||
}
|
||||
|
||||
entry.setIcon(iconBytes, _selectedIcon.getIconType());
|
||||
icon = new VaultEntryIcon(iconBytes, _selectedIcon.getIconType());
|
||||
}
|
||||
|
||||
entry.setIcon(icon);
|
||||
} else {
|
||||
entry.setIcon(null, IconType.INVALID);
|
||||
entry.setIcon(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -762,9 +770,10 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
private void onSaveError(String msg) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(getString(R.string.saving_profile_error))
|
||||
.setMessage(msg)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create());
|
||||
}
|
||||
@@ -852,11 +861,12 @@ public class EditEntryActivity extends AegisActivity {
|
||||
private final File _file;
|
||||
|
||||
protected CustomSvgIcon(File file) {
|
||||
super(file.getAbsolutePath(), null, null);
|
||||
super(file.getAbsolutePath(), null, null, null);
|
||||
_file = file;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public File getFile() {
|
||||
return _file;
|
||||
}
|
||||
|
||||
@@ -7,24 +7,25 @@ import android.view.View;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
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 com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
|
||||
private GroupAdapter _adapter;
|
||||
private HashSet<String> _removedGroups;
|
||||
private RecyclerView _slotsView;
|
||||
private HashSet<UUID> _removedGroups;
|
||||
private RecyclerView _groupsView;
|
||||
private View _emptyStateView;
|
||||
private BackPressHandler _backPressHandler;
|
||||
|
||||
@@ -43,22 +44,27 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
_backPressHandler = new BackPressHandler();
|
||||
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
|
||||
|
||||
_removedGroups = new HashSet<>();
|
||||
if (savedInstanceState != null) {
|
||||
List<String> groups = savedInstanceState.getStringArrayList("removedGroups");
|
||||
_removedGroups = new HashSet<>(Objects.requireNonNull(groups));
|
||||
} else {
|
||||
_removedGroups = new HashSet<>();
|
||||
if (groups != null) {
|
||||
for (String uuid : groups) {
|
||||
_removedGroups.add(UUID.fromString(uuid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_adapter = new GroupAdapter(this);
|
||||
_slotsView= findViewById(R.id.list_slots);
|
||||
_groupsView = findViewById(R.id.list_groups);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
_slotsView.setLayoutManager(layoutManager);
|
||||
_slotsView.setAdapter(_adapter);
|
||||
_slotsView.setNestedScrollingEnabled(false);
|
||||
_groupsView.setLayoutManager(layoutManager);
|
||||
_groupsView.setAdapter(_adapter);
|
||||
_groupsView.setNestedScrollingEnabled(false);
|
||||
|
||||
for (String group : _vaultManager.getVault().getGroups()) {
|
||||
_adapter.addGroup(group);
|
||||
for (VaultGroup group : _vaultManager.getVault().getGroups()) {
|
||||
if (!_removedGroups.contains(group.getUUID())) {
|
||||
_adapter.addGroup(group);
|
||||
}
|
||||
}
|
||||
|
||||
_emptyStateView = findViewById(R.id.vEmptyList);
|
||||
@@ -68,16 +74,22 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putStringArrayList("removedGroups", new ArrayList<>(_removedGroups));
|
||||
ArrayList<String> removed = new ArrayList<>();
|
||||
for (UUID uuid : _removedGroups) {
|
||||
removed.add(uuid.toString());
|
||||
}
|
||||
|
||||
outState.putStringArrayList("removedGroups", removed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveGroup(String group) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
public void onRemoveGroup(VaultGroup group) {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.remove_group)
|
||||
.setMessage(R.string.remove_group_description)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
_removedGroups.add(group);
|
||||
_removedGroups.add(group.getUUID());
|
||||
_adapter.removeGroup(group);
|
||||
_backPressHandler.setEnabled(true);
|
||||
updateEmptyState();
|
||||
@@ -86,12 +98,30 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
.create());
|
||||
}
|
||||
|
||||
public void onRemoveUnusedGroups() {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.remove_unused_groups)
|
||||
.setMessage(R.string.remove_unused_groups_description)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
Set<VaultGroup> unusedGroups = new HashSet<>(_vaultManager.getVault().getGroups());
|
||||
unusedGroups.removeAll(_vaultManager.getVault().getUsedGroups());
|
||||
|
||||
for (VaultGroup group : unusedGroups) {
|
||||
_removedGroups.add(group.getUUID());
|
||||
_adapter.removeGroup(group);
|
||||
}
|
||||
_backPressHandler.setEnabled(true);
|
||||
updateEmptyState();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
}
|
||||
|
||||
private void saveAndFinish() {
|
||||
if (!_removedGroups.isEmpty()) {
|
||||
for (VaultEntry entry : _vaultManager.getVault().getEntries()) {
|
||||
if (_removedGroups.contains(entry.getGroup())) {
|
||||
entry.setGroup(null);
|
||||
}
|
||||
for (UUID uuid : _removedGroups) {
|
||||
_vaultManager.getVault().removeGroup(uuid);
|
||||
}
|
||||
|
||||
saveAndBackupVault();
|
||||
@@ -119,15 +149,15 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
discardAndFinish();
|
||||
break;
|
||||
case R.id.action_save:
|
||||
saveAndFinish();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
discardAndFinish();
|
||||
} else if (itemId == R.id.action_save) {
|
||||
saveAndFinish();
|
||||
} else if (itemId == R.id.action_delete_unused_groups) {
|
||||
onRemoveUnusedGroups();
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -135,10 +165,10 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
|
||||
private void updateEmptyState() {
|
||||
if (_adapter.getItemCount() > 0) {
|
||||
_slotsView.setVisibility(View.VISIBLE);
|
||||
_groupsView.setVisibility(View.VISIBLE);
|
||||
_emptyStateView.setVisibility(View.GONE);
|
||||
} else {
|
||||
_slotsView.setVisibility(View.GONE);
|
||||
_groupsView.setVisibility(View.GONE);
|
||||
_emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -24,21 +25,31 @@ import com.beemdevelopment.aegis.ui.tasks.RootShellTask;
|
||||
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ImportEntriesActivity extends AegisActivity {
|
||||
private View _view;
|
||||
private Menu _menu;
|
||||
private ImportEntriesAdapter _adapter;
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
private UUIDMap<VaultGroup> _importedGroups;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -48,8 +59,10 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
setContentView(R.layout.activity_import_entries);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
_view = findViewById(R.id.importEntriesRootView);
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_outline_close_24);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
_adapter = new ImportEntriesAdapter();
|
||||
@@ -88,10 +101,11 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
if (importer.isInstalledAppVersionSupported()) {
|
||||
startImportApp(importer);
|
||||
} else {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(getString(R.string.app_version_error, importerDef.getName()))
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(R.string.yes, (dialog1, which) -> {
|
||||
startImportApp(importer);
|
||||
})
|
||||
@@ -180,6 +194,7 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
private void importDatabase(DatabaseImporter.State state) {
|
||||
List<ImportEntry> importEntries = new ArrayList<>();
|
||||
DatabaseImporter.Result result;
|
||||
try {
|
||||
result = state.convert();
|
||||
@@ -191,14 +206,20 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
|
||||
UUIDMap<VaultEntry> entries = result.getEntries();
|
||||
for (VaultEntry entry : entries.getValues()) {
|
||||
_adapter.addEntry(new ImportEntry(entry));
|
||||
ImportEntry importEntry = new ImportEntry(entry);
|
||||
_adapter.addEntry(importEntry);
|
||||
importEntries.add(importEntry);
|
||||
}
|
||||
|
||||
_importedGroups = result.getGroups();
|
||||
|
||||
List<DatabaseImporterEntryException> errors = result.getErrors();
|
||||
if (errors.size() > 0) {
|
||||
String message = getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size());
|
||||
Dialogs.showMultiErrorDialog(this, R.string.import_error_title, message, errors, null);
|
||||
Dialogs.showMultiExceptionDialog(this, R.string.import_error_title, message, errors, null);
|
||||
}
|
||||
|
||||
findDuplicates(importEntries);
|
||||
}
|
||||
|
||||
private void showWipeEntriesDialog() {
|
||||
@@ -212,10 +233,43 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
private void saveAndFinish(boolean wipeEntries) {
|
||||
VaultRepository vault = _vaultManager.getVault();
|
||||
if (wipeEntries) {
|
||||
vault.wipeEntries();
|
||||
vault.wipeContents();
|
||||
}
|
||||
|
||||
// Given the list of selected entries, collect the UUID's of all groups
|
||||
// that we're actually going to import
|
||||
List<ImportEntry> selectedEntries = _adapter.getCheckedEntries();
|
||||
List<UUID> selectedGroupUuids = new ArrayList<>();
|
||||
for (ImportEntry entry : selectedEntries) {
|
||||
selectedGroupUuids.addAll(entry.getEntry().getGroups());
|
||||
}
|
||||
|
||||
// Add all of the new groups to the vault. If a group with the same name already
|
||||
// exists in the vault, rewrite all entries in that group to reference the existing group.
|
||||
for (VaultGroup importedGroup : _importedGroups) {
|
||||
if (!selectedGroupUuids.contains(importedGroup.getUUID())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
VaultGroup existingGroup = vault.findGroupByUUID(importedGroup.getUUID());
|
||||
if (existingGroup != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
existingGroup = vault.findGroupByName(importedGroup.getName());
|
||||
if (existingGroup == null) {
|
||||
vault.addGroup(importedGroup);
|
||||
} else {
|
||||
for (ImportEntry entry : selectedEntries) {
|
||||
Set<UUID> entryGroups = entry.getEntry().getGroups();
|
||||
if (entryGroups.contains(importedGroup.getUUID())) {
|
||||
entryGroups.remove(importedGroup.getUUID());
|
||||
entryGroups.add(existingGroup.getUUID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ImportEntry selectedEntry : selectedEntries) {
|
||||
VaultEntry entry = selectedEntry.getEntry();
|
||||
|
||||
@@ -231,11 +285,60 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size());
|
||||
Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show();
|
||||
|
||||
|
||||
setResult(RESULT_OK, null);
|
||||
finish();
|
||||
|
||||
if (_iconPackManager.hasIconPack()) {
|
||||
ArrayList<UUID> assignIconEntriesIds = new ArrayList<>();
|
||||
Intent assignIconIntent = new Intent(getBaseContext(), AssignIconsActivity.class);
|
||||
for (ImportEntry entry : selectedEntries) {
|
||||
assignIconEntriesIds.add(entry.getEntry().getUUID());
|
||||
}
|
||||
|
||||
assignIconIntent.putExtra("entries", assignIconEntriesIds);
|
||||
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.import_assign_icons_dialog_title)
|
||||
.setMessage(R.string.import_assign_icons_dialog_text)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
startActivity(assignIconIntent);
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, ((dialogInterface, i) -> finish()))
|
||||
.create());
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void findDuplicates(List<ImportEntry> importEntries) {
|
||||
List<UUID> duplicateEntries = new ArrayList<>();
|
||||
for (ImportEntry importEntry: importEntries) {
|
||||
boolean exists = _vaultManager.getVault().getEntries().stream().anyMatch(item ->
|
||||
item.getIssuer().equals(importEntry.getEntry().getIssuer()) &&
|
||||
Arrays.equals(item.getInfo().getSecret(), importEntry.getEntry().getInfo().getSecret()));
|
||||
|
||||
if (exists) {
|
||||
duplicateEntries.add(importEntry.getEntry().getUUID());
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicateEntries.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
_adapter.setCheckboxStates(duplicateEntries, false);
|
||||
Snackbar snackbar = Snackbar.make(_view, getResources().getQuantityString(R.plurals.import_duplicate_toast, duplicateEntries.size(), duplicateEntries.size()), Snackbar.LENGTH_INDEFINITE);
|
||||
snackbar.setAction(R.string.undo, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
_adapter.setCheckboxStates(duplicateEntries, true);
|
||||
}
|
||||
});
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
_menu = menu;
|
||||
@@ -245,18 +348,15 @@ public class ImportEntriesActivity extends AegisActivity {
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
break;
|
||||
case R.id.toggle_checkboxes:
|
||||
_adapter.toggleCheckboxes();
|
||||
break;
|
||||
case R.id.toggle_wipe_vault:
|
||||
item.setChecked(!item.isChecked());
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
finish();
|
||||
} else if (itemId == R.id.toggle_checkboxes) {
|
||||
_adapter.toggleCheckboxes();
|
||||
} else if (itemId == R.id.toggle_wipe_vault) {
|
||||
item.setChecked(!item.isChecked());
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.intro.IntroBaseActivity;
|
||||
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
|
||||
@@ -21,7 +20,9 @@ import com.beemdevelopment.aegis.ui.slides.DoneSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.WelcomeSlide;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
@@ -40,11 +41,6 @@ public class IntroActivity extends IntroBaseActivity {
|
||||
addSlide(DoneSlide.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.NO_ACTION_BAR);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, @NonNull Class<? extends SlideFragment> newSlide) {
|
||||
// hide the keyboard before every slide change
|
||||
@@ -108,8 +104,17 @@ public class IntroActivity extends IntroBaseActivity {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
VaultFile vaultFile;
|
||||
try {
|
||||
_vaultManager.load(creds);
|
||||
vaultFile = VaultRepository.readVaultFile(this);
|
||||
} catch (VaultRepositoryException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_vaultManager.loadFrom(vaultFile, creds);
|
||||
} catch (VaultRepositoryException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder;
|
||||
import com.mikepenz.aboutlibraries.ui.LibsActivity;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import dagger.hilt.InstallIn;
|
||||
import dagger.hilt.android.EarlyEntryPoint;
|
||||
import dagger.hilt.android.EarlyEntryPoints;
|
||||
import dagger.hilt.components.SingletonComponent;
|
||||
|
||||
public class LicensesActivity extends LibsActivity {
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
LibsBuilder builder = new LibsBuilder()
|
||||
.withSearchEnabled(true)
|
||||
.withAboutMinimalDesign(true)
|
||||
.withActivityTitle(getString(R.string.title_activity_licenses));
|
||||
setIntent(builder.intent(this));
|
||||
|
||||
Preferences _prefs = EarlyEntryPoints.get(getApplicationContext(), PrefEntryPoint.class).getPreferences();
|
||||
ThemeHelper themeHelper = new ThemeHelper(this, _prefs);
|
||||
themeHelper.setTheme(ThemeMap.DEFAULT);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@EarlyEntryPoint
|
||||
@InstallIn(SingletonComponent.class)
|
||||
public interface PrefEntryPoint {
|
||||
Preferences getPreferences();
|
||||
}
|
||||
}
|
||||
@@ -25,18 +25,18 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
|
||||
import com.beemdevelopment.aegis.CopyBehavior;
|
||||
import com.beemdevelopment.aegis.AccountNamePosition;
|
||||
import com.beemdevelopment.aegis.CopyBehavior;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
@@ -49,11 +49,16 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
|
||||
import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryListView;
|
||||
import com.beemdevelopment.aegis.util.TimeUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
@@ -63,20 +68,11 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MainActivity extends AegisActivity implements EntryListView.Listener {
|
||||
// activity request codes
|
||||
private static final int CODE_SCAN = 0;
|
||||
private static final int CODE_ADD_ENTRY = 1;
|
||||
private static final int CODE_EDIT_ENTRY = 2;
|
||||
private static final int CODE_DO_INTRO = 3;
|
||||
private static final int CODE_DECRYPT = 4;
|
||||
private static final int CODE_PREFERENCES = 5;
|
||||
private static final int CODE_SCAN_IMAGE = 6;
|
||||
|
||||
// Permission request codes
|
||||
private static final int CODE_PERM_CAMERA = 0;
|
||||
|
||||
@@ -94,8 +90,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
private Menu _menu;
|
||||
private SearchView _searchView;
|
||||
private EntryListView _entryListView;
|
||||
private LinearLayout _btnErrorBar;
|
||||
private TextView _textErrorBar;
|
||||
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
@@ -106,6 +100,69 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
private SearchViewBackPressHandler _searchViewBackPressHandler;
|
||||
private ActionModeBackPressHandler _actionModeBackPressHandler;
|
||||
|
||||
private final ActivityResultLauncher<Intent> authResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
_isAuthenticating = false;
|
||||
if (activityResult.getResultCode() == RESULT_OK) {
|
||||
onDecryptResult();
|
||||
}
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> introResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
_isDoingIntro = false;
|
||||
if (activityResult.getResultCode() == RESULT_OK) {
|
||||
onIntroResult();
|
||||
}
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> scanResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||
return;
|
||||
}
|
||||
onScanResult(activityResult.getData());
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> assignIconsResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||
return;
|
||||
}
|
||||
onAssignEntriesResult(activityResult.getData());
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> preferenceResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||
return;
|
||||
}
|
||||
onPreferencesResult(activityResult.getData());
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> editEntryResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||
return;
|
||||
}
|
||||
onEditEntryResult(activityResult.getData());
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> addEntryResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||
return;
|
||||
}
|
||||
onAddEntryResult(activityResult.getData());
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> codeScanResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
if (activityResult.getResultCode() == RESULT_OK && activityResult.getData() != null) {
|
||||
onScanImageResult(activityResult.getData());
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -153,7 +210,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
view.findViewById(R.id.fab_enter).setOnClickListener(v1 -> {
|
||||
dialog.dismiss();
|
||||
startEditEntryActivityForManual(CODE_ADD_ENTRY);
|
||||
startEditEntryActivityForManual();
|
||||
});
|
||||
view.findViewById(R.id.fab_scan_image).setOnClickListener(v2 -> {
|
||||
dialog.dismiss();
|
||||
@@ -167,9 +224,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
});
|
||||
|
||||
_btnErrorBar = findViewById(R.id.btn_error_bar);
|
||||
_textErrorBar = findViewById(R.id.text_error_bar);
|
||||
|
||||
_fabScrollHelper = new FabScrollHelper(fab);
|
||||
_selectedEntries = new ArrayList<>();
|
||||
}
|
||||
@@ -187,6 +241,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_prefs.setUsageCount(usageMap);
|
||||
}
|
||||
|
||||
Map<UUID, Long> lastUsedMap = _entryListView.getLastUsedTimestamps();
|
||||
if (lastUsedMap != null) {
|
||||
_prefs.setLastUsedTimestamps(lastUsedMap);
|
||||
}
|
||||
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@@ -199,44 +258,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
instance.putBoolean("isAuthenticating", _isAuthenticating);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == CODE_DECRYPT) {
|
||||
_isAuthenticating = false;
|
||||
}
|
||||
if (requestCode == CODE_DO_INTRO) {
|
||||
_isDoingIntro = false;
|
||||
}
|
||||
if (resultCode != RESULT_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case CODE_SCAN:
|
||||
onScanResult(data);
|
||||
break;
|
||||
case CODE_ADD_ENTRY:
|
||||
onAddEntryResult(data);
|
||||
break;
|
||||
case CODE_EDIT_ENTRY:
|
||||
onEditEntryResult(data);
|
||||
break;
|
||||
case CODE_DO_INTRO:
|
||||
onIntroResult();
|
||||
break;
|
||||
case CODE_DECRYPT:
|
||||
onDecryptResult();
|
||||
break;
|
||||
case CODE_PREFERENCES:
|
||||
onPreferencesResult(data);
|
||||
break;
|
||||
case CODE_SCAN_IMAGE:
|
||||
onScanImageResult(data);
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
if (!PermissionHelper.checkResults(grantResults)) {
|
||||
@@ -297,30 +318,41 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private void startEditEntryActivityForNew(int requestCode, VaultEntry entry) {
|
||||
private void startEditEntryActivityForNew(VaultEntry entry) {
|
||||
Intent intent = new Intent(this, EditEntryActivity.class);
|
||||
intent.putExtra("newEntry", entry);
|
||||
intent.putExtra("isManual", false);
|
||||
startActivityForResult(intent, requestCode);
|
||||
addEntryResultLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private void startEditEntryActivityForManual(int requestCode) {
|
||||
private void startEditEntryActivityForManual() {
|
||||
Intent intent = new Intent(this, EditEntryActivity.class);
|
||||
intent.putExtra("newEntry", VaultEntry.getDefault());
|
||||
intent.putExtra("isManual", true);
|
||||
startActivityForResult(intent, requestCode);
|
||||
addEntryResultLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private void startEditEntryActivity(int requestCode, VaultEntry entry) {
|
||||
private void startEditEntryActivity(VaultEntry entry) {
|
||||
Intent intent = new Intent(this, EditEntryActivity.class);
|
||||
intent.putExtra("entryUUID", entry.getUUID());
|
||||
startActivityForResult(intent, requestCode);
|
||||
editEntryResultLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private void startAssignIconsActivity(List<VaultEntry> entries) {
|
||||
ArrayList<UUID> assignIconEntriesIds = new ArrayList<>();
|
||||
Intent assignIconIntent = new Intent(getBaseContext(), AssignIconsActivity.class);
|
||||
for (VaultEntry entry : entries) {
|
||||
assignIconEntriesIds.add(entry.getUUID());
|
||||
}
|
||||
|
||||
assignIconIntent.putExtra("entries", assignIconEntriesIds);
|
||||
assignIconsResultLauncher.launch(assignIconIntent);
|
||||
}
|
||||
|
||||
private void startIntroActivity() {
|
||||
if (!_isDoingIntro) {
|
||||
Intent intro = new Intent(this, IntroActivity.class);
|
||||
startActivityForResult(intro, CODE_DO_INTRO);
|
||||
introResultLauncher.launch(intro);
|
||||
_isDoingIntro = true;
|
||||
}
|
||||
}
|
||||
@@ -353,6 +385,17 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private void onAssignEntriesResult(Intent data) {
|
||||
if (_loaded) {
|
||||
ArrayList<UUID> entryUUIDs = (ArrayList<UUID>) data.getSerializableExtra("entryUUIDs");
|
||||
|
||||
for (UUID entryUUID: entryUUIDs) {
|
||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
|
||||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onScanImageResult(Intent intent) {
|
||||
if (intent.getData() != null) {
|
||||
startDecodeQrCodeImages(Collections.singletonList(intent.getData()));
|
||||
@@ -418,7 +461,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
boolean isSingleBatch = GoogleAuthInfo.Export.isSingleBatch(googleAuthExports);
|
||||
if (!isSingleBatch && errors.size() > 0) {
|
||||
errors.add(getString(R.string.unrelated_google_auth_batches_error));
|
||||
Dialogs.showMultiMessageDialog(this, R.string.import_error_title, getString(R.string.no_tokens_can_be_imported), errors, null);
|
||||
Dialogs.showMultiErrorDialog(this, R.string.import_error_title, getString(R.string.no_tokens_can_be_imported), errors, null);
|
||||
return;
|
||||
} else if (!isSingleBatch) {
|
||||
Dialogs.showErrorDialog(this, R.string.import_google_auth_failure, getString(R.string.unrelated_google_auth_batches_error));
|
||||
@@ -433,7 +476,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
if ((errors.size() > 0 && results.size() > 1) || errors.size() > 1) {
|
||||
Dialogs.showMultiMessageDialog(this, R.string.import_error_title, getString(R.string.unable_to_read_qrcode_files, uris.size() - errors.size(), uris.size()), errors, dialogDismissHandler);
|
||||
Dialogs.showMultiErrorDialog(this, R.string.import_error_title, getString(R.string.unable_to_read_qrcode_files, uris.size() - errors.size(), uris.size()), errors, dialogDismissHandler);
|
||||
} else if (errors.size() > 0) {
|
||||
Dialogs.showErrorDialog(this, getString(R.string.unable_to_read_qrcode_file, results.get(0).getFileName()), errors.get(0), dialogDismissHandler);
|
||||
} else {
|
||||
@@ -445,7 +488,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
private void importScannedEntries(List<VaultEntry> entries) {
|
||||
if (entries.size() == 1) {
|
||||
startEditEntryActivityForNew(CODE_ADD_ENTRY, entries.get(0));
|
||||
startEditEntryActivityForNew(entries.get(0));
|
||||
} else if (entries.size() > 1) {
|
||||
for (VaultEntry entry: entries) {
|
||||
_vaultManager.getVault().addEntry(entry);
|
||||
@@ -489,7 +532,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
|
||||
startActivityForResult(scannerActivity, CODE_SCAN);
|
||||
scanResultLauncher.launch(scannerActivity);
|
||||
}
|
||||
|
||||
private void startScanImageActivity() {
|
||||
@@ -503,7 +546,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
|
||||
_vaultManager.startActivityForResult(this, chooserIntent, CODE_SCAN_IMAGE);
|
||||
_vaultManager.fireIntentLauncher(this, chooserIntent, codeScanResultLauncher);
|
||||
}
|
||||
|
||||
private void startPreferencesActivity() {
|
||||
@@ -514,7 +557,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
Intent intent = new Intent(this, PreferencesActivity.class);
|
||||
intent.putExtra("fragment", fragmentType);
|
||||
intent.putExtra("pref", preference);
|
||||
startActivityForResult(intent, CODE_PREFERENCES);
|
||||
preferenceResultLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private void doShortcutActions() {
|
||||
@@ -561,7 +604,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
|
||||
startEditEntryActivityForNew(entry);
|
||||
}
|
||||
break;
|
||||
case Intent.ACTION_SEND:
|
||||
@@ -589,7 +632,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
|
||||
startEditEntryActivityForNew(entry);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -623,20 +666,43 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_vaultManager.isVaultLoaded() && !_vaultManager.isVaultFileLoaded()) {
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, _vaultManager.getVaultFileError(), (dialog1, which) -> finish());
|
||||
return;
|
||||
// If the vault is not loaded yet, try to load it now in case it's plain text
|
||||
if (!_vaultManager.isVaultLoaded()) {
|
||||
VaultFile vaultFile;
|
||||
try {
|
||||
vaultFile = VaultRepository.readVaultFile(this);
|
||||
} catch (VaultRepositoryException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
|
||||
finish();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vaultFile.isEncrypted()) {
|
||||
try {
|
||||
_vaultManager.loadFrom(vaultFile);
|
||||
} catch (VaultRepositoryException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
|
||||
finish();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_vaultManager.isVaultLoaded()) {
|
||||
startAuthActivity(false);
|
||||
} else if (_loaded) {
|
||||
// update the list of groups in the entry list view so that the chip gets updated
|
||||
_entryListView.setGroups(_vaultManager.getVault().getGroups());
|
||||
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
|
||||
// update the usage counts in case they are edited outside of the EntryListView
|
||||
_entryListView.setUsageCounts(_prefs.getUsageCounts());
|
||||
|
||||
_entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps());
|
||||
|
||||
// refresh all codes to prevent showing old ones
|
||||
_entryListView.refresh(false);
|
||||
} else {
|
||||
@@ -651,7 +717,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
handleIncomingIntent();
|
||||
updateLockIcon();
|
||||
doShortcutActions();
|
||||
updateErrorBar();
|
||||
updateErrorCard();
|
||||
}
|
||||
|
||||
private void deleteEntries(List<VaultEntry> entries) {
|
||||
@@ -670,7 +736,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
updateLockIcon();
|
||||
if (_loaded) {
|
||||
_entryListView.setGroups(_vaultManager.getVault().getGroups());
|
||||
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
updateSortCategoryMenu();
|
||||
}
|
||||
|
||||
@@ -742,51 +808,44 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings: {
|
||||
startPreferencesActivity();
|
||||
return true;
|
||||
}
|
||||
case R.id.action_about: {
|
||||
Intent intent = new Intent(this, AboutActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_lock:
|
||||
_vaultManager.lock(true);
|
||||
return true;
|
||||
default:
|
||||
if (item.getGroupId() == R.id.action_sort_category) {
|
||||
item.setChecked(true);
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_settings) {
|
||||
startPreferencesActivity();
|
||||
} else if (itemId == R.id.action_about) {
|
||||
Intent intent = new Intent(this, AboutActivity.class);
|
||||
startActivity(intent);
|
||||
} else if (itemId == R.id.action_lock) {
|
||||
_vaultManager.lock(true);
|
||||
} else {
|
||||
if (item.getGroupId() == R.id.action_sort_category) {
|
||||
item.setChecked(true);
|
||||
|
||||
SortCategory sortCategory;
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_sort_alphabetically:
|
||||
sortCategory = SortCategory.ISSUER;
|
||||
break;
|
||||
case R.id.menu_sort_alphabetically_reverse:
|
||||
sortCategory = SortCategory.ISSUER_REVERSED;
|
||||
break;
|
||||
case R.id.menu_sort_alphabetically_name:
|
||||
sortCategory = SortCategory.ACCOUNT;
|
||||
break;
|
||||
case R.id.menu_sort_alphabetically_name_reverse:
|
||||
sortCategory = SortCategory.ACCOUNT_REVERSED;
|
||||
break;
|
||||
case R.id.menu_sort_usage_count:
|
||||
sortCategory = SortCategory.USAGE_COUNT;
|
||||
break;
|
||||
case R.id.menu_sort_custom:
|
||||
default:
|
||||
sortCategory = SortCategory.CUSTOM;
|
||||
break;
|
||||
}
|
||||
|
||||
_entryListView.setSortCategory(sortCategory, true);
|
||||
_prefs.setCurrentSortCategory(sortCategory);
|
||||
SortCategory sortCategory;
|
||||
int subItemId = item.getItemId();
|
||||
if (subItemId == R.id.menu_sort_alphabetically) {
|
||||
sortCategory = SortCategory.ISSUER;
|
||||
} else if (subItemId == R.id.menu_sort_alphabetically_reverse) {
|
||||
sortCategory = SortCategory.ISSUER_REVERSED;
|
||||
} else if (subItemId == R.id.menu_sort_alphabetically_name) {
|
||||
sortCategory = SortCategory.ACCOUNT;
|
||||
} else if (subItemId == R.id.menu_sort_alphabetically_name_reverse) {
|
||||
sortCategory = SortCategory.ACCOUNT_REVERSED;
|
||||
} else if (subItemId == R.id.menu_sort_usage_count) {
|
||||
sortCategory = SortCategory.USAGE_COUNT;
|
||||
} else if (subItemId == R.id.menu_sort_last_used) {
|
||||
sortCategory = SortCategory.LAST_USED;
|
||||
} else {
|
||||
sortCategory = SortCategory.CUSTOM;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
||||
_entryListView.setSortCategory(sortCategory, true);
|
||||
_prefs.setCurrentSortCategory(sortCategory);
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void collapseSearchView() {
|
||||
@@ -797,6 +856,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
private void loadEntries() {
|
||||
if (!_loaded) {
|
||||
_entryListView.setUsageCounts(_prefs.getUsageCounts());
|
||||
_entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps());
|
||||
_entryListView.addEntries(_vaultManager.getVault().getEntries());
|
||||
_entryListView.runEntriesAnimation();
|
||||
_loaded = true;
|
||||
@@ -807,7 +867,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
if (!_isAuthenticating) {
|
||||
Intent intent = new Intent(this, AuthActivity.class);
|
||||
intent.putExtra("inhibitBioPrompt", inhibitBioPrompt);
|
||||
startActivityForResult(intent, CODE_DECRYPT);
|
||||
authResultLauncher.launch(intent);
|
||||
_isAuthenticating = true;
|
||||
}
|
||||
}
|
||||
@@ -820,49 +880,49 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private void updateErrorBar() {
|
||||
Preferences.BackupResult backupRes = _prefs.getErroredBackupResult();
|
||||
if (backupRes != null) {
|
||||
_textErrorBar.setText(R.string.backup_error_bar_message);
|
||||
_btnErrorBar.setOnClickListener(view -> {
|
||||
Dialogs.showBackupErrorDialog(this, backupRes, (dialog, which) -> {
|
||||
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
|
||||
});
|
||||
});
|
||||
_btnErrorBar.setVisibility(View.VISIBLE);
|
||||
} else if (_prefs.isBackupsReminderNeeded() && _prefs.isBackupReminderEnabled()) {
|
||||
Date date = _prefs.getLatestBackupOrExportTime();
|
||||
if (date != null) {
|
||||
_textErrorBar.setText(getString(R.string.backup_reminder_bar_message_with_latest, TimeUtils.getElapsedSince(this, date)));
|
||||
} else {
|
||||
_textErrorBar.setText(R.string.backup_reminder_bar_message);
|
||||
}
|
||||
_btnErrorBar.setOnClickListener(view -> {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.backup_reminder_bar_dialog_title)
|
||||
.setMessage(R.string.backup_reminder_bar_dialog_summary)
|
||||
.setPositiveButton(R.string.backup_reminder_bar_dialog_accept, (dialog, whichButton) -> {
|
||||
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create());
|
||||
});
|
||||
_btnErrorBar.setVisibility(View.VISIBLE);
|
||||
} else if (_prefs.isPlaintextBackupWarningNeeded()) {
|
||||
_textErrorBar.setText(R.string.backup_plaintext_export_warning);
|
||||
_btnErrorBar.setOnClickListener(view -> showPlaintextExportWarningOptions());
|
||||
_btnErrorBar.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
_btnErrorBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
private void updateErrorCard() {
|
||||
ErrorCardInfo info = null;
|
||||
|
||||
Preferences.BackupResult backupRes = _prefs.getErroredBackupResult();
|
||||
if (backupRes != null) {
|
||||
info = new ErrorCardInfo(getString(R.string.backup_error_bar_message), view -> {
|
||||
Dialogs.showBackupErrorDialog(this, backupRes, (dialog, which) -> {
|
||||
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
|
||||
});
|
||||
});
|
||||
} else if (_prefs.isBackupsReminderNeeded() && _prefs.isBackupReminderEnabled()) {
|
||||
String text;
|
||||
Date date = _prefs.getLatestBackupOrExportTime();
|
||||
if (date != null) {
|
||||
text = getString(R.string.backup_reminder_bar_message_with_latest, TimeUtils.getElapsedSince(this, date));
|
||||
} else {
|
||||
text = getString(R.string.backup_reminder_bar_message);
|
||||
}
|
||||
info = new ErrorCardInfo(text, view -> {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(R.string.backup_reminder_bar_dialog_title)
|
||||
.setMessage(R.string.backup_reminder_bar_dialog_summary)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(R.string.backup_reminder_bar_dialog_accept, (dialog, whichButton) -> {
|
||||
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create());
|
||||
});
|
||||
} else if (_prefs.isPlaintextBackupWarningNeeded()) {
|
||||
info = new ErrorCardInfo(getString(R.string.backup_plaintext_export_warning), view -> showPlaintextExportWarningOptions());
|
||||
}
|
||||
|
||||
_entryListView.setErrorCardInfo(info);
|
||||
}
|
||||
|
||||
private void showPlaintextExportWarningOptions() {
|
||||
View view = LayoutInflater.from(this).inflate(R.layout.dialog_plaintext_warning, null);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.backup_plaintext_export_warning)
|
||||
.setView(view)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
@@ -879,7 +939,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_prefs.setIsPlaintextBackupWarningDisabled(checkBox.isChecked());
|
||||
_prefs.setIsPlaintextBackupWarningNeeded(false);
|
||||
|
||||
updateErrorBar();
|
||||
updateErrorCard();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -895,8 +955,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
setFavoriteMenuItemVisiblity();
|
||||
setIsMultipleSelected(_selectedEntries.size() > 1);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -916,19 +974,24 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_actionMode.getMenu().findItem(R.id.action_copy).setVisible(!multipleSelected);
|
||||
}
|
||||
|
||||
private void setAssignIconsMenuItemVisibility() {
|
||||
MenuItem assignIconsMenuItem = _actionMode.getMenu().findItem(R.id.action_assign_icons);
|
||||
assignIconsMenuItem.setVisible(_iconPackManager.hasIconPack());
|
||||
}
|
||||
|
||||
private void setFavoriteMenuItemVisiblity() {
|
||||
MenuItem toggleFavoriteMenuItem = _actionMode.getMenu().findItem(R.id.action_toggle_favorite);
|
||||
|
||||
if (_selectedEntries.size() == 1){
|
||||
if (_selectedEntries.get(0).isFavorite()) {
|
||||
toggleFavoriteMenuItem.setIcon(R.drawable.ic_set_favorite);
|
||||
toggleFavoriteMenuItem.setIcon(R.drawable.ic_filled_star_24);
|
||||
toggleFavoriteMenuItem.setTitle(R.string.unfavorite);
|
||||
} else {
|
||||
toggleFavoriteMenuItem.setIcon(R.drawable.ic_unset_favorite);
|
||||
toggleFavoriteMenuItem.setIcon(R.drawable.ic_outline_star_24);
|
||||
toggleFavoriteMenuItem.setTitle(R.string.favorite);
|
||||
}
|
||||
} else {
|
||||
toggleFavoriteMenuItem.setIcon(R.drawable.ic_unset_favorite);
|
||||
toggleFavoriteMenuItem.setIcon(R.drawable.ic_outline_star_24);
|
||||
toggleFavoriteMenuItem.setTitle(String.format("%s / %s", getString(R.string.favorite), getString(R.string.unfavorite)));
|
||||
}
|
||||
}
|
||||
@@ -948,6 +1011,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_actionMode = startSupportActionMode(_actionModeCallbacks);
|
||||
_actionModeBackPressHandler.setEnabled(true);
|
||||
setFavoriteMenuItemVisiblity();
|
||||
setAssignIconsMenuItemVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -980,7 +1044,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
public void onListChange() { _fabScrollHelper.setVisible(true); }
|
||||
|
||||
@Override
|
||||
public void onSaveGroupFilter(List<String> groupFilter) {
|
||||
public void onSaveGroupFilter(Set<UUID> groupFilter) {
|
||||
_prefs.setGroupFilter(groupFilter);
|
||||
}
|
||||
|
||||
@@ -1071,88 +1135,79 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
private class ActionModeCallbacks implements ActionMode.Callback {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_action_mode, menu);
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_action_mode, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
if (_selectedEntries.size() == 0) {
|
||||
mode.finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_copy) {
|
||||
copyEntryCode(_selectedEntries.get(0));
|
||||
mode.finish();
|
||||
} else if (itemId == R.id.action_edit) {
|
||||
startEditEntryActivity(_selectedEntries.get(0));
|
||||
mode.finish();
|
||||
} else if (itemId == R.id.action_toggle_favorite) {
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
entry.setIsFavorite(!entry.isFavorite());
|
||||
_entryListView.replaceEntry(entry.getUUID(), entry);
|
||||
}
|
||||
_entryListView.refresh(true);
|
||||
|
||||
saveAndBackupVault();
|
||||
mode.finish();
|
||||
} else if (itemId == R.id.action_share_qr) {
|
||||
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
|
||||
ArrayList<GoogleAuthInfo> authInfos = new ArrayList<>();
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
|
||||
authInfos.add(authInfo);
|
||||
}
|
||||
|
||||
intent.putExtra("authInfos", authInfos);
|
||||
startActivity(intent);
|
||||
|
||||
mode.finish();
|
||||
} else if (itemId == R.id.action_delete) {
|
||||
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
|
||||
deleteEntries(_selectedEntries);
|
||||
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
mode.finish();
|
||||
});
|
||||
} else if (itemId == R.id.action_select_all) {
|
||||
_selectedEntries = _entryListView.selectAllEntries();
|
||||
setFavoriteMenuItemVisiblity();
|
||||
setIsMultipleSelected(_selectedEntries.size() > 1);
|
||||
} else if (itemId == R.id.action_assign_icons) {
|
||||
startAssignIconsActivity(_selectedEntries);
|
||||
mode.finish();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
if (_selectedEntries.size() == 0) {
|
||||
mode.finish();
|
||||
return true;
|
||||
}
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_copy:
|
||||
copyEntryCode(_selectedEntries.get(0));
|
||||
mode.finish();
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
case R.id.action_edit:
|
||||
startEditEntryActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0));
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_toggle_favorite:
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
entry.setIsFavorite(!entry.isFavorite());
|
||||
_entryListView.replaceEntry(entry.getUUID(), entry);
|
||||
}
|
||||
_entryListView.refresh(true);
|
||||
|
||||
saveAndBackupVault();
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_share_qr:
|
||||
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
|
||||
ArrayList<GoogleAuthInfo> authInfos = new ArrayList<>();
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
|
||||
authInfos.add(authInfo);
|
||||
}
|
||||
|
||||
intent.putExtra("authInfos", authInfos);
|
||||
startActivity(intent);
|
||||
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_delete:
|
||||
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
|
||||
deleteEntries(_selectedEntries);
|
||||
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
if (entry.getGroup() != null) {
|
||||
TreeSet<String> groups = _vaultManager.getVault().getGroups();
|
||||
if (!groups.contains(entry.getGroup())) {
|
||||
_entryListView.setGroups(groups);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mode.finish();
|
||||
});
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
_entryListView.setActionModeState(false, null);
|
||||
_actionModeBackPressHandler.setEnabled(false);
|
||||
_selectedEntries.clear();
|
||||
_actionMode = null;
|
||||
}
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
_entryListView.setActionModeState(false, null);
|
||||
_actionModeBackPressHandler.setEnabled(false);
|
||||
_selectedEntries.clear();
|
||||
_actionMode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
|
||||
public class PreferencesActivity extends AegisActivity implements
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
private Fragment _fragment;
|
||||
private CharSequence _prefTitle;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -48,6 +49,10 @@ public class PreferencesActivity extends AegisActivity implements
|
||||
}
|
||||
} else {
|
||||
_fragment = getSupportFragmentManager().findFragmentById(R.id.content);
|
||||
_prefTitle = savedInstanceState.getCharSequence("prefTitle");
|
||||
if (_prefTitle != null) {
|
||||
setTitle(_prefTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +74,7 @@ public class PreferencesActivity extends AegisActivity implements
|
||||
// this is done so we don't lose anything if the fragment calls recreate on this activity
|
||||
outState.putParcelable("result", ((PreferencesFragment) _fragment).getResult());
|
||||
}
|
||||
outState.putCharSequence("prefTitle", _prefTitle);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@@ -90,7 +96,8 @@ public class PreferencesActivity extends AegisActivity implements
|
||||
_fragment.setTargetFragment(caller, 0);
|
||||
showFragment(_fragment);
|
||||
|
||||
setTitle(pref.getTitle());
|
||||
_prefTitle = pref.getTitle();
|
||||
setTitle(_prefTitle);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,11 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
_themeHelper.setTheme(ThemeMap.FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (_executor != null) {
|
||||
@@ -95,11 +100,6 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
_menu = menu;
|
||||
@@ -142,10 +142,10 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
||||
if (dual) {
|
||||
switch (_currentLens) {
|
||||
case CameraSelector.LENS_FACING_BACK:
|
||||
item.setIcon(R.drawable.ic_camera_front_24dp);
|
||||
item.setIcon(R.drawable.ic_outline_camera_front_24);
|
||||
break;
|
||||
case CameraSelector.LENS_FACING_FRONT:
|
||||
item.setIcon(R.drawable.ic_camera_rear_24dp);
|
||||
item.setIcon(R.drawable.ic_outline_camera_rear_24);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,9 +152,9 @@ public class TransferEntriesActivity extends AegisActivity {
|
||||
_entriesCount.setText(getResources().getQuantityString(R.plurals.qr_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
|
||||
|
||||
@ColorInt int backgroundColor = Color.WHITE;
|
||||
if (getConfiguredTheme() == Theme.LIGHT) {
|
||||
if (_themeHelper.getConfiguredTheme() == Theme.LIGHT) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
getTheme().resolveAttribute(R.attr.background, typedValue, true);
|
||||
getTheme().resolveAttribute(androidx.appcompat.R.attr.background, typedValue, true);
|
||||
backgroundColor = typedValue.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,19 +18,19 @@ import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||
public class DropdownCheckBoxes<T> extends AppCompatAutoCompleteTextView {
|
||||
private @PluralsRes int _selectedCountPlural = R.plurals.dropdown_checkboxes_default_count;
|
||||
|
||||
private boolean _allowFiltering = false;
|
||||
|
||||
private final List<String> _items = new ArrayList<>();
|
||||
private List<String> _visibleItems = new ArrayList<>();
|
||||
private final Set<String> _checkedItems = new TreeSet<>();
|
||||
private final List<T> _items = new ArrayList<>();
|
||||
private List<T> _visibleItems = new ArrayList<>();
|
||||
private final Set<T> _checkedItems = new HashSet<>();
|
||||
|
||||
private CheckboxAdapter _adapter;
|
||||
|
||||
@@ -70,7 +70,15 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||
}
|
||||
}
|
||||
|
||||
public void addItems(List<String> items, boolean startChecked) {
|
||||
/**
|
||||
* Add parameterized items to be displayed as a checkbox in the dropdown view
|
||||
* the label for the checkbox is determined by the toString() method of the items
|
||||
* you add.
|
||||
*
|
||||
* @param items a list of the items you want to show in the dropdown
|
||||
* @param startChecked whether the checkbox should be checked initially
|
||||
*/
|
||||
public void addItems(List<T> items, boolean startChecked) {
|
||||
_items.addAll(items);
|
||||
_visibleItems.addAll(items);
|
||||
|
||||
@@ -97,7 +105,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||
_selectedCountPlural = resId;
|
||||
}
|
||||
|
||||
public Set<String> getCheckedItems() {
|
||||
public Set<T> getCheckedItems() {
|
||||
return _checkedItems;
|
||||
}
|
||||
|
||||
@@ -109,7 +117,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getItem(int i) {
|
||||
public T getItem(int i) {
|
||||
return _visibleItems.get(i);
|
||||
}
|
||||
|
||||
@@ -124,19 +132,18 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||
convertView = LayoutInflater.from(getContext()).inflate(R.layout.dropdown_checkbox, viewGroup, false);
|
||||
}
|
||||
|
||||
String item = _visibleItems.get(i);
|
||||
T item = _visibleItems.get(i);
|
||||
|
||||
CheckBox checkBox = convertView.findViewById(R.id.checkbox_in_dropdown);
|
||||
checkBox.setText(item);
|
||||
checkBox.setText(item.toString());
|
||||
checkBox.setTag(item);
|
||||
checkBox.setChecked(_checkedItems.contains(item));
|
||||
|
||||
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
String label = buttonView.getText().toString();
|
||||
|
||||
if (isChecked) {
|
||||
_checkedItems.add(label);
|
||||
_checkedItems.add((T) buttonView.getTag());
|
||||
} else {
|
||||
_checkedItems.remove(label);
|
||||
_checkedItems.remove((T) buttonView.getTag());
|
||||
}
|
||||
|
||||
updateCheckedItemsCountText();
|
||||
@@ -153,9 +160,9 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||
FilterResults results = new FilterResults();
|
||||
results.values = (query == null || query.toString().isEmpty())
|
||||
? _items
|
||||
: _items.stream().filter(str -> {
|
||||
: _items.stream().filter(item -> {
|
||||
String q = query.toString().toLowerCase();
|
||||
String strLower = str.toLowerCase();
|
||||
String strLower = item.toString().toLowerCase();
|
||||
|
||||
return strLower.contains(q);
|
||||
})
|
||||
@@ -166,7 +173,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
|
||||
|
||||
@Override
|
||||
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
|
||||
_visibleItems = (List<String>) filterResults.values;
|
||||
_visibleItems = (List<T>) filterResults.values;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class ChangelogDialog extends SimpleWebViewDialog {
|
||||
private ChangelogDialog() {
|
||||
public ChangelogDialog() {
|
||||
super(R.string.changelog);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.ComponentActivity;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
@@ -39,6 +40,7 @@ import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.nulabinc.zxcvbn.Strength;
|
||||
@@ -86,9 +88,10 @@ public class Dialogs {
|
||||
}
|
||||
textMessage.setText(message);
|
||||
|
||||
showSecureDialog(new AlertDialog.Builder(context)
|
||||
showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(title)
|
||||
.setView(view)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.yes, onDelete)
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
@@ -107,9 +110,10 @@ public class Dialogs {
|
||||
}
|
||||
|
||||
public static void showDiscardDialog(Context context, DialogInterface.OnClickListener onSave, DialogInterface.OnClickListener onDiscard) {
|
||||
showSecureDialog(new AlertDialog.Builder(context)
|
||||
showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(context.getString(R.string.discard_changes))
|
||||
.setMessage(context.getString(R.string.discard_changes_description))
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(R.string.save, onSave)
|
||||
.setNegativeButton(R.string.discard, onDiscard)
|
||||
.create());
|
||||
@@ -137,7 +141,7 @@ public class Dialogs {
|
||||
}
|
||||
});
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(activity)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.set_password)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
@@ -208,7 +212,7 @@ public class Dialogs {
|
||||
}
|
||||
inputLayout.setHint(hintId);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(titleId)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null);
|
||||
@@ -242,8 +246,8 @@ public class Dialogs {
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret);
|
||||
}
|
||||
|
||||
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
|
||||
showTextInputDialog(context, titleId, hintId, listener, false);
|
||||
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, @Nullable DialogInterface.OnCancelListener onCancel) {
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, onCancel, false);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, TextInputListener listener) {
|
||||
@@ -271,7 +275,7 @@ public class Dialogs {
|
||||
CheckBox checkBox = view.findViewById(R.id.checkbox);
|
||||
checkBox.setText(checkboxMessageId);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(titleId)
|
||||
.setView(view)
|
||||
.setNegativeButton(R.string.no, (dialog1, which) ->
|
||||
@@ -305,7 +309,7 @@ public class Dialogs {
|
||||
numberPicker.setValue(currentValue);
|
||||
numberPicker.setWrapSelectorWheel(true);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.set_number)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
|
||||
@@ -330,7 +334,7 @@ public class Dialogs {
|
||||
numberPicker.setValue(currentVersionCount / 5 - 1);
|
||||
numberPicker.setWrapSelectorWheel(false);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.set_number)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
|
||||
@@ -371,10 +375,11 @@ public class Dialogs {
|
||||
TextView textMessage = view.findViewById(R.id.error_message);
|
||||
textMessage.setText(message);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog1, which);
|
||||
@@ -406,38 +411,40 @@ public class Dialogs {
|
||||
|
||||
public static void showBackupErrorDialog(Context context, Preferences.BackupResult backupRes, DialogInterface.OnClickListener listener) {
|
||||
String system = context.getString(backupRes.isBuiltIn() ? R.string.backup_system_builtin : R.string.backup_system_android);
|
||||
String message = context.getString(R.string.backup_error_dialog_details, system, backupRes.getElapsedSince(context));
|
||||
@StringRes int details = backupRes.isPermissionError() ? R.string.backup_permission_error_dialog_details : R.string.backup_error_dialog_details;
|
||||
String message = context.getString(details, system, backupRes.getElapsedSince(context));
|
||||
Dialogs.showErrorDialog(context, message, backupRes.getError(), listener);
|
||||
}
|
||||
|
||||
public static void showMultiMessageDialog(
|
||||
public static void showMultiErrorDialog(
|
||||
Context context, @StringRes int title, String message, List<CharSequence> messages, DialogInterface.OnClickListener listener) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog, which);
|
||||
}
|
||||
})
|
||||
.setNeutralButton(context.getString(R.string.details), (dialog, which) -> {
|
||||
showDetailedMultiMessageDialog(context, title, messages, listener);
|
||||
showDetailedMultiErrorDialog(context, title, messages, listener);
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
public static <T extends Throwable> void showMultiErrorDialog(
|
||||
public static <T extends Throwable> void showMultiExceptionDialog(
|
||||
Context context, @StringRes int title, String message, List<T> errors, DialogInterface.OnClickListener listener) {
|
||||
List<CharSequence> messages = new ArrayList<>();
|
||||
for (Throwable e : errors) {
|
||||
messages.add(e.toString());
|
||||
}
|
||||
|
||||
showMultiMessageDialog(context, title, message, messages, listener);
|
||||
showMultiErrorDialog(context, title, message, messages, listener);
|
||||
}
|
||||
|
||||
private static void showDetailedMultiMessageDialog(
|
||||
private static void showDetailedMultiErrorDialog(
|
||||
Context context, @StringRes int title, List<CharSequence> messages, DialogInterface.OnClickListener listener) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
for (CharSequence message : messages) {
|
||||
@@ -445,10 +452,11 @@ public class Dialogs {
|
||||
builder.append("\n\n");
|
||||
}
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(title)
|
||||
.setMessage(builder)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
if (listener != null) {
|
||||
listener.onClick(dialog1, which);
|
||||
@@ -475,10 +483,11 @@ public class Dialogs {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_time_sync, null);
|
||||
CheckBox checkBox = view.findViewById(R.id.check_warning_disable);
|
||||
|
||||
showSecureDialog(new AlertDialog.Builder(context)
|
||||
showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.time_sync_warning_title)
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
if (checkBox.isChecked()) {
|
||||
prefs.setIsTimeSyncWarningEnabled(false);
|
||||
@@ -513,7 +522,7 @@ public class Dialogs {
|
||||
setImporterHelpText(helpText, importers.get(position), isDirect);
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.choose_application)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
@@ -534,11 +543,12 @@ public class Dialogs {
|
||||
errorDetails.append("\n\n");
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.partial_google_auth_import)
|
||||
.setMessage(context.getString(R.string.partial_google_auth_import_warning, missingIndexesAsString))
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(context.getString(R.string.import_partial_export_anyway, entries), (dialog, which) -> {
|
||||
dismissHandler.onClick(dialog, which);
|
||||
})
|
||||
|
||||
@@ -18,14 +18,13 @@ import androidx.recyclerview.widget.GridLayoutManager;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||
import com.beemdevelopment.aegis.ui.views.IconRecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.ListPreloader;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
@@ -40,7 +39,7 @@ public class IconPickerDialog {
|
||||
|
||||
}
|
||||
|
||||
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, IconAdapter.Listener listener) {
|
||||
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, boolean showAddCustom, IconAdapter.Listener listener) {
|
||||
View view = LayoutInflater.from(activity).inflate(R.layout.dialog_icon_picker, null);
|
||||
TextView textIconPack = view.findViewById(R.id.text_icon_pack);
|
||||
|
||||
@@ -76,12 +75,9 @@ public class IconPickerDialog {
|
||||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
|
||||
return Glide.with(dialog.getContext())
|
||||
.asDrawable()
|
||||
.load(icon.getFile())
|
||||
.set(IconLoader.ICON_TYPE, icon.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false);
|
||||
RequestBuilder<Drawable> rb = Glide.with(dialog.getContext())
|
||||
.load(icon.getFile());
|
||||
return GlideHelper.setCommonOptions(rb, icon.getIconType());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +124,7 @@ public class IconPickerDialog {
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.addOnScrollListener(preloader);
|
||||
adapter.loadIcons(iconPacks.get(0));
|
||||
adapter.loadIcons(iconPacks.get(0), showAddCustom);
|
||||
textIconPack.setText(iconPacks.get(0).getName());
|
||||
|
||||
view.findViewById(R.id.btn_icon_pack).setOnClickListener(v -> {
|
||||
@@ -139,7 +135,7 @@ public class IconPickerDialog {
|
||||
PopupMenu popupMenu = new PopupMenu(activity, v);
|
||||
popupMenu.setOnMenuItemClickListener(item -> {
|
||||
IconPack pack = iconPacks.get(iconPackNames.indexOf(item.getTitle().toString()));
|
||||
adapter.loadIcons(pack);
|
||||
adapter.loadIcons(pack, showAddCustom);
|
||||
|
||||
String query = iconSearch.getText().toString();
|
||||
if (!query.isEmpty()) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class LicenseDialog extends SimpleWebViewDialog {
|
||||
private LicenseDialog() {
|
||||
public LicenseDialog() {
|
||||
super(R.string.license);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.common.io.CharStreams;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -44,14 +45,14 @@ public abstract class SimpleWebViewDialog extends DialogFragment {
|
||||
view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_web_view, null);
|
||||
} catch (InflateException e) {
|
||||
e.printStackTrace();
|
||||
return new AlertDialog.Builder(requireContext())
|
||||
return new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(android.R.string.dialog_alert_title)
|
||||
.setMessage(getString(R.string.webview_error))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(requireContext())
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(_title)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
@@ -69,12 +70,21 @@ public abstract class SimpleWebViewDialog extends DialogFragment {
|
||||
}
|
||||
|
||||
protected String getBackgroundColor() {
|
||||
int backgroundColorResource = _theme == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
return colorToCSS(ThemeHelper.getThemeColor(backgroundColorResource, requireContext().getTheme()));
|
||||
int color = MaterialColors.getColor(
|
||||
requireContext(),
|
||||
com.google.android.material.R.attr.colorSurfaceContainerHigh,
|
||||
getClass().getCanonicalName()
|
||||
);
|
||||
return colorToCSS(color);
|
||||
}
|
||||
|
||||
protected String getTextColor() {
|
||||
return colorToCSS(0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, requireContext().getTheme()));
|
||||
int color = MaterialColors.getColor(
|
||||
requireContext(),
|
||||
com.google.android.material.R.attr.colorOnSurface,
|
||||
getClass().getCanonicalName()
|
||||
);
|
||||
return colorToCSS(0xFFFFFF & color);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
|
||||
+53
-9
@@ -8,11 +8,17 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.beemdevelopment.aegis.AccountNamePosition;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.ui.GroupManagerActivity;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.google.android.material.color.DynamicColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
private Preference _groupsPreference;
|
||||
@@ -33,7 +39,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
|
||||
_resetUsageCountPreference = requirePreference("pref_reset_usage_count");
|
||||
_resetUsageCountPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.preference_reset_usage_count)
|
||||
.setMessage(R.string.preference_reset_usage_count_dialog)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> _prefs.clearUsageCount())
|
||||
@@ -48,7 +54,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
darkModePreference.setOnPreferenceClickListener(preference -> {
|
||||
int currentTheme1 = _prefs.getCurrentTheme().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.choose_theme)
|
||||
.setSingleChoiceItems(R.array.theme_titles, currentTheme1, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
@@ -65,11 +71,36 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference dynamicColorsPreference = requirePreference("pref_dynamic_colors");
|
||||
dynamicColorsPreference.setEnabled(DynamicColors.isDynamicColorAvailable());
|
||||
dynamicColorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
requireActivity().recreate();
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference langPreference = requirePreference("pref_lang");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
requireActivity().recreate();
|
||||
String[] langs = getResources().getStringArray(R.array.pref_lang_values);
|
||||
String[] langNames = getResources().getStringArray(R.array.pref_lang_entries);
|
||||
List<String> langList = Arrays.asList(langs);
|
||||
int curLangIndex = langList.contains(_prefs.getLanguage()) ? langList.indexOf(_prefs.getLanguage()) : 0;
|
||||
langPreference.setSummary(langNames[curLangIndex]);
|
||||
langPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.pref_lang_title)
|
||||
.setSingleChoiceItems(langNames, curLangIndex, (dialog, which) -> {
|
||||
int newLangIndex = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
_prefs.setLanguage(langs[newLangIndex]);
|
||||
langPreference.setSummary(langNames[newLangIndex]);
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
requireActivity().recreate();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create());
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
@@ -83,7 +114,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
viewModePreference.setOnPreferenceClickListener(preference -> {
|
||||
int currentViewMode1 = _prefs.getCurrentViewMode().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.choose_view_mode)
|
||||
.setSingleChoiceItems(R.array.view_mode_titles, currentViewMode1, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
@@ -99,9 +130,22 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
return true;
|
||||
});
|
||||
|
||||
String[] codeGroupings = getResources().getStringArray(R.array.pref_code_groupings_values);
|
||||
String[] codeGroupingNames = getResources().getStringArray(R.array.pref_code_groupings);
|
||||
int currentCodeGroupingIndex = Arrays.asList(codeGroupings).indexOf(_prefs.getCodeGroupSize().name());
|
||||
Preference codeDigitGroupingPreference = requirePreference("pref_code_group_size_string");
|
||||
codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
codeDigitGroupingPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.pref_code_group_size_title)
|
||||
.setSingleChoiceItems(codeGroupingNames, currentCodeGroupingIndex, (dialog, which) -> {
|
||||
int newCodeGroupingIndex = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
_prefs.setCodeGroupSize(Preferences.CodeGrouping.valueOf(codeGroupings[newCodeGroupingIndex]));
|
||||
|
||||
dialog.dismiss();
|
||||
getResult().putExtra("needsRefresh", true);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create());
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -117,7 +161,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
||||
_currentAccountNamePositionPreference.setOnPreferenceClickListener(preference -> {
|
||||
int currentAccountNamePosition1 = _prefs.getAccountNamePosition().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.choose_account_name_position))
|
||||
.setSingleChoiceItems(R.array.account_name_position_titles, currentAccountNamePosition1, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
|
||||
+20
-16
@@ -11,6 +11,8 @@ import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
@@ -19,6 +21,7 @@ import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
public class BackupsPreferencesFragment extends PreferencesFragment {
|
||||
private SwitchPreferenceCompat _androidBackupsPreference;
|
||||
@@ -32,6 +35,15 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
||||
private Preference _builtinBackupStatusPreference;
|
||||
private Preference _androidBackupStatusPreference;
|
||||
|
||||
private final ActivityResultLauncher<Intent> backupsResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
Intent data = activityResult.getData();
|
||||
int resultCode = activityResult.getResultCode();
|
||||
if (data != null) {
|
||||
onSelectBackupsLocationResult(resultCode, data);
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
@@ -137,13 +149,6 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (data != null && requestCode == CODE_BACKUPS) {
|
||||
onSelectBackupsLocationResult(resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void onSelectBackupsLocationResult(int resultCode, Intent data) {
|
||||
Uri uri = data.getData();
|
||||
if (resultCode != Activity.RESULT_OK || uri == null) {
|
||||
@@ -191,9 +196,9 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
||||
|
||||
// TODO: Find out why setting the tint of the icon doesn't work
|
||||
if (backupFailed) {
|
||||
pref.setIcon(R.drawable.ic_info_outline_black_24dp);
|
||||
pref.setIcon(R.drawable.ic_outline_error_24);
|
||||
} else if (res != null) {
|
||||
pref.setIcon(R.drawable.ic_check_black_24dp);
|
||||
pref.setIcon(R.drawable.ic_outline_check_24);
|
||||
} else {
|
||||
pref.setIcon(null);
|
||||
}
|
||||
@@ -201,21 +206,20 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
||||
|
||||
private CharSequence getBackupStatusMessage(@Nullable Preferences.BackupResult res) {
|
||||
String message;
|
||||
int color = R.color.warning_color;
|
||||
int colorAttr = com.google.android.material.R.attr.colorError;
|
||||
if (res == null) {
|
||||
message = getString(R.string.backup_status_none);
|
||||
} else if (res.isSuccessful()) {
|
||||
color = R.color.success_color;
|
||||
colorAttr = R.attr.colorSuccess;
|
||||
message = getString(R.string.backup_status_success, res.getElapsedSince(requireContext()));
|
||||
} else {
|
||||
message = getString(R.string.backup_status_failed, res.getElapsedSince(requireContext()));
|
||||
}
|
||||
|
||||
int color = MaterialColors.getColor(requireContext(), colorAttr, getClass().getCanonicalName());
|
||||
Spannable spannable = new SpannableString(message);
|
||||
spannable.setSpan(new ForegroundColorSpan(getResources().getColor(color)), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
if (color == R.color.warning_color) {
|
||||
spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
spannable.setSpan(new ForegroundColorSpan(color), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
}
|
||||
|
||||
@@ -226,7 +230,7 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
||||
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||
|
||||
_vaultManager.startActivityForResult(this, intent, CODE_BACKUPS);
|
||||
_vaultManager.fireIntentLauncher(this, intent, backupsResultLauncher);
|
||||
}
|
||||
|
||||
private void scheduleBackup() {
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ import androidx.preference.Preference;
|
||||
import com.beemdevelopment.aegis.CopyBehavior;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
public class BehaviorPreferencesFragment extends PreferencesFragment {
|
||||
private Preference _entryPausePreference;
|
||||
@@ -23,7 +24,7 @@ public class BehaviorPreferencesFragment extends PreferencesFragment {
|
||||
copyBehaviorPreference.setOnPreferenceClickListener(preference -> {
|
||||
int currentCopyBehavior1 = _prefs.getCopyBehavior().ordinal();
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.choose_copy_behavior))
|
||||
.setSingleChoiceItems(R.array.copy_behavior_titles, currentCopyBehavior1, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
|
||||
+18
-19
@@ -10,9 +10,10 @@ import android.view.animation.Animation;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -28,6 +29,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ImportIconPackTask;
|
||||
import com.beemdevelopment.aegis.ui.views.IconPackAdapter;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -36,20 +38,25 @@ import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class IconPacksManagerFragment extends Fragment implements IconPackAdapter.Listener {
|
||||
private static final int CODE_IMPORT = 0;
|
||||
|
||||
@Inject
|
||||
IconPackManager _iconPackManager;
|
||||
|
||||
@Inject
|
||||
VaultManager _vaultManager;
|
||||
|
||||
private View _iconPacksView;
|
||||
private RecyclerView _iconPacksRecyclerView;
|
||||
private IconPackAdapter _adapter;
|
||||
private LinearLayout _noIconPacksView;
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
private final ActivityResultLauncher<Intent> importResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
Intent data = activityResult.getData();
|
||||
if (activityResult.getResultCode() == Activity.RESULT_OK && data != null && data.getData() != null) {
|
||||
importIconPack(data.getData());
|
||||
}
|
||||
});
|
||||
|
||||
public IconPacksManagerFragment() {
|
||||
super(R.layout.fragment_icon_packs);
|
||||
}
|
||||
@@ -62,7 +69,6 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
|
||||
|
||||
_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(requireContext());
|
||||
@@ -96,9 +102,10 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
|
||||
|
||||
@Override
|
||||
public void onRemoveIconPack(IconPack pack) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.remove_icon_pack)
|
||||
.setMessage(R.string.remove_icon_pack_description)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
try {
|
||||
_iconPackManager.removeIconPack(pack);
|
||||
@@ -114,22 +121,14 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
|
||||
.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(requireContext(), result -> {
|
||||
Exception e = result.getException();
|
||||
if (e instanceof IconPackExistsException) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setMessage(R.string.icon_pack_import_exists_error)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
if (removeIconPack(((IconPackExistsException) e).getIconPack())) {
|
||||
importIconPack(uri);
|
||||
@@ -164,15 +163,15 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
|
||||
private void startImportIconPack() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
_vaultManager.startActivityForResult(this, intent, CODE_IMPORT);
|
||||
_vaultManager.fireIntentLauncher(this, intent, importResultLauncher);
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
if (_adapter.getItemCount() > 0) {
|
||||
_iconPacksView.setVisibility(View.VISIBLE);
|
||||
_iconPacksRecyclerView.setVisibility(View.VISIBLE);
|
||||
_noIconPacksView.setVisibility(View.GONE);
|
||||
} else {
|
||||
_iconPacksView.setVisibility(View.GONE);
|
||||
_iconPacksRecyclerView.setVisibility(View.GONE);
|
||||
_noIconPacksView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
+80
-48
@@ -12,6 +12,8 @@ import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.ArrayRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -31,16 +33,19 @@ import com.beemdevelopment.aegis.ui.ImportEntriesActivity;
|
||||
import com.beemdevelopment.aegis.ui.TransferEntriesActivity;
|
||||
import com.beemdevelopment.aegis.ui.components.DropdownCheckBoxes;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ExportTask;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultBackupManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import java.io.File;
|
||||
@@ -48,12 +53,14 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
@@ -62,6 +69,35 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
private DatabaseImporter.Definition _importerDef;
|
||||
private Vault.EntryFilter _exportFilter;
|
||||
|
||||
private final ActivityResultLauncher<Intent> importResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
getResult().putExtra("needsRecreate", true);
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> importSelectResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
Intent data = activityResult.getData();
|
||||
if (data != null) {
|
||||
onImportSelectResult(activityResult.getResultCode(), data);
|
||||
}
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> exportResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult ->
|
||||
onExportResult(CODE_EXPORT, activityResult.getResultCode(), activityResult.getData()));
|
||||
|
||||
private final ActivityResultLauncher<Intent> exportPlainResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult ->
|
||||
onExportResult(CODE_EXPORT_PLAIN, activityResult.getResultCode(), activityResult.getData()));
|
||||
|
||||
private final ActivityResultLauncher<Intent> exportHtmlResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult ->
|
||||
onExportResult(CODE_EXPORT_HTML, activityResult.getResultCode(), activityResult.getData()));
|
||||
|
||||
private final ActivityResultLauncher<Intent> exportGoogleUriResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult ->
|
||||
onExportResult(CODE_EXPORT_GOOGLE_URI, activityResult.getResultCode(), activityResult.getData()));
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
@@ -78,7 +114,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
_vaultManager.startActivityForResult(this, intent, CODE_IMPORT_SELECT);
|
||||
_vaultManager.fireIntentLauncher(this, intent, importSelectResultLauncher);
|
||||
});
|
||||
return true;
|
||||
});
|
||||
@@ -110,28 +146,6 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
outState.putSerializable("importerDef", _importerDef);
|
||||
}
|
||||
|
||||
@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_HTML:
|
||||
// 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) {
|
||||
@@ -153,7 +167,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
Intent intent = new Intent(requireContext(), ImportEntriesActivity.class);
|
||||
intent.putExtra("importerDef", importerDef);
|
||||
intent.putExtra("file", file);
|
||||
startActivityForResult(intent, CODE_IMPORT);
|
||||
importResultLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private void startExport() {
|
||||
@@ -164,7 +178,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept);
|
||||
CheckBox checkBoxExportAllGroups = view.findViewById(R.id.export_selected_groups);
|
||||
TextInputLayout groupsSelectionLayout = view.findViewById(R.id.group_selection_layout);
|
||||
DropdownCheckBoxes groupsSelection = view.findViewById(R.id.group_selection_dropdown);
|
||||
DropdownCheckBoxes<VaultGroupModel> groupsSelection = view.findViewById(R.id.group_selection_dropdown);
|
||||
TextView passwordInfoText = view.findViewById(R.id.text_separate_password);
|
||||
passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE);
|
||||
AutoCompleteTextView dropdown = view.findViewById(R.id.dropdown_export_format);
|
||||
@@ -177,19 +191,19 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE);
|
||||
});
|
||||
|
||||
TreeSet<String> groups = _vaultManager.getVault().getGroups();
|
||||
Collection<VaultGroup> groups = _vaultManager.getVault().getUsedGroups();
|
||||
if (groups.size() > 0) {
|
||||
checkBoxExportAllGroups.setVisibility(View.VISIBLE);
|
||||
|
||||
ArrayList<String> groupsArray = new ArrayList<>();
|
||||
groupsArray.add(getString(R.string.no_group));
|
||||
groupsArray.addAll(groups);
|
||||
ArrayList<VaultGroupModel> groupsArray = new ArrayList<>();
|
||||
groupsArray.add(new VaultGroupModel(getString(R.string.no_group)));
|
||||
groupsArray.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
|
||||
|
||||
groupsSelection.setCheckedItemsCountTextRes(R.plurals.export_groups_selected_count);
|
||||
groupsSelection.addItems(groupsArray, false);
|
||||
}
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(requireContext())
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.pref_export_summary)
|
||||
.setView(view)
|
||||
.setNeutralButton(R.string.share, null)
|
||||
@@ -256,14 +270,15 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
}
|
||||
|
||||
int pos = getStringResourceIndex(R.array.export_formats, dropdown.getText().toString());
|
||||
int requestCode = getExportRequestCode(pos, checkBoxEncrypt.isChecked());
|
||||
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked());
|
||||
boolean encrypt = checkBoxEncrypt.isChecked();
|
||||
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, encrypt);
|
||||
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType(getExportMimeType(requestCode))
|
||||
.setType(getExportMimeType(getExportRequestCode(pos, encrypt)))
|
||||
.putExtra(Intent.EXTRA_TITLE, fileInfo.toString());
|
||||
|
||||
_vaultManager.startActivityForResult(this, intent, requestCode);
|
||||
ActivityResultLauncher<Intent> resultLauncher = getExportRequestLauncher(pos, encrypt);
|
||||
_vaultManager.fireIntentLauncher(this, intent, resultLauncher);
|
||||
});
|
||||
|
||||
btnNeutral.setOnClickListener(v -> {
|
||||
@@ -282,8 +297,9 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
}
|
||||
|
||||
File file;
|
||||
boolean encrypt = checkBoxEncrypt.isChecked();
|
||||
try {
|
||||
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked());
|
||||
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, encrypt);
|
||||
file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
@@ -291,7 +307,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
return;
|
||||
}
|
||||
|
||||
int requestCode = getExportRequestCode(pos, checkBoxEncrypt.isChecked());
|
||||
int requestCode = getExportRequestCode(pos, encrypt);
|
||||
ActivityResultLauncher<Intent> resultLauncher = getExportRequestLauncher(pos, encrypt);
|
||||
startExportVault(requestCode, cb -> {
|
||||
try (OutputStream stream = new FileOutputStream(file)) {
|
||||
cb.exportVault(stream);
|
||||
@@ -310,7 +327,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
.setType(getExportMimeType(requestCode))
|
||||
.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
Intent chooser = Intent.createChooser(intent, getString(R.string.pref_export_summary));
|
||||
_vaultManager.startActivity(this, chooser);
|
||||
_vaultManager.fireIntentLauncher(this, chooser, resultLauncher);
|
||||
}, _exportFilter);
|
||||
_exportFilter = null;
|
||||
});
|
||||
@@ -319,17 +336,19 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private Vault.EntryFilter getVaultEntryFilter(DropdownCheckBoxes dropdownCheckBoxes) {
|
||||
Set<String> groups = new HashSet<>();
|
||||
for (String group: dropdownCheckBoxes.getCheckedItems()) {
|
||||
if (group.equals(getString(R.string.no_group))) {
|
||||
groups.add(null);
|
||||
} else {
|
||||
groups.add(group);
|
||||
}
|
||||
private Vault.EntryFilter getVaultEntryFilter(DropdownCheckBoxes<VaultGroupModel> dropdownCheckBoxes) {
|
||||
Set<UUID> groups = new HashSet<>();
|
||||
for (VaultGroupModel group : dropdownCheckBoxes.getCheckedItems()) {
|
||||
groups.add(group.getUUID());
|
||||
}
|
||||
|
||||
return groups.isEmpty() ? null : entry -> groups.contains(entry.getGroup());
|
||||
return groups.isEmpty() ? null : entry -> {
|
||||
if (entry.getGroups().isEmpty()) {
|
||||
return groups.contains(null);
|
||||
} else {
|
||||
return entry.getGroups().stream().anyMatch(groups::contains);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void startGoogleAuthenticatorStyleExport() {
|
||||
@@ -385,6 +404,16 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
return CODE_EXPORT_GOOGLE_URI;
|
||||
}
|
||||
|
||||
private ActivityResultLauncher<Intent> getExportRequestLauncher(int spinnerPos, boolean encrypt) {
|
||||
if (spinnerPos == 0) {
|
||||
return encrypt ? exportResultLauncher : exportPlainResultLauncher;
|
||||
} else if (spinnerPos == 1) {
|
||||
return exportHtmlResultLauncher;
|
||||
}
|
||||
|
||||
return exportGoogleUriResultLauncher;
|
||||
}
|
||||
|
||||
private static VaultBackupManager.FileInfo getExportFileInfo(int spinnerPos, boolean encrypt) {
|
||||
if (spinnerPos == 0) {
|
||||
String filename = encrypt ? VaultRepository.FILENAME_PREFIX_EXPORT : VaultRepository.FILENAME_PREFIX_EXPORT_PLAIN;
|
||||
@@ -477,7 +506,10 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void onExportResult(int requestCode, int resultCode, Intent data) {
|
||||
private void onExportResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
Uri uri = data.getData();
|
||||
if (resultCode != Activity.RESULT_OK || uri == null) {
|
||||
return;
|
||||
|
||||
-4
@@ -25,14 +25,10 @@ import dagger.hilt.android.AndroidEntryPoint;
|
||||
@AndroidEntryPoint
|
||||
public abstract class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
// activity request codes
|
||||
public static final int CODE_IMPORT_SELECT = 0;
|
||||
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_EXPORT_HTML = 8;
|
||||
public static final int CODE_BACKUPS = 9;
|
||||
|
||||
private Intent _result;
|
||||
|
||||
|
||||
+9
-5
@@ -30,6 +30,7 @@ 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 com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -91,9 +92,10 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
|
||||
if (!_vaultManager.getVault().isEncryptionEnabled()) {
|
||||
Dialogs.showSetPasswordDialog(requireActivity(), new EnableEncryptionListener());
|
||||
} else {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.disable_encryption)
|
||||
.setMessage(getText(R.string.disable_encryption_description))
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||
try {
|
||||
_vaultManager.disableEncryption();
|
||||
@@ -169,9 +171,10 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
|
||||
task.execute(getLifecycle(), params);
|
||||
} else {
|
||||
_pinKeyboardPreference.setChecked(false);
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(R.string.pin_keyboard_error)
|
||||
.setMessage(R.string.pin_keyboard_error_description)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create());
|
||||
@@ -192,7 +195,7 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
|
||||
checkedItems[i] = _prefs.isAutoLockTypeEnabled(items[i]);
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.pref_auto_lock_prompt)
|
||||
.setMultiChoiceItems(textItems, checkedItems, (dialog, index, isChecked) -> checkedItems[index] = isChecked)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
@@ -221,7 +224,7 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
|
||||
.map(f -> getString(f.getStringRes()))
|
||||
.toArray(String[]::new);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.pref_password_reminder_title)
|
||||
.setSingleChoiceItems(textItems, currFreq.ordinal(), (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
@@ -459,9 +462,10 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
|
||||
if (result != null) {
|
||||
_pinKeyboardPreference.setChecked(true);
|
||||
} else {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Aegis_AlertDialog_Error)
|
||||
.setTitle(R.string.pin_keyboard_error)
|
||||
.setMessage(R.string.invalid_password)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create());
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.graphics.drawable.PictureDrawable;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
@@ -19,7 +20,7 @@ import java.nio.ByteBuffer;
|
||||
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.prepend(VaultEntryIcon.class, ByteBuffer.class, new VaultEntryIconLoader.Factory());
|
||||
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
|
||||
.append(InputStream.class, SVG.class, new SvgDecoder())
|
||||
.append(ByteBuffer.class, SVG.class, new SvgBytesDecoder());
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RawRes;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.RequestManager;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.DrawableImageViewTarget;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class GlideHelper {
|
||||
private GlideHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static void loadIconFile(RequestManager rm, File file, IconType iconType, ImageView targetView) {
|
||||
load(rm.load(file), iconType, targetView);
|
||||
}
|
||||
|
||||
public static void loadIcon(RequestManager rm, IconPack.Icon icon, ImageView targetView) {
|
||||
loadIconFile(rm, icon.getFile(), icon.getIconType(), targetView);
|
||||
}
|
||||
|
||||
public static void loadResource(RequestManager rm, @RawRes @DrawableRes @Nullable Integer resourceId, ImageView targetView) {
|
||||
loadResource(rm, resourceId, null, targetView);
|
||||
}
|
||||
|
||||
public static void loadResource(RequestManager rm, @RawRes @DrawableRes @Nullable Integer resourceId, @Nullable Integer tint, ImageView targetView) {
|
||||
setCommonOptions(rm.load(resourceId), null)
|
||||
.listener(new ViewReadyListener<>(view -> {
|
||||
if (tint != null) {
|
||||
view.setColorFilter(tint);
|
||||
}
|
||||
setLayerType(targetView, IconType.INVALID);
|
||||
}))
|
||||
.into(targetView);
|
||||
}
|
||||
|
||||
public static void loadEntryIcon(RequestManager rm, VaultEntry entry, ImageView targetView) {
|
||||
if (entry.hasIcon()) {
|
||||
setCommonOptions(rm.load(entry.getIcon()), entry.getIcon().getType()).into(targetView);
|
||||
} else {
|
||||
// Clear any pending loads for targetView, so that the TextDrawable
|
||||
// we're about to display doesn't get overwritten when that pending load finishes
|
||||
rm.clear(targetView);
|
||||
|
||||
setLayerType(targetView, IconType.INVALID);
|
||||
TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName(), targetView);
|
||||
targetView.setImageDrawable(drawable);
|
||||
}
|
||||
}
|
||||
|
||||
private static void load(RequestBuilder<Drawable> rb, IconType iconType, ImageView targetView) {
|
||||
setCommonOptions(rb, iconType).into(targetView);
|
||||
}
|
||||
|
||||
public static RequestBuilder<Drawable> setCommonOptions(RequestBuilder<Drawable> rb, IconType iconType) {
|
||||
if (iconType != null) {
|
||||
rb = rb.set(VaultEntryIconLoader.ICON_TYPE, iconType)
|
||||
.listener(new ViewReadyListener<>(targetView -> {
|
||||
targetView.setImageTintList(null);
|
||||
setLayerType(targetView, iconType);
|
||||
}));
|
||||
}
|
||||
|
||||
return rb.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static void setLayerType(ImageView view, IconType iconType) {
|
||||
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
|
||||
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
|
||||
return;
|
||||
}
|
||||
|
||||
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
|
||||
private static class ViewReadyListener<T> implements RequestListener<T> {
|
||||
private final Listener<T> _listener;
|
||||
|
||||
public ViewReadyListener(Listener<T> listener) {
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e, @Nullable Object model, @NonNull Target<T> target, boolean isFirstResource) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(@NonNull T resource, @NonNull Object model, Target<T> target, @NonNull DataSource dataSource, boolean isFirstResource) {
|
||||
if (target instanceof DrawableImageViewTarget) {
|
||||
DrawableImageViewTarget viewTarget = (DrawableImageViewTarget) target;
|
||||
if (_listener != null) {
|
||||
_listener.onConfigureImageView(viewTarget.getView());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface Listener<T> {
|
||||
void onConfigureImageView(ImageView targetView);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ 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;
|
||||
return options.get(VaultEntryIconLoader.ICON_TYPE) == IconType.SVG;
|
||||
}
|
||||
|
||||
public Resource<SVG> decode(
|
||||
|
||||
+8
-8
@@ -2,30 +2,30 @@ package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.bumptech.glide.load.Key;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.UUID;
|
||||
|
||||
public class UUIDKey implements Key {
|
||||
private UUID _uuid;
|
||||
public class VaultEntryIconKey implements Key {
|
||||
private final VaultEntryIcon _icon;
|
||||
|
||||
public UUIDKey(UUID uuid) {
|
||||
_uuid = uuid;
|
||||
public VaultEntryIconKey(VaultEntryIcon icon) {
|
||||
_icon = icon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
||||
messageDigest.update(_uuid.toString().getBytes(CHARSET));
|
||||
messageDigest.update(_icon.getHash());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return _uuid.equals(o);
|
||||
return _icon.equals(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return _uuid.hashCode();
|
||||
return _icon.hashCode();
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -3,7 +3,7 @@ package com.beemdevelopment.aegis.ui.glide;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.Option;
|
||||
@@ -15,29 +15,29 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
|
||||
public class VaultEntryIconLoader implements ModelLoader<VaultEntryIcon, 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));
|
||||
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntryIcon icon, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(new VaultEntryIconKey(icon), new Fetcher(icon));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull VaultEntry model) {
|
||||
public boolean handles(@NonNull VaultEntryIcon icon) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class Fetcher implements DataFetcher<ByteBuffer> {
|
||||
private final VaultEntry _model;
|
||||
private final VaultEntryIcon _icon;
|
||||
|
||||
private Fetcher(VaultEntry model) {
|
||||
_model = model;
|
||||
private Fetcher(VaultEntryIcon icon) {
|
||||
_icon = icon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
|
||||
byte[] bytes = _model.getIcon();
|
||||
byte[] bytes = _icon.getBytes();
|
||||
ByteBuffer buf = ByteBuffer.wrap(bytes);
|
||||
callback.onDataReady(buf);
|
||||
}
|
||||
@@ -65,11 +65,11 @@ public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<VaultEntry, ByteBuffer> {
|
||||
public static class Factory implements ModelLoaderFactory<VaultEntryIcon, ByteBuffer> {
|
||||
@NonNull
|
||||
@Override
|
||||
public ModelLoader<VaultEntry, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
|
||||
return new IconLoader();
|
||||
public ModelLoader<VaultEntryIcon, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
|
||||
return new VaultEntryIconLoader();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2,7 +2,6 @@ package com.beemdevelopment.aegis.ui.intro;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -16,6 +15,7 @@ import androidx.viewpager2.widget.ViewPager2;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
|
||||
import com.beemdevelopment.aegis.ui.AegisActivity;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
@@ -28,8 +28,8 @@ public abstract class IntroBaseActivity extends AegisActivity implements IntroAc
|
||||
private List<Class<? extends SlideFragment>> _slides;
|
||||
private WeakReference<SlideFragment> _currentSlide;
|
||||
|
||||
private ImageButton _btnPrevious;
|
||||
private ImageButton _btnNext;
|
||||
private MaterialButton _btnPrevious;
|
||||
private MaterialButton _btnNext;
|
||||
private SlideIndicator _slideIndicator;
|
||||
|
||||
@Override
|
||||
@@ -158,7 +158,7 @@ public abstract class IntroBaseActivity extends AegisActivity implements IntroAc
|
||||
? View.VISIBLE
|
||||
: View.INVISIBLE);
|
||||
if (pos == _slides.size() - 1) {
|
||||
_btnNext.setImageResource(R.drawable.circular_button_done);
|
||||
_btnNext.setIconResource(R.drawable.ic_outline_check_24);
|
||||
}
|
||||
_slideIndicator.setSlideCount(_slides.size());
|
||||
_slideIndicator.setCurrentSlide(pos);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.beemdevelopment.aegis.ui.models;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class AssignIconEntry implements Serializable {
|
||||
private final VaultEntry _entry;
|
||||
|
||||
private IconPack.Icon _newIcon;
|
||||
|
||||
private transient AssignIconEntry.Listener _listener;
|
||||
|
||||
public void setOnResetListener(AssignIconEntry.Listener listener) {
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
public AssignIconEntry(VaultEntry entry) {
|
||||
_entry = entry;
|
||||
}
|
||||
|
||||
public VaultEntry getEntry() {
|
||||
return _entry;
|
||||
}
|
||||
|
||||
public IconPack.Icon getNewIcon() { return _newIcon; }
|
||||
|
||||
public void setNewIcon(IconPack.Icon icon) {
|
||||
_newIcon = icon;
|
||||
|
||||
if (_listener != null) {
|
||||
_listener.onNewIconChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onNewIconChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.beemdevelopment.aegis.ui.models;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
public class ErrorCardInfo {
|
||||
private final String _message;
|
||||
private final View.OnClickListener _listener;
|
||||
|
||||
public ErrorCardInfo(String message, View.OnClickListener listener) {
|
||||
_message = message;
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return _message;
|
||||
}
|
||||
|
||||
public View.OnClickListener getListener() {
|
||||
return _listener;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.beemdevelopment.aegis.ui.models;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
public class VaultGroupModel implements Serializable {
|
||||
private final VaultGroup _group;
|
||||
private final String _placeholderName;
|
||||
|
||||
public VaultGroupModel(VaultGroup group) {
|
||||
_group = group;
|
||||
_placeholderName = null;
|
||||
}
|
||||
|
||||
public VaultGroupModel(String placeholderName) {
|
||||
_group = null;
|
||||
_placeholderName = placeholderName;
|
||||
}
|
||||
|
||||
public VaultGroup getGroup() {
|
||||
return _group;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _group != null ? _group.getName() : _placeholderName;
|
||||
}
|
||||
|
||||
public boolean isPlaceholder() {
|
||||
return _group == null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public UUID getUUID() {
|
||||
return _group == null ? null : _group.getUUID();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
}
|
||||
@@ -74,18 +74,14 @@ public class SecurityPickerSlide extends SlideFragment {
|
||||
int buttonId = _buttonGroup.getCheckedRadioButtonId();
|
||||
|
||||
int type;
|
||||
switch (buttonId) {
|
||||
case R.id.rb_none:
|
||||
type = CRYPT_TYPE_NONE;
|
||||
break;
|
||||
case R.id.rb_password:
|
||||
type = CRYPT_TYPE_PASS;
|
||||
break;
|
||||
case R.id.rb_biometrics:
|
||||
type = CRYPT_TYPE_BIOMETRIC;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException(String.format("Unsupported security type: %d", buttonId));
|
||||
if (buttonId == R.id.rb_none) {
|
||||
type = CRYPT_TYPE_NONE;
|
||||
} else if (buttonId == R.id.rb_password) {
|
||||
type = CRYPT_TYPE_PASS;
|
||||
} else if (buttonId == R.id.rb_biometrics) {
|
||||
type = CRYPT_TYPE_BIOMETRIC;
|
||||
} else {
|
||||
throw new RuntimeException(String.format("Unsupported security type: %d", buttonId));
|
||||
}
|
||||
|
||||
introState.putInt("cryptType", type);
|
||||
|
||||
@@ -7,6 +7,8 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
@@ -24,29 +26,28 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class WelcomeSlide extends SlideFragment {
|
||||
public static final int CODE_IMPORT_VAULT = 0;
|
||||
|
||||
private boolean _imported;
|
||||
private VaultFileCredentials _creds;
|
||||
|
||||
private final ActivityResultLauncher<Intent> vaultImportResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
Intent data = activityResult.getData();
|
||||
if (data != null && data.getData() != null) {
|
||||
startImportVault(data.getData());
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_welcome_slide, container, false);
|
||||
view.findViewById(R.id.btnImport).setOnClickListener(v -> {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, CODE_IMPORT_VAULT);
|
||||
vaultImportResultLauncher.launch(intent);
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == CODE_IMPORT_VAULT && data != null && data.getData() != null) {
|
||||
startImportVault(data.getData());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveIntroState(@NonNull Bundle introState) {
|
||||
introState.putBoolean("imported", _imported);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.beemdevelopment.aegis.ui.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
|
||||
import org.bouncycastle.crypto.params.Argon2Parameters;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class Argon2Task extends ProgressDialogTask<Argon2Task.Params, SecretKey> {
|
||||
private final Callback _cb;
|
||||
|
||||
public Argon2Task(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.unlocking_vault));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SecretKey doInBackground(Params... args) {
|
||||
setPriority();
|
||||
|
||||
Params params = args[0];
|
||||
return deriveKey(params);
|
||||
}
|
||||
|
||||
public static SecretKey deriveKey(Params params) {
|
||||
Argon2BytesGenerator gen = new Argon2BytesGenerator();
|
||||
gen.init(params.getArgon2Params());
|
||||
|
||||
byte[] key = new byte[params.getKeySize()];
|
||||
gen.generateBytes(params.getPassword(), key);
|
||||
return new SecretKeySpec(key, 0, key.length, "AES");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(SecretKey key) {
|
||||
super.onPostExecute(key);
|
||||
_cb.onTaskFinished(key);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(SecretKey key);
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
private final char[] _password;
|
||||
private final Argon2Parameters _argon2Params;
|
||||
private final int _keySize;
|
||||
|
||||
public Params(char[] password, Argon2Parameters argon2Params, int keySize) {
|
||||
_password = password;
|
||||
_argon2Params = argon2Params;
|
||||
_keySize = keySize;
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return _password;
|
||||
}
|
||||
|
||||
public Argon2Parameters getArgon2Params() {
|
||||
return _argon2Params;
|
||||
}
|
||||
|
||||
public int getKeySize() {
|
||||
return _keySize;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,38 @@
|
||||
package com.beemdevelopment.aegis.ui.tasks;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Process;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.OnLifecycleEvent;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, String, Result> {
|
||||
private ProgressDialog _dialog;
|
||||
private final AlertDialog _dialog;
|
||||
private final TextView _textProgress;
|
||||
|
||||
public ProgressDialogTask(Context context, String message) {
|
||||
_dialog = new ProgressDialog(context);
|
||||
_dialog.setCancelable(false);
|
||||
_dialog.setMessage(message);
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_progress, null);
|
||||
_textProgress = view.findViewById(R.id.text_progress);
|
||||
_textProgress.setText(message);
|
||||
|
||||
_dialog = new MaterialAlertDialogBuilder(context)
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.create();
|
||||
|
||||
Dialogs.secureDialog(_dialog);
|
||||
}
|
||||
|
||||
@@ -41,7 +53,7 @@ public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Param
|
||||
@Override
|
||||
protected void onProgressUpdate(String... values) {
|
||||
if (values.length == 1) {
|
||||
_dialog.setMessage(values[0]);
|
||||
_textProgress.setText(values[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +61,7 @@ public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Param
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
|
||||
}
|
||||
|
||||
protected final ProgressDialog getDialog() {
|
||||
protected final AlertDialog getDialog() {
|
||||
return _dialog;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public class AssignIconAdapter extends RecyclerView.Adapter<AssignIconHolder> {
|
||||
private AssignIconAdapter.Listener _listener;
|
||||
private ArrayList<AssignIconEntry> _entries;
|
||||
|
||||
public AssignIconAdapter(AssignIconAdapter.Listener listener) {
|
||||
_listener = listener;
|
||||
_entries = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void addEntries(Collection<AssignIconEntry> entries) {
|
||||
_entries.addAll(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssignIconHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_assign_icon_entry, parent, false);
|
||||
AssignIconHolder holder = new AssignIconHolder(view);
|
||||
// NOTE: This assumes that the old and new icon views are the same size
|
||||
_listener.onSetPreloadView(holder.getOldIconView());
|
||||
return holder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(AssignIconHolder holder, int position) {
|
||||
holder.setData(_entries.get(position));
|
||||
holder.itemView.setOnClickListener(view -> {
|
||||
_listener.onAssignIconEntryClick(_entries.get(position));
|
||||
});
|
||||
_entries.get(position).setOnResetListener(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return _entries.size();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onAssignIconEntryClick(AssignIconEntry entry);
|
||||
void onSetPreloadView(View view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
public class AssignIconHolder extends RecyclerView.ViewHolder implements AssignIconEntry.Listener {
|
||||
private View _view;
|
||||
|
||||
private AssignIconEntry _entry;
|
||||
private TextView _issuer;
|
||||
private TextView _accountName;
|
||||
private ImageView _oldIcon;
|
||||
private ImageView _newIcon;
|
||||
private ImageView _btnReset;
|
||||
|
||||
public AssignIconHolder(final View view) {
|
||||
super(view);
|
||||
|
||||
_view = view.findViewById(R.id.rlCardEntry);
|
||||
|
||||
_issuer = view.findViewById(R.id.tvIssuer);
|
||||
_accountName = view.findViewById(R.id.tvAccountName);
|
||||
_oldIcon = view.findViewById(R.id.ivOldImage);
|
||||
_newIcon = view.findViewById(R.id.ivNewImage);
|
||||
_btnReset = view.findViewById(R.id.btnReset);
|
||||
_btnReset.setOnClickListener(l -> _entry.setNewIcon(null));
|
||||
}
|
||||
|
||||
public void setData(AssignIconEntry entry) {
|
||||
_entry = entry;
|
||||
_issuer.setText(entry.getEntry().getIssuer());
|
||||
_accountName.setText(entry.getEntry().getName());
|
||||
|
||||
GlideHelper.loadEntryIcon(Glide.with(_view.getContext()), _entry.getEntry(), _oldIcon);
|
||||
setNewIcon();
|
||||
}
|
||||
|
||||
private void setNewIcon() {
|
||||
if (_entry.getNewIcon() != null) {
|
||||
GlideHelper.loadIcon(Glide.with(_view.getContext()), _entry.getNewIcon(), _newIcon);
|
||||
} else {
|
||||
GlideHelper.loadResource(Glide.with(_view.getContext()), R.drawable.ic_unselected, _newIcon);
|
||||
}
|
||||
|
||||
_btnReset.setVisibility(_entry.getNewIcon() != null ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
|
||||
public View getOldIconView() {
|
||||
return _oldIcon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIconChanged() {
|
||||
setNewIcon();
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,10 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.CopyBehavior;
|
||||
import com.beemdevelopment.aegis.AccountNamePosition;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.CopyBehavior;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
|
||||
@@ -29,6 +29,7 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
|
||||
import com.beemdevelopment.aegis.util.CollectionUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
@@ -36,9 +37,12 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -48,6 +52,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
private List<VaultEntry> _shownEntries;
|
||||
private List<VaultEntry> _selectedEntries;
|
||||
private Map<UUID, Integer> _usageCounts;
|
||||
private Map<UUID, Long> _lastUsedTimestamps;
|
||||
private VaultEntry _focusedEntry;
|
||||
private VaultEntry _clickedEntry;
|
||||
private Preferences.CodeGrouping _codeGroupSize;
|
||||
@@ -59,7 +64,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
private boolean _tapToReveal;
|
||||
private int _tapToRevealTime;
|
||||
private CopyBehavior _copyBehavior;
|
||||
private List<String> _groupFilter;
|
||||
private Set<UUID> _groupFilter;
|
||||
private SortCategory _sortCategory;
|
||||
private ViewMode _viewMode;
|
||||
private String _searchFilter;
|
||||
@@ -68,6 +73,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
private Handler _dimHandler;
|
||||
private Handler _doubleTapHandler;
|
||||
private boolean _pauseFocused;
|
||||
private ErrorCardInfo _errorCardInfo;
|
||||
|
||||
// keeps track of the EntryHolders that are currently bound
|
||||
private List<EntryHolder> _holders;
|
||||
@@ -76,7 +82,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
_entries = new ArrayList<>();
|
||||
_shownEntries = new ArrayList<>();
|
||||
_selectedEntries = new ArrayList<>();
|
||||
_groupFilter = new ArrayList<>();
|
||||
_groupFilter = new TreeSet<>();
|
||||
_holders = new ArrayList<>();
|
||||
_dimHandler = new Handler();
|
||||
_doubleTapHandler = new Handler();
|
||||
@@ -128,8 +134,21 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
_pauseFocused = pauseFocused;
|
||||
}
|
||||
|
||||
public VaultEntry getEntryAt(int position) {
|
||||
return _shownEntries.get(position);
|
||||
public void setErrorCardInfo(ErrorCardInfo info) {
|
||||
ErrorCardInfo oldInfo = _errorCardInfo;
|
||||
_errorCardInfo = info;
|
||||
|
||||
if (oldInfo == null && info != null) {
|
||||
notifyItemInserted(0);
|
||||
} else if (oldInfo != null && info == null) {
|
||||
notifyItemRemoved(0);
|
||||
} else {
|
||||
notifyItemChanged(0);
|
||||
}
|
||||
}
|
||||
|
||||
public VaultEntry getEntryAtPos(int position) {
|
||||
return _shownEntries.get(translateEntryPosToIndex(position));
|
||||
}
|
||||
|
||||
public int addEntry(VaultEntry entry) {
|
||||
@@ -146,17 +165,17 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
for (int i = getShownFavoritesCount(); i < _shownEntries.size(); i++) {
|
||||
if (comparator.compare(_shownEntries.get(i), entry) > 0) {
|
||||
_shownEntries.add(i, entry);
|
||||
notifyItemInserted(i);
|
||||
position = i;
|
||||
position = translateEntryIndexToPos(i);
|
||||
notifyItemInserted(position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (position < 0){
|
||||
if (position < 0) {
|
||||
_shownEntries.add(entry);
|
||||
|
||||
position = getItemCount() - 1;
|
||||
position = translateEntryIndexToPos(getShownEntriesCount() - 1);
|
||||
if (position == 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
@@ -173,6 +192,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
public void addEntries(Collection<VaultEntry> entries) {
|
||||
for (VaultEntry entry: entries) {
|
||||
entry.setUsageCount(_usageCounts.containsKey(entry.getUUID()) ? _usageCounts.get(entry.getUUID()) : 0);
|
||||
entry.setLastUsedTimestamp(_lastUsedTimestamps.containsKey(entry.getUUID()) ? _lastUsedTimestamps.get(entry.getUUID()) : 0);
|
||||
}
|
||||
|
||||
_entries.addAll(entries);
|
||||
@@ -184,9 +204,12 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
_entries.remove(entry);
|
||||
|
||||
if (_shownEntries.contains(entry)) {
|
||||
int position = _shownEntries.indexOf(entry);
|
||||
_shownEntries.remove(position);
|
||||
int index = _shownEntries.indexOf(entry);
|
||||
_shownEntries.remove(index);
|
||||
|
||||
int position = translateEntryIndexToPos(index);
|
||||
notifyItemRemoved(position);
|
||||
|
||||
updateFooter();
|
||||
}
|
||||
|
||||
@@ -211,26 +234,32 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||
|
||||
if (_shownEntries.contains(oldEntry)) {
|
||||
int position = _shownEntries.indexOf(oldEntry);
|
||||
int index = _shownEntries.indexOf(oldEntry);
|
||||
int position = translateEntryIndexToPos(index);
|
||||
if (isEntryFiltered(newEntry)) {
|
||||
_shownEntries.remove(position);
|
||||
_shownEntries.remove(index);
|
||||
notifyItemRemoved(position);
|
||||
} else {
|
||||
_shownEntries.set(position, newEntry);
|
||||
_shownEntries.set(index, newEntry);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
sortShownEntries();
|
||||
int newPosition = _shownEntries.indexOf(newEntry);
|
||||
int newIndex = _shownEntries.indexOf(newEntry);
|
||||
int newPosition = translateEntryIndexToPos(newIndex);
|
||||
if (newPosition != NO_POSITION && position != newPosition) {
|
||||
notifyItemMoved(position, newPosition);
|
||||
}
|
||||
} else if (!isEntryFiltered(newEntry)) {
|
||||
// NOTE: This logic is wrong, because sorting is not taken into account. This code
|
||||
// path is currently never hit though, because it is not possible to edit an entry
|
||||
// that is not shown.
|
||||
_shownEntries.add(newEntry);
|
||||
|
||||
int position = getItemCount() - 1;
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
|
||||
checkPeriodUniformity();
|
||||
updateFooter();
|
||||
}
|
||||
@@ -245,13 +274,46 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given entry position in the recycler view, to its index in the shown entries list.
|
||||
*/
|
||||
public int translateEntryPosToIndex(int position) {
|
||||
if (position == NO_POSITION) {
|
||||
return NO_POSITION;
|
||||
}
|
||||
|
||||
if (isErrorCardShown()) {
|
||||
position -= 1;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given entry index in the shown entries list, to its position in the recycler view.
|
||||
*/
|
||||
private int translateEntryIndexToPos(int index) {
|
||||
if (index == NO_POSITION) {
|
||||
return NO_POSITION;
|
||||
}
|
||||
|
||||
if (isErrorCardShown()) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private boolean isEntryFiltered(VaultEntry entry) {
|
||||
String group = entry.getGroup();
|
||||
Set<UUID> groups = entry.getGroups();
|
||||
String issuer = entry.getIssuer().toLowerCase();
|
||||
String name = entry.getName().toLowerCase();
|
||||
|
||||
if (!_groupFilter.isEmpty()) {
|
||||
if (!_groupFilter.contains(group)) {
|
||||
if (groups.isEmpty() && !_groupFilter.contains(null)) {
|
||||
return true;
|
||||
}
|
||||
if (!groups.isEmpty() && _groupFilter.stream().filter(Objects::nonNull).noneMatch(groups::contains)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -274,7 +336,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
}
|
||||
}
|
||||
|
||||
public void setGroupFilter(@NonNull List<String> groups) {
|
||||
public void setGroupFilter(@NonNull Set<UUID> groups) {
|
||||
if (_groupFilter.equals(groups)) {
|
||||
return;
|
||||
}
|
||||
@@ -316,6 +378,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
}
|
||||
|
||||
sortShownEntries();
|
||||
checkPeriodUniformity();
|
||||
_view.onListChange();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
@@ -348,14 +411,14 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
|
||||
public Map<UUID, Integer> getUsageCounts() { return _usageCounts; }
|
||||
|
||||
public void setLastUsedTimestamps(Map<UUID, Long> lastUsedTimestamps) { _lastUsedTimestamps = lastUsedTimestamps; }
|
||||
|
||||
public Map<UUID, Long> getLastUsedTimestamps() { return _lastUsedTimestamps; }
|
||||
|
||||
public int getShownFavoritesCount() {
|
||||
return (int) _shownEntries.stream().filter(VaultEntry::isFavorite).count();
|
||||
}
|
||||
|
||||
public void setGroups(TreeSet<String> groups) {
|
||||
_view.setGroups(groups);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDismiss(int position) {
|
||||
|
||||
@@ -365,33 +428,42 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
public void onItemDrop(int position) {
|
||||
// moving entries is not allowed when a filter is applied
|
||||
// footer cant be moved, nor can items be moved below it
|
||||
if (!_groupFilter.isEmpty() || isPositionFooter(position)) {
|
||||
if (!_groupFilter.isEmpty() || isPositionFooter(position) || isPositionErrorCard(position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_view.onEntryDrop(_shownEntries.get(position));
|
||||
int index = translateEntryPosToIndex(position);
|
||||
_view.onEntryDrop(_shownEntries.get(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMove(int firstPosition, int secondPosition) {
|
||||
// moving entries is not allowed when a filter is applied
|
||||
// footer cant be moved, nor can items be moved below it
|
||||
if (!_groupFilter.isEmpty() || isPositionFooter(firstPosition) || isPositionFooter(secondPosition)) {
|
||||
if (!_groupFilter.isEmpty()
|
||||
|| isPositionFooter(firstPosition) || isPositionFooter(secondPosition)
|
||||
|| isPositionErrorCard(firstPosition) || isPositionErrorCard(secondPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// notify the vault first
|
||||
_view.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
|
||||
int firstIndex = translateEntryPosToIndex(firstPosition);
|
||||
int secondIndex = translateEntryPosToIndex(secondPosition);
|
||||
_view.onEntryMove(_entries.get(firstIndex), _entries.get(secondIndex));
|
||||
|
||||
// then update our end
|
||||
CollectionUtils.move(_entries, firstPosition, secondPosition);
|
||||
CollectionUtils.move(_shownEntries, firstPosition, secondPosition);
|
||||
CollectionUtils.move(_entries, firstIndex, secondIndex);
|
||||
CollectionUtils.move(_shownEntries, firstIndex, secondIndex);
|
||||
|
||||
notifyItemMoved(firstPosition, secondPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (isPositionErrorCard(position)) {
|
||||
return R.layout.card_error;
|
||||
}
|
||||
|
||||
if (isPositionFooter(position)) {
|
||||
return R.layout.card_footer;
|
||||
}
|
||||
@@ -405,7 +477,15 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
|
||||
RecyclerView.ViewHolder holder;
|
||||
View view = inflater.inflate(viewType, parent, false);
|
||||
holder = viewType == R.layout.card_footer ? new FooterView(view) : new EntryHolder(view);
|
||||
|
||||
if (viewType == R.layout.card_error) {
|
||||
holder = new ErrorCardHolder(view, _errorCardInfo);
|
||||
} else if (viewType == R.layout.card_footer) {
|
||||
holder = new FooterView(view);
|
||||
} else {
|
||||
holder = new EntryHolder(view);
|
||||
}
|
||||
|
||||
if (_showIcon && holder instanceof EntryHolder) {
|
||||
_view.setPreloadView(((EntryHolder) holder).getIconView());
|
||||
}
|
||||
@@ -425,7 +505,8 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
|
||||
if (holder instanceof EntryHolder) {
|
||||
EntryHolder entryHolder = (EntryHolder) holder;
|
||||
VaultEntry entry = _shownEntries.get(position);
|
||||
int index = translateEntryPosToIndex(position);
|
||||
VaultEntry entry = _shownEntries.get(index);
|
||||
|
||||
boolean hidden = _tapToReveal && entry != _focusedEntry;
|
||||
boolean paused = _pauseFocused && entry == _focusedEntry;
|
||||
@@ -507,12 +588,13 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
entryHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
int position = holder.getBindingAdapterPosition();
|
||||
if (_selectedEntries.isEmpty()) {
|
||||
entryHolder.setFocusedAndAnimate(true);
|
||||
}
|
||||
|
||||
boolean returnVal = _view.onLongEntryClick(_shownEntries.get(position));
|
||||
int index = translateEntryPosToIndex(position);
|
||||
boolean returnVal = _view.onLongEntryClick(_shownEntries.get(index));
|
||||
if (_selectedEntries.size() == 0 || isEntryDraggable(entry)) {
|
||||
_view.startDrag(entryHolder);
|
||||
}
|
||||
@@ -595,6 +677,10 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (infos.size() == 1) {
|
||||
return infos.get(0).getPeriod();
|
||||
}
|
||||
|
||||
Map<Integer, Integer> occurrences = new HashMap<>();
|
||||
for (TotpInfo info : infos) {
|
||||
int period = info.getPeriod();
|
||||
@@ -692,6 +778,23 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
updateDraggableStatus();
|
||||
}
|
||||
|
||||
public List<VaultEntry> selectAllEntries() {
|
||||
_selectedEntries.clear();
|
||||
|
||||
for (VaultEntry entry: _shownEntries) {
|
||||
for (EntryHolder holder: _holders) {
|
||||
if (holder.getEntry() == entry) {
|
||||
holder.setFocused(true);
|
||||
}
|
||||
}
|
||||
|
||||
_selectedEntries.add(entry);
|
||||
updateDraggableStatus();
|
||||
}
|
||||
|
||||
return new ArrayList<>(_selectedEntries);
|
||||
}
|
||||
|
||||
public void deselectAllEntries() {
|
||||
for (VaultEntry entry: _selectedEntries) {
|
||||
for (EntryHolder holder : _holders) {
|
||||
@@ -714,6 +817,8 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
int usageCount = _usageCounts.get(entry.getUUID());
|
||||
_usageCounts.put(entry.getUUID(), ++usageCount);
|
||||
}
|
||||
|
||||
_lastUsedTimestamps.put(entry.getUUID(), new Date().getTime());
|
||||
}
|
||||
|
||||
public boolean isDragAndDropAllowed() {
|
||||
@@ -730,15 +835,30 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return getEntriesCount() + 1;
|
||||
// Always at least one item because of the footer
|
||||
// Two in case there's also an error card
|
||||
int baseCount = 1;
|
||||
if (isErrorCardShown()) {
|
||||
baseCount++;
|
||||
}
|
||||
|
||||
return baseCount + getShownEntriesCount();
|
||||
}
|
||||
|
||||
public int getEntriesCount() {
|
||||
public int getShownEntriesCount() {
|
||||
return _shownEntries.size();
|
||||
}
|
||||
|
||||
public boolean isPositionFooter(int position) {
|
||||
return position == getEntriesCount();
|
||||
return position == (getItemCount() - 1);
|
||||
}
|
||||
|
||||
public boolean isPositionErrorCard(int position) {
|
||||
return isErrorCardShown() && position == 0;
|
||||
}
|
||||
|
||||
public boolean isErrorCardShown() {
|
||||
return _errorCardInfo != null;
|
||||
}
|
||||
|
||||
private void updateFooter() {
|
||||
@@ -754,13 +874,15 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
int entriesShown = getEntriesCount();
|
||||
int entriesShown = getShownEntriesCount();
|
||||
SpannableString entriesShownSpannable = new SpannableString(_footerView.getResources().getQuantityString(R.plurals.entries_shown, entriesShown, entriesShown));
|
||||
|
||||
String entriesShownString = String.format("%d", entriesShown);
|
||||
int spanStart = entriesShownSpannable.toString().indexOf(entriesShownString);
|
||||
int spanEnd = spanStart + entriesShownString.length();
|
||||
entriesShownSpannable.setSpan(new StyleSpan(Typeface.BOLD), spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
if (spanStart >= 0) {
|
||||
int spanEnd = spanStart + entriesShownString.length();
|
||||
entriesShownSpannable.setSpan(new StyleSpan(Typeface.BOLD), spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
TextView textView = _footerView.findViewById(R.id.entries_shown_count);
|
||||
textView.setText(entriesShownSpannable);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Handler;
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
@@ -11,15 +10,12 @@ import android.widget.TextView;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.beemdevelopment.aegis.AccountNamePosition;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
@@ -27,10 +23,10 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.YandexInfo;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
|
||||
public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private static final float DEFAULT_ALPHA = 1.0f;
|
||||
@@ -59,7 +55,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private boolean _paused;
|
||||
|
||||
private TotpProgressBar _progressBar;
|
||||
private View _view;
|
||||
private MaterialCardView _view;
|
||||
|
||||
private UiRefresher _refresher;
|
||||
private Handler _animationHandler;
|
||||
@@ -70,8 +66,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
public EntryHolder(final View view) {
|
||||
super(view);
|
||||
|
||||
_view = view.findViewById(R.id.rlCardEntry);
|
||||
|
||||
_view = (MaterialCardView) view;
|
||||
_profileName = view.findViewById(R.id.profile_account_name);
|
||||
_profileCode = view.findViewById(R.id.profile_code);
|
||||
_profileIssuer = view.findViewById(R.id.profile_issuer);
|
||||
@@ -87,9 +82,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_animationHandler = new Handler();
|
||||
|
||||
_progressBar = view.findViewById(R.id.progressBar);
|
||||
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
|
||||
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
|
||||
_view.setBackground(_view.getContext().getResources().getDrawable(R.color.card_background));
|
||||
|
||||
_scaleIn = AnimationsHelper.loadScaledAnimation(view.getContext(), R.anim.item_scale_in);
|
||||
_scaleOut = AnimationsHelper.loadScaledAnimation(view.getContext(), R.anim.item_scale_out);
|
||||
@@ -169,10 +161,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
profileNameLayoutParams.setMarginStart(0);
|
||||
_profileName.setLayoutParams(profileNameLayoutParams);
|
||||
_profileName.setVisibility(View.VISIBLE);
|
||||
|
||||
copiedLayoutParams = (RelativeLayout.LayoutParams) _profileCopied.getLayoutParams();
|
||||
copiedLayoutParams.addRule(RelativeLayout.ABOVE, R.id.profile_account_name);
|
||||
_profileCopied.setLayoutParams(copiedLayoutParams);
|
||||
break;
|
||||
|
||||
case END:
|
||||
@@ -183,10 +171,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
profileNameLayoutParams.removeRule(RelativeLayout.BELOW);
|
||||
_profileName.setLayoutParams(profileNameLayoutParams);
|
||||
_profileName.setVisibility(View.VISIBLE);
|
||||
|
||||
copiedLayoutParams = (RelativeLayout.LayoutParams) _profileCopied.getLayoutParams();
|
||||
copiedLayoutParams.addRule(RelativeLayout.ABOVE, R.id.description);
|
||||
_profileCopied.setLayoutParams(copiedLayoutParams);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -196,19 +180,7 @@ 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);
|
||||
} else {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_entry.getIssuer(), _entry.getName(), _profileDrawable);
|
||||
_profileDrawable.setImageDrawable(drawable);
|
||||
}
|
||||
GlideHelper.loadEntryIcon(Glide.with(fragment), _entry, _profileDrawable);
|
||||
}
|
||||
|
||||
public ImageView getIconView() {
|
||||
@@ -244,11 +216,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
public void setFocused(boolean focused) {
|
||||
if (focused) {
|
||||
_selected.setVisibility(View.VISIBLE);
|
||||
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, _view.getContext().getTheme()));
|
||||
} else {
|
||||
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackground, _view.getContext().getTheme()));
|
||||
}
|
||||
_view.setSelected(focused);
|
||||
_view.setChecked(focused);
|
||||
}
|
||||
|
||||
public void setFocusedAndAnimate(boolean focused) {
|
||||
@@ -258,7 +227,9 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_selected.startAnimation(_scaleIn);
|
||||
} else {
|
||||
_selected.startAnimation(_scaleOut);
|
||||
_selectedHandler.postDelayed(() -> _selected.setVisibility(View.GONE), 150);
|
||||
_scaleOut.setAnimationListener(new SimpleAnimationEndListener(animation -> {
|
||||
_selected.setVisibility(View.GONE);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@ package com.beemdevelopment.aegis.ui.views;
|
||||
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
@@ -14,17 +16,18 @@ import android.view.animation.LayoutAnimationController;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.CopyBehavior;
|
||||
import com.beemdevelopment.aegis.AccountNamePosition;
|
||||
import com.beemdevelopment.aegis.CopyBehavior;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
@@ -32,29 +35,33 @@ import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.MetricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
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.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
|
||||
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.bumptech.glide.Glide;
|
||||
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.BottomSheetDialog;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.android.material.divider.MaterialDividerItemDecoration;
|
||||
import com.google.android.material.shape.CornerFamily;
|
||||
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -65,17 +72,16 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private ItemTouchHelper _touchHelper;
|
||||
|
||||
private RecyclerView _recyclerView;
|
||||
private RecyclerView.ItemDecoration _verticalDividerDecoration;
|
||||
private RecyclerView.ItemDecoration _horizontalDividerDecoration;
|
||||
private RecyclerView.ItemDecoration _itemDecoration;
|
||||
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
|
||||
private TotpProgressBar _progressBar;
|
||||
private boolean _showProgress;
|
||||
private ViewMode _viewMode;
|
||||
private TreeSet<String> _groups;
|
||||
private Collection<VaultGroup> _groups;
|
||||
private LinearLayout _emptyStateView;
|
||||
private Chip _groupChip;
|
||||
private List<String> _groupFilter;
|
||||
private List<String> _prefGroupFilter;
|
||||
private Set<UUID> _groupFilter;
|
||||
private Set<UUID> _prefGroupFilter;
|
||||
|
||||
private UiRefresher _refresher;
|
||||
|
||||
@@ -101,7 +107,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
|
||||
// set up the recycler view
|
||||
_recyclerView = view.findViewById(R.id.rvKeyProfiles);
|
||||
_recyclerView.setItemAnimator(null);
|
||||
_recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
@@ -129,7 +134,9 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
if (_viewMode == ViewMode.TILES && position == _adapter.getEntriesCount()) {
|
||||
if (_viewMode == ViewMode.TILES
|
||||
&& (_adapter.isPositionFooter(position)
|
||||
|| _adapter.isPositionErrorCard(position))) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -168,7 +175,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
public void setGroupFilter(List<String> groups, boolean animate) {
|
||||
public void setGroupFilter(Set<UUID> groups, boolean animate) {
|
||||
_groupFilter = groups;
|
||||
_adapter.setGroupFilter(groups);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
@@ -188,6 +195,10 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_adapter.setCopyBehavior(copyBehavior);
|
||||
}
|
||||
|
||||
public List<VaultEntry> selectAllEntries() {
|
||||
return _adapter.selectAllEntries();
|
||||
}
|
||||
|
||||
public void setActionModeState(boolean enabled, VaultEntry entry) {
|
||||
_touchCallback.setSelectedEntry(entry);
|
||||
_touchCallback.setIsLongPressDragEnabled(enabled && _adapter.isDragAndDropAllowed());
|
||||
@@ -216,6 +227,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
return _adapter.getUsageCounts();
|
||||
}
|
||||
|
||||
public void setLastUsedTimestamps(Map<UUID, Long> lastUsedTimestamps) {
|
||||
_adapter.setLastUsedTimestamps(lastUsedTimestamps);
|
||||
}
|
||||
|
||||
public Map<UUID, Long> getLastUsedTimestamps() {
|
||||
return _adapter.getLastUsedTimestamps();
|
||||
}
|
||||
|
||||
public void setSearchFilter(String search) {
|
||||
_adapter.setSearchFilter(search);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
@@ -237,7 +256,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_touchCallback.setDragFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN);
|
||||
}
|
||||
|
||||
((GridLayoutManager)_recyclerView.getLayoutManager()).setSpanCount(mode.getColumnSpan());
|
||||
((GridLayoutManager)_recyclerView.getLayoutManager()).setSpanCount(mode.getSpanCount());
|
||||
}
|
||||
|
||||
public void startDrag(RecyclerView.ViewHolder viewHolder) {
|
||||
@@ -334,7 +353,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
public void setPrefGroupFilter(List<String> groupFilter) {
|
||||
public void setPrefGroupFilter(Set<UUID> groupFilter) {
|
||||
_prefGroupFilter = groupFilter;
|
||||
}
|
||||
|
||||
@@ -370,6 +389,10 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_adapter.setTapToRevealTime(number);
|
||||
}
|
||||
|
||||
public void setErrorCardInfo(ErrorCardInfo info) {
|
||||
_adapter.setErrorCardInfo(info);
|
||||
}
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
addEntry(entry, false);
|
||||
}
|
||||
@@ -465,17 +488,17 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_recyclerView.scheduleLayoutAnimation();
|
||||
}
|
||||
|
||||
private void addChipTo(ChipGroup chipGroup, String group) {
|
||||
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_material, null, false);
|
||||
chip.setText(group == null ? getString(R.string.no_group) : group);
|
||||
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) {
|
||||
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
|
||||
chip.setText(group.getName());
|
||||
chip.setCheckable(true);
|
||||
chip.setChecked(_groupFilter != null && _groupFilter.contains(group));
|
||||
chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID()));
|
||||
chip.setCheckedIconVisible(true);
|
||||
chip.setOnCheckedChangeListener((group1, checkedId) -> {
|
||||
List<String> groupFilter = getGroupFilter(chipGroup);
|
||||
Set<UUID> groupFilter = getGroupFilter(chipGroup);
|
||||
setGroupFilter(groupFilter, true);
|
||||
});
|
||||
chip.setTag(group == null ? new Object() : null);
|
||||
chip.setTag(group);
|
||||
chipGroup.addView(chip);
|
||||
}
|
||||
|
||||
@@ -489,7 +512,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
Button saveButton = view.findViewById(R.id.btnSave);
|
||||
clearButton.setOnClickListener(v -> {
|
||||
chipGroup.clearCheck();
|
||||
List<String> groupFilter = Collections.emptyList();
|
||||
Set<UUID> groupFilter = Collections.emptySet();
|
||||
if (_listener != null) {
|
||||
_listener.onSaveGroupFilter(groupFilter);
|
||||
}
|
||||
@@ -498,7 +521,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
});
|
||||
|
||||
saveButton.setOnClickListener(v -> {
|
||||
List<String> groupFilter = getGroupFilter(chipGroup);
|
||||
Set<UUID> groupFilter = getGroupFilter(chipGroup);
|
||||
if (_listener != null) {
|
||||
_listener.onSaveGroupFilter(groupFilter);
|
||||
}
|
||||
@@ -509,25 +532,23 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_groupChip.setOnClickListener(v -> {
|
||||
chipGroup.removeAllViews();
|
||||
|
||||
for (String group : _groups) {
|
||||
addChipTo(chipGroup, group);
|
||||
for (VaultGroup group : _groups) {
|
||||
addChipTo(chipGroup, new VaultGroupModel(group));
|
||||
}
|
||||
addChipTo(chipGroup, null);
|
||||
addChipTo(chipGroup, new VaultGroupModel(getString(R.string.no_group)));
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
private static List<String> getGroupFilter(ChipGroup chipGroup) {
|
||||
private static Set<UUID> getGroupFilter(ChipGroup chipGroup) {
|
||||
return chipGroup.getCheckedChipIds().stream()
|
||||
.map(i -> {
|
||||
Chip chip = chipGroup.findViewById(i);
|
||||
if (chip.getTag() != null) {
|
||||
return null;
|
||||
}
|
||||
return chip. getText().toString();
|
||||
VaultGroupModel group = (VaultGroupModel) chip.getTag();
|
||||
return group.getUUID();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private void updateGroupChip() {
|
||||
@@ -543,58 +564,50 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setGroups(TreeSet<String> groups) {
|
||||
public void setGroups(Collection<VaultGroup> groups) {
|
||||
_groups = groups;
|
||||
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
updateDividerDecoration();
|
||||
|
||||
if (_prefGroupFilter != null) {
|
||||
List<String> groupFilter = cleanGroupFilter(_prefGroupFilter);
|
||||
Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter);
|
||||
_prefGroupFilter = null;
|
||||
if (!groupFilter.isEmpty()) {
|
||||
setGroupFilter(groupFilter, false);
|
||||
}
|
||||
} else if (_groupFilter != null) {
|
||||
List<String> groupFilter = cleanGroupFilter(_groupFilter);
|
||||
Set<UUID> groupFilter = cleanGroupFilter(_groupFilter);
|
||||
if (!_groupFilter.equals(groupFilter)) {
|
||||
setGroupFilter(groupFilter, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> cleanGroupFilter(List<String> groupFilter) {
|
||||
return groupFilter.stream()
|
||||
.filter(g -> g == null || _groups.contains(g))
|
||||
.collect(Collectors.toList());
|
||||
private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) {
|
||||
Set<UUID> groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet());
|
||||
|
||||
return groupFilter.stream()
|
||||
.filter(g -> g == null || groupUuids.contains(g))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private void updateDividerDecoration() {
|
||||
if (_verticalDividerDecoration != null) {
|
||||
_recyclerView.removeItemDecoration(_verticalDividerDecoration);
|
||||
if (_itemDecoration != null) {
|
||||
_recyclerView.removeItemDecoration(_itemDecoration);
|
||||
}
|
||||
|
||||
if(_horizontalDividerDecoration != null) {
|
||||
_recyclerView.removeItemDecoration(_horizontalDividerDecoration);
|
||||
}
|
||||
|
||||
float height = _viewMode.getDividerHeight();
|
||||
float width = _viewMode.getDividerWidth();
|
||||
if (_showProgress && height == 0) {
|
||||
_verticalDividerDecoration = new CompactDividerDecoration();
|
||||
float offset = _viewMode.getItemOffset();
|
||||
if (_viewMode == ViewMode.TILES) {
|
||||
_itemDecoration = new TileSpaceItemDecoration(offset);
|
||||
} else {
|
||||
_verticalDividerDecoration = new VerticalSpaceItemDecoration(height);
|
||||
_itemDecoration = new VerticalSpaceItemDecoration(offset);
|
||||
}
|
||||
|
||||
if (width != 0) {
|
||||
_horizontalDividerDecoration = new TileSpaceItemDecoration(width, height);
|
||||
_recyclerView.addItemDecoration(_horizontalDividerDecoration);
|
||||
} else {
|
||||
_recyclerView.addItemDecoration(_verticalDividerDecoration);
|
||||
}
|
||||
_recyclerView.addItemDecoration(_itemDecoration);
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
if (_adapter.getEntriesCount() > 0) {
|
||||
if (_adapter.getShownEntriesCount() > 0) {
|
||||
_recyclerView.setVisibility(View.VISIBLE);
|
||||
_emptyStateView.setVisibility(View.GONE);
|
||||
} else {
|
||||
@@ -616,37 +629,22 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
void onSelect(VaultEntry entry);
|
||||
void onDeselect(VaultEntry entry);
|
||||
void onListChange();
|
||||
void onSaveGroupFilter(List<String> groupFilter);
|
||||
void onSaveGroupFilter(Set<UUID> groupFilter);
|
||||
void onEntryListTouch();
|
||||
}
|
||||
|
||||
private class CompactDividerDecoration extends MaterialDividerItemDecoration {
|
||||
public CompactDividerDecoration() {
|
||||
super(requireContext(), DividerItemDecoration.VERTICAL);
|
||||
setDividerColor(ThemeHelper.getThemeColor(R.attr.divider, requireContext().getTheme()));
|
||||
setLastItemDecorated(false);
|
||||
setDividerThickness(MetricsHelper.convertDpToPixels(requireContext(), 0.5f));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
|
||||
if (_adapter.isPositionFooter(parent.getChildAdapterPosition(view))) {
|
||||
int pixels = MetricsHelper.convertDpToPixels(requireContext(), 20);
|
||||
outRect.top = pixels;
|
||||
outRect.bottom = pixels;
|
||||
return;
|
||||
}
|
||||
|
||||
super.getItemOffsets(outRect, view, parent, state);
|
||||
}
|
||||
}
|
||||
|
||||
private class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {
|
||||
private final int _height;
|
||||
private final int _offset;
|
||||
private final ShapeAppearanceModel _defaultShapeModel;
|
||||
|
||||
private VerticalSpaceItemDecoration(float dp) {
|
||||
// convert dp to pixels
|
||||
_height = MetricsHelper.convertDpToPixels(requireContext(), dp);
|
||||
private VerticalSpaceItemDecoration(float offset) {
|
||||
_offset = MetricsHelper.convertDpToPixels(requireContext(), offset);
|
||||
|
||||
int shapeAppearanceId = getStyledAttrs(R.style.Widget_Aegis_EntryCardView,
|
||||
com.google.android.material.R.attr.shapeAppearance);
|
||||
|
||||
_defaultShapeModel = ShapeAppearanceModel.builder(
|
||||
requireContext(), shapeAppearanceId, 0).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -656,54 +654,81 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
// The footer always has a top and bottom margin
|
||||
// The error card and the footer always have a top and bottom margin
|
||||
if (_adapter.isPositionErrorCard(adapterPosition)) {
|
||||
outRect.top = _viewMode == ViewMode.COMPACT ? _offset * 4 : _offset;
|
||||
outRect.bottom = _offset;
|
||||
return;
|
||||
}
|
||||
if (_adapter.isPositionFooter(adapterPosition)) {
|
||||
outRect.top = _height;
|
||||
outRect.bottom = _height;
|
||||
outRect.top = _offset * 2;
|
||||
outRect.bottom = _offset;
|
||||
return;
|
||||
}
|
||||
|
||||
// The first entry should have a top margin, but only if the group chip is not shown
|
||||
if (adapterPosition == 0 && (_groups == null || _groups.isEmpty())) {
|
||||
outRect.top = _height;
|
||||
int entryIndex = _adapter.translateEntryPosToIndex(adapterPosition);
|
||||
// The first entry should have a top margin, but only if the group chip is not shown and the error card is not shown
|
||||
if (entryIndex == 0 && (_groups == null || _groups.isEmpty()) && !_adapter.isErrorCardShown()) {
|
||||
outRect.top = _offset;
|
||||
}
|
||||
|
||||
// Only non-favorite entries have a bottom margin, except for the final favorite entry
|
||||
int totalFavorites = _adapter.getShownFavoritesCount();
|
||||
if (totalFavorites == 0
|
||||
|| (adapterPosition < _adapter.getEntriesCount() && !_adapter.getEntryAt(adapterPosition).isFavorite())
|
||||
|| totalFavorites == adapterPosition + 1) {
|
||||
outRect.bottom = _height;
|
||||
}
|
||||
|
||||
if (totalFavorites > 0) {
|
||||
// If this entry is the last favorite entry in the list, it should always have
|
||||
// a bottom margin, regardless of the view mode
|
||||
if (adapterPosition == totalFavorites - 1) {
|
||||
outRect.bottom = _height;
|
||||
}
|
||||
|
||||
// If this is the first non-favorite entry, it should have a top margin
|
||||
if (adapterPosition == totalFavorites) {
|
||||
outRect.top = _height;
|
||||
}
|
||||
|| (entryIndex < _adapter.getShownEntriesCount() && !_adapter.getEntryAtPos(adapterPosition).isFavorite())
|
||||
|| totalFavorites == entryIndex + 1) {
|
||||
outRect.bottom = _offset;
|
||||
}
|
||||
|
||||
// The last entry should never have a bottom margin
|
||||
if (_adapter.getEntriesCount() == adapterPosition + 1) {
|
||||
if (_adapter.getShownEntriesCount() == entryIndex + 1) {
|
||||
outRect.bottom = 0;
|
||||
}
|
||||
|
||||
decorateFavoriteEntries((MaterialCardView) view, parent);
|
||||
}
|
||||
|
||||
private void decorateFavoriteEntries(@NonNull MaterialCardView view, @NonNull RecyclerView parent) {
|
||||
int adapterPosition = parent.getChildAdapterPosition(view);
|
||||
int entryIndex = _adapter.translateEntryPosToIndex(adapterPosition);
|
||||
int totalFavorites = _adapter.getShownFavoritesCount();
|
||||
|
||||
ShapeAppearanceModel.Builder builder = _defaultShapeModel.toBuilder();
|
||||
if (entryIndex < totalFavorites) {
|
||||
if ((entryIndex == 0 && totalFavorites > 1) || (entryIndex < (totalFavorites - 1))) {
|
||||
builder.setBottomLeftCorner(CornerFamily.ROUNDED, 0);
|
||||
builder.setBottomRightCorner(CornerFamily.ROUNDED, 0);
|
||||
}
|
||||
if (entryIndex > 0) {
|
||||
builder.setTopLeftCorner(CornerFamily.ROUNDED, 0);
|
||||
builder.setTopRightCorner(CornerFamily.ROUNDED, 0);
|
||||
}
|
||||
}
|
||||
|
||||
view.setShapeAppearanceModel(builder.build());
|
||||
view.setClipToOutline(true);
|
||||
}
|
||||
|
||||
private int getStyledAttrs(@StyleRes int styleId, @AttrRes int attrId) {
|
||||
TypedArray cardAttrs = null;
|
||||
try {
|
||||
cardAttrs = requireContext().obtainStyledAttributes(styleId, new int[]{attrId});
|
||||
TypedValue value = new TypedValue();
|
||||
cardAttrs.getValue(0, value);
|
||||
return value.data;
|
||||
} finally {
|
||||
if (cardAttrs != null) {
|
||||
cardAttrs.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TileSpaceItemDecoration extends RecyclerView.ItemDecoration {
|
||||
private final int _width;
|
||||
private final int _height;
|
||||
private final int _offset;
|
||||
|
||||
private TileSpaceItemDecoration(float width, float height) {
|
||||
// convert dp to pixels
|
||||
_width = MetricsHelper.convertDpToPixels(requireContext(), width);
|
||||
_height = MetricsHelper.convertDpToPixels(requireContext(), height);
|
||||
private TileSpaceItemDecoration(float offset) {
|
||||
_offset = MetricsHelper.convertDpToPixels(requireContext(), offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -713,10 +738,21 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
outRect.left = _width;
|
||||
outRect.right = _width;
|
||||
outRect.top = _height;
|
||||
outRect.bottom = _height;
|
||||
outRect.left = _offset;
|
||||
outRect.right = _offset;
|
||||
outRect.top = _offset;
|
||||
outRect.bottom = _offset;
|
||||
|
||||
if (_adapter.isPositionErrorCard(adapterPosition)
|
||||
|| (isInFirstEntryRow(adapterPosition) && !_adapter.isErrorCardShown())
|
||||
|| _adapter.isPositionFooter(adapterPosition)) {
|
||||
outRect.top *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInFirstEntryRow(int pos) {
|
||||
int index = _adapter.translateEntryPosToIndex(pos);
|
||||
return index >= 0 && index < _viewMode.getSpanCount();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,22 +764,24 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
VaultEntry entry = _adapter.getEntryAt(position);
|
||||
if (_adapter.getItemViewType(position) == R.layout.card_error) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
VaultEntry entry = _adapter.getEntryAtPos(position);
|
||||
if (!entry.hasIcon()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.singletonList(entry);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
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);
|
||||
RequestBuilder<Drawable> rb = Glide.with(EntryListView.this)
|
||||
.load(entry.getIcon());
|
||||
return GlideHelper.setCommonOptions(rb, entry.getIcon().getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
|
||||
public class ErrorCardHolder extends RecyclerView.ViewHolder {
|
||||
public ErrorCardHolder(@NonNull View itemView, ErrorCardInfo info) {
|
||||
super(itemView);
|
||||
|
||||
TextView errorTextView = itemView.findViewById(R.id.text_error_bar);
|
||||
errorTextView.setText(info.getMessage());
|
||||
|
||||
MaterialCardView errorCard = itemView.findViewById(R.id.card_error);
|
||||
errorCard.setOnClickListener(info.getListener());
|
||||
}
|
||||
}
|
||||
@@ -7,19 +7,20 @@ import android.view.ViewGroup;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||
private GroupAdapter.Listener _listener;
|
||||
private ArrayList<String> _groups;
|
||||
private ArrayList<VaultGroup> _groups;
|
||||
|
||||
public GroupAdapter(GroupAdapter.Listener listener) {
|
||||
_listener = listener;
|
||||
_groups = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void addGroup(String group) {
|
||||
public void addGroup(VaultGroup group) {
|
||||
_groups.add(group);
|
||||
|
||||
int position = getItemCount() - 1;
|
||||
@@ -30,7 +31,7 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||
}
|
||||
}
|
||||
|
||||
public void removeGroup(String group) {
|
||||
public void removeGroup(VaultGroup group) {
|
||||
int position = _groups.indexOf(group);
|
||||
_groups.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
@@ -57,6 +58,6 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onRemoveGroup(String group);
|
||||
void onRemoveGroup(VaultGroup group);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.widget.TextView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
|
||||
public class GroupHolder extends RecyclerView.ViewHolder {
|
||||
private TextView _slotName;
|
||||
@@ -18,8 +19,8 @@ public class GroupHolder extends RecyclerView.ViewHolder {
|
||||
_buttonDelete = view.findViewById(R.id.button_delete);
|
||||
}
|
||||
|
||||
public void setData(String groupName) {
|
||||
_slotName.setText(groupName);
|
||||
public void setData(VaultGroup group) {
|
||||
_slotName.setText(group.getName());
|
||||
}
|
||||
|
||||
public void setOnDeleteClickListener(View.OnClickListener listener) {
|
||||
|
||||
@@ -18,7 +18,6 @@ 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;
|
||||
@@ -40,7 +39,7 @@ public class IconAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
/**
|
||||
* 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) {
|
||||
public void loadIcons(IconPack pack, boolean showAddCustom) {
|
||||
_pack = pack;
|
||||
_query = null;
|
||||
_icons = new ArrayList<>(_pack.getIcons());
|
||||
@@ -60,7 +59,11 @@ public class IconAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
.count();
|
||||
|
||||
List<IconPack.Icon> suggested = pack.getSuggestedIcons(_issuer);
|
||||
suggested.add(0, new DummyIcon(_context.getString(R.string.icon_custom)));
|
||||
|
||||
if (showAddCustom) {
|
||||
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);
|
||||
@@ -90,13 +93,9 @@ public class IconAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
_query = query;
|
||||
|
||||
if (_query == null) {
|
||||
loadIcons(_pack);
|
||||
loadIcons(_pack, false);
|
||||
} else {
|
||||
_icons = _pack.getIcons().stream()
|
||||
.filter(i -> i.isSuggestedFor(query))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Collections.sort(_icons, Comparator.comparing(IconPack.Icon::getName));
|
||||
_icons = _pack.getSuggestedIcons(query);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
@@ -232,16 +231,8 @@ public class IconAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
}
|
||||
|
||||
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;
|
||||
super(name, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,13 +8,11 @@ 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.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@@ -41,19 +39,10 @@ public class IconHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
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);
|
||||
int tint = MaterialColors.getColor(itemView, com.google.android.material.R.attr.colorOnSurfaceVariant);
|
||||
GlideHelper.loadResource(Glide.with(context), R.drawable.ic_outline_add_24, tint, _imageView);
|
||||
} 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);
|
||||
GlideHelper.loadIconFile(Glide.with(context), _iconFile, _iconType, _imageView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ImportEntriesAdapter extends RecyclerView.Adapter<ImportEntryHolder> {
|
||||
private List<ImportEntry> _entries;
|
||||
@@ -67,6 +68,14 @@ public class ImportEntriesAdapter extends RecyclerView.Adapter<ImportEntryHolder
|
||||
return entries;
|
||||
}
|
||||
|
||||
public void setCheckboxStates(List<UUID> uuids, boolean state) {
|
||||
for (ImportEntry entry : _entries) {
|
||||
if(uuids.contains(entry.getEntry().getUUID())) {
|
||||
entry.setIsChecked(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleCheckboxes() {
|
||||
int checkedEntries = getCheckedEntries().size();
|
||||
if (checkedEntries == 0 || checkedEntries != _entries.size()) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -99,7 +100,14 @@ public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializa
|
||||
* Reports whether the internal map contains a value with the UUID of the given value.
|
||||
*/
|
||||
public boolean has(T value) {
|
||||
return _map.containsKey(value.getUUID());
|
||||
return has(value.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether the internal map contains a value with the given UUID.
|
||||
*/
|
||||
public boolean has(UUID uuid) {
|
||||
return _map.containsKey(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,7 +169,7 @@ public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializa
|
||||
return false;
|
||||
}
|
||||
|
||||
return getUUID().equals(((Value) o).getUUID());
|
||||
return Objects.equals(getUUID(), ((Value) o).getUUID());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,13 @@ import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Vault {
|
||||
private static final int VERSION = 2;
|
||||
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||
private static final int VERSION = 3;
|
||||
private final UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||
private final UUIDMap<VaultGroup> _groups = new UUIDMap<>();
|
||||
|
||||
public JSONObject toJson() {
|
||||
return toJson(null);
|
||||
@@ -18,16 +22,24 @@ public class Vault {
|
||||
|
||||
public JSONObject toJson(@Nullable EntryFilter filter) {
|
||||
try {
|
||||
JSONArray array = new JSONArray();
|
||||
JSONArray entriesArray = new JSONArray();
|
||||
for (VaultEntry e : _entries) {
|
||||
if (filter == null || filter.includeEntry(e)) {
|
||||
array.put(e.toJson());
|
||||
entriesArray.put(e.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
// Always include all groups, even if they're not assigned to any entry (before or after the entry filter)
|
||||
JSONArray groupsArray = new JSONArray();
|
||||
for (VaultGroup group : _groups) {
|
||||
groupsArray.put(group.toJson());
|
||||
}
|
||||
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("version", VERSION);
|
||||
obj.put("entries", array);
|
||||
obj.put("entries", entriesArray);
|
||||
obj.put("groups", groupsArray);
|
||||
|
||||
return obj;
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -37,6 +49,7 @@ public class Vault {
|
||||
public static Vault fromJson(JSONObject obj) throws VaultException {
|
||||
Vault vault = new Vault();
|
||||
UUIDMap<VaultEntry> entries = vault.getEntries();
|
||||
UUIDMap<VaultGroup> groups = vault.getGroups();
|
||||
|
||||
try {
|
||||
int ver = obj.getInt("version");
|
||||
@@ -44,9 +57,28 @@ public class Vault {
|
||||
throw new VaultException("Unsupported version");
|
||||
}
|
||||
|
||||
if (obj.has("groups")) {
|
||||
JSONArray groupsArray = obj.getJSONArray("groups");
|
||||
for (int i = 0; i < groupsArray.length(); i++) {
|
||||
VaultGroup group = VaultGroup.fromJson(groupsArray.getJSONObject(i));
|
||||
if (!groups.has(group)) {
|
||||
groups.add(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray array = obj.getJSONArray("entries");
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(i));
|
||||
vault.migrateOldGroup(entry);
|
||||
|
||||
// check the vault has a group corresponding to each one the entry claims to be in
|
||||
for (UUID groupUuid: entry.getGroups()) {
|
||||
if (!groups.has(groupUuid)) {
|
||||
entry.removeGroup(groupUuid);
|
||||
}
|
||||
}
|
||||
|
||||
entries.add(entry);
|
||||
}
|
||||
} catch (VaultEntryException | JSONException e) {
|
||||
@@ -56,10 +88,33 @@ public class Vault {
|
||||
return vault;
|
||||
}
|
||||
|
||||
public void migrateOldGroup(VaultEntry entry) {
|
||||
if (entry.getOldGroup() != null) {
|
||||
Optional<VaultGroup> optGroup = getGroups().getValues()
|
||||
.stream()
|
||||
.filter(g -> g.getName().equals(entry.getOldGroup()))
|
||||
.findFirst();
|
||||
|
||||
if (optGroup.isPresent()) {
|
||||
entry.addGroup(optGroup.get().getUUID());
|
||||
} else {
|
||||
VaultGroup group = new VaultGroup(entry.getOldGroup());
|
||||
getGroups().add(group);
|
||||
entry.addGroup(group.getUUID());
|
||||
}
|
||||
|
||||
entry.setOldGroup(null);
|
||||
}
|
||||
}
|
||||
|
||||
public UUIDMap<VaultEntry> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
public UUIDMap<VaultGroup> getGroups() {
|
||||
return _groups;
|
||||
}
|
||||
|
||||
public interface EntryFilter {
|
||||
boolean includeEntry(VaultEntry entry);
|
||||
}
|
||||
|
||||
@@ -53,14 +53,15 @@ public class VaultBackupManager {
|
||||
try {
|
||||
createBackup(tempFile, dirUri, versionsToKeep);
|
||||
_prefs.setBuiltInBackupResult(new Preferences.BackupResult(null));
|
||||
} catch (VaultRepositoryException e) {
|
||||
} catch (VaultRepositoryException | VaultBackupPermissionException e) {
|
||||
e.printStackTrace();
|
||||
_prefs.setBuiltInBackupResult(new Preferences.BackupResult(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createBackup(File tempFile, Uri dirUri, int versionsToKeep) throws VaultRepositoryException {
|
||||
private void createBackup(File tempFile, Uri dirUri, int versionsToKeep)
|
||||
throws VaultRepositoryException, VaultBackupPermissionException {
|
||||
FileInfo fileInfo = new FileInfo(FILENAME_PREFIX);
|
||||
DocumentFile dir = DocumentFile.fromTreeUri(_context, dirUri);
|
||||
|
||||
@@ -68,7 +69,7 @@ public class VaultBackupManager {
|
||||
Log.i(TAG, String.format("Creating backup at %s: %s", Uri.decode(dir.getUri().toString()), fileInfo.toString()));
|
||||
|
||||
if (!hasPermissionsAt(dirUri)) {
|
||||
throw new VaultRepositoryException("No persisted URI permissions");
|
||||
throw new VaultBackupPermissionException("No persisted URI permissions");
|
||||
}
|
||||
|
||||
// If we create a file with a name that already exists, SAF will append a number
|
||||
@@ -92,7 +93,7 @@ public class VaultBackupManager {
|
||||
} catch (IOException e) {
|
||||
throw new VaultRepositoryException(e);
|
||||
}
|
||||
} catch (VaultRepositoryException e) {
|
||||
} catch (VaultRepositoryException | VaultBackupPermissionException e) {
|
||||
Log.e(TAG, String.format("Unable to create backup: %s", e.toString()));
|
||||
throw e;
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
public class VaultBackupPermissionException extends Exception {
|
||||
public VaultBackupPermissionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
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;
|
||||
@@ -10,23 +7,26 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
|
||||
public class VaultEntry extends UUIDMap.Value {
|
||||
private String _name = "";
|
||||
private String _issuer = "";
|
||||
private String _group;
|
||||
private OtpInfo _info;
|
||||
private byte[] _icon;
|
||||
private IconType _iconType = IconType.INVALID;
|
||||
private VaultEntryIcon _icon;
|
||||
private boolean _isFavorite;
|
||||
private int _usageCount;
|
||||
private long _lastUsedTimestamp;
|
||||
private String _note = "";
|
||||
private String _oldGroup;
|
||||
private Set<UUID> _groups = new TreeSet<>();
|
||||
|
||||
private VaultEntry(UUID uuid, OtpInfo info) {
|
||||
super(uuid);
|
||||
@@ -44,13 +44,6 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
setIssuer(issuer);
|
||||
}
|
||||
|
||||
public VaultEntry(OtpInfo info, String name, String issuer, String group) {
|
||||
this(info);
|
||||
setName(name);
|
||||
setIssuer(issuer);
|
||||
setGroup(group);
|
||||
}
|
||||
|
||||
public VaultEntry(GoogleAuthInfo info) {
|
||||
this(info.getOtpInfo(), info.getAccountName(), info.getIssuer());
|
||||
}
|
||||
@@ -63,12 +56,17 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
obj.put("uuid", getUUID().toString());
|
||||
obj.put("name", _name);
|
||||
obj.put("issuer", _issuer);
|
||||
obj.put("group", _group);
|
||||
obj.put("note", _note);
|
||||
obj.put("favorite", _isFavorite);
|
||||
obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon));
|
||||
obj.put("icon_mime", _icon == null ? null : _iconType.toMimeType());
|
||||
VaultEntryIcon.toJson(_icon, obj);
|
||||
obj.put("info", _info.toJson());
|
||||
|
||||
JSONArray groupUuids = new JSONArray();
|
||||
for (UUID uuid : _groups) {
|
||||
groupUuids.put(uuid.toString());
|
||||
}
|
||||
obj.put("groups", groupUuids);
|
||||
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -90,25 +88,26 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
VaultEntry entry = new VaultEntry(uuid, info);
|
||||
entry.setName(obj.getString("name"));
|
||||
entry.setIssuer(obj.getString("issuer"));
|
||||
entry.setGroup(obj.optString("group", null));
|
||||
entry.setNote(obj.optString("note", ""));
|
||||
entry.setIsFavorite(obj.optBoolean("favorite", false));
|
||||
|
||||
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));
|
||||
// If the entry contains a list of group UUID's, assume conversion from the
|
||||
// old group system has already taken place and ignore the old group field.
|
||||
if (obj.has("groups")) {
|
||||
JSONArray groups = obj.getJSONArray("groups");
|
||||
for (int i = 0; i < groups.length(); i++) {
|
||||
String groupUuid = groups.getString(i);
|
||||
entry.addGroup(UUID.fromString(groupUuid));
|
||||
}
|
||||
|
||||
byte[] iconBytes = Base64.decode((String) icon);
|
||||
entry.setIcon(iconBytes, iconType);
|
||||
} else if (obj.has("group")) {
|
||||
entry.setOldGroup(JsonUtils.optString(obj, "group"));
|
||||
}
|
||||
|
||||
VaultEntryIcon icon = VaultEntryIcon.fromJson(obj);
|
||||
entry.setIcon(icon);
|
||||
|
||||
return entry;
|
||||
} catch (OtpInfoException | JSONException | EncodingException e) {
|
||||
} catch (OtpInfoException | JSONException e) {
|
||||
throw new VaultEntryException(e);
|
||||
}
|
||||
}
|
||||
@@ -121,18 +120,14 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
return _issuer;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return _group;
|
||||
public Set<UUID> getGroups() {
|
||||
return _groups;
|
||||
}
|
||||
|
||||
public byte[] getIcon() {
|
||||
public VaultEntryIcon getIcon() {
|
||||
return _icon;
|
||||
}
|
||||
|
||||
public IconType getIconType() {
|
||||
return _iconType;
|
||||
}
|
||||
|
||||
public OtpInfo getInfo() {
|
||||
return _info;
|
||||
}
|
||||
@@ -141,9 +136,17 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
return _usageCount;
|
||||
}
|
||||
|
||||
public String getNote() { return _note; }
|
||||
public long getLastUsedTimestamp() {
|
||||
return _lastUsedTimestamp;
|
||||
}
|
||||
|
||||
public boolean isFavorite() { return _isFavorite; };
|
||||
public String getNote() {
|
||||
return _note;
|
||||
}
|
||||
|
||||
public boolean isFavorite() {
|
||||
return _isFavorite;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
_name = name;
|
||||
@@ -153,28 +156,57 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
_issuer = issuer;
|
||||
}
|
||||
|
||||
public void setGroup(String group) {
|
||||
_group = group;
|
||||
public void addGroup(UUID group) {
|
||||
if (group == null) {
|
||||
throw new AssertionError("Attempt to add null group to entry's group list");
|
||||
}
|
||||
_groups.add(group);
|
||||
}
|
||||
|
||||
public void removeGroup(UUID group) {
|
||||
_groups.remove(group);
|
||||
}
|
||||
|
||||
public void setGroups(Set<UUID> groups) {
|
||||
if (groups.contains(null)) {
|
||||
throw new AssertionError("Attempt to add null group to entry's group list");
|
||||
}
|
||||
_groups = groups;
|
||||
}
|
||||
|
||||
public void setInfo(OtpInfo info) {
|
||||
_info = info;
|
||||
}
|
||||
|
||||
public void setIcon(byte[] icon, IconType iconType) {
|
||||
public void setIcon(VaultEntryIcon icon) {
|
||||
_icon = icon;
|
||||
_iconType = iconType;
|
||||
}
|
||||
|
||||
public boolean hasIcon() {
|
||||
return _icon != null;
|
||||
}
|
||||
|
||||
public void setUsageCount(int usageCount) { _usageCount = usageCount; }
|
||||
public void setUsageCount(int usageCount) {
|
||||
_usageCount = usageCount;
|
||||
}
|
||||
|
||||
public void setNote(String note) { _note = note; }
|
||||
public void setLastUsedTimestamp(long lastUsedTimestamp) { _lastUsedTimestamp = lastUsedTimestamp; }
|
||||
|
||||
public void setIsFavorite(boolean isFavorite) { _isFavorite = isFavorite; }
|
||||
public void setNote(String note) {
|
||||
_note = note;
|
||||
}
|
||||
|
||||
public void setIsFavorite(boolean isFavorite) {
|
||||
_isFavorite = isFavorite;
|
||||
}
|
||||
|
||||
void setOldGroup(String oldGroup) {
|
||||
_oldGroup = oldGroup;
|
||||
}
|
||||
|
||||
String getOldGroup() {
|
||||
return _oldGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
@@ -194,12 +226,11 @@ public class VaultEntry extends UUIDMap.Value {
|
||||
public boolean equivalates(VaultEntry entry) {
|
||||
return getName().equals(entry.getName())
|
||||
&& getIssuer().equals(entry.getIssuer())
|
||||
&& Objects.equals(getGroup(), entry.getGroup())
|
||||
&& getInfo().equals(entry.getInfo())
|
||||
&& Arrays.equals(getIcon(), entry.getIcon())
|
||||
&& getIconType().equals(entry.getIconType())
|
||||
&& Objects.equals(getIcon(), entry.getIcon())
|
||||
&& getNote().equals(entry.getNote())
|
||||
&& isFavorite() == entry.isFavorite();
|
||||
&& isFavorite() == entry.isFavorite()
|
||||
&& getGroups().equals(entry.getGroups());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class VaultEntryIcon implements Serializable {
|
||||
private final byte[] _bytes;
|
||||
private final byte[] _hash;
|
||||
private final IconType _type;
|
||||
|
||||
public VaultEntryIcon(byte @NonNull [] bytes, @NonNull IconType type) {
|
||||
this(bytes, type, generateHash(bytes, type));
|
||||
}
|
||||
|
||||
VaultEntryIcon(byte @NonNull [] bytes, @NonNull IconType type, byte @NonNull [] hash) {
|
||||
_bytes = bytes;
|
||||
_hash = hash;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
public byte @NonNull [] getBytes() {
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
public byte @NonNull [] getHash() {
|
||||
return _hash;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public IconType getType() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof VaultEntryIcon)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
VaultEntryIcon entry = (VaultEntryIcon) o;
|
||||
return Arrays.equals(getHash(), entry.getHash());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return HashCode.fromBytes(_hash).asInt();
|
||||
}
|
||||
|
||||
static void toJson(@Nullable VaultEntryIcon icon, @NonNull JSONObject obj) throws JSONException {
|
||||
obj.put("icon", icon == null ? JSONObject.NULL : Base64.encode(icon.getBytes()));
|
||||
if (icon != null) {
|
||||
obj.put("icon_mime", icon.getType().toMimeType());
|
||||
obj.put("icon_hash", Hex.encode(icon.getHash()));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static VaultEntryIcon fromJson(@NonNull JSONObject obj) throws VaultEntryException {
|
||||
try {
|
||||
Object icon = obj.get("icon");
|
||||
if (icon == JSONObject.NULL) {
|
||||
return 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);
|
||||
String iconHashStr = JsonUtils.optString(obj, "icon_hash");
|
||||
if (iconHashStr != null) {
|
||||
byte[] iconHash = Hex.decode(iconHashStr);
|
||||
return new VaultEntryIcon(iconBytes, iconType, iconHash);
|
||||
}
|
||||
|
||||
return new VaultEntryIcon(iconBytes, iconType);
|
||||
} catch (JSONException | EncodingException e) {
|
||||
throw new VaultEntryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte @NonNull [] generateHash(byte @NonNull [] bytes, @NonNull IconType type) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
md.update(type.toMimeType().getBytes(StandardCharsets.UTF_8));
|
||||
return md.digest(bytes);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class VaultGroup extends UUIDMap.Value {
|
||||
private String _name;
|
||||
|
||||
private VaultGroup(UUID uuid, String name) {
|
||||
super(uuid);
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public VaultGroup(String name) {
|
||||
super();
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public JSONObject toJson() {
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try {
|
||||
obj.put("uuid", getUUID().toString());
|
||||
obj.put("name", _name);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static VaultGroup fromJson(JSONObject obj) throws VaultEntryException {
|
||||
try {
|
||||
UUID uuid = UUID.fromString(obj.getString("uuid"));
|
||||
String groupName = obj.getString("name");
|
||||
|
||||
return new VaultGroup(uuid, groupName);
|
||||
} catch (JSONException e) {
|
||||
throw new VaultEntryException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof VaultGroup)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
VaultGroup entry = (VaultGroup) o;
|
||||
return super.equals(entry) && getName().equals(entry.getName());
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
_name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return _name;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -18,7 +19,6 @@ import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
@@ -29,8 +29,6 @@ public class VaultManager {
|
||||
private final Context _context;
|
||||
private final Preferences _prefs;
|
||||
|
||||
private VaultFile _vaultFile;
|
||||
private VaultRepositoryException _vaultFileError;
|
||||
private VaultRepository _repo;
|
||||
|
||||
private final VaultBackupManager _backups;
|
||||
@@ -45,35 +43,11 @@ public class VaultManager {
|
||||
_backups = new VaultBackupManager(_context);
|
||||
_androidBackups = new BackupManager(context);
|
||||
_lockListeners = new ArrayList<>();
|
||||
loadVaultFile();
|
||||
}
|
||||
|
||||
private void loadVaultFile() {
|
||||
try {
|
||||
_vaultFile = VaultRepository.readVaultFile(_context);
|
||||
} catch (VaultRepositoryException e) {
|
||||
if (!(e.getCause() instanceof FileNotFoundException)) {
|
||||
_vaultFileError = e;
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (_vaultFile != null && !_vaultFile.isEncrypted()) {
|
||||
try {
|
||||
loadFrom(_vaultFile, null);
|
||||
} catch (VaultRepositoryException e) {
|
||||
e.printStackTrace();
|
||||
_vaultFile = null;
|
||||
_vaultFileError = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the vault repository with a new empty vault and the given creds. It can
|
||||
* only be called if isVaultLoaded() returns false.
|
||||
*
|
||||
* Calling this method removes the manager's internal reference to the raw vault file (if it had one).
|
||||
*/
|
||||
@NonNull
|
||||
public VaultRepository initNew(@Nullable VaultFileCredentials creds) throws VaultRepositoryException {
|
||||
@@ -81,10 +55,9 @@ public class VaultManager {
|
||||
throw new IllegalStateException("Vault manager is already initialized");
|
||||
}
|
||||
|
||||
_vaultFile = null;
|
||||
_vaultFileError = null;
|
||||
_repo = new VaultRepository(_context, new Vault(), creds);
|
||||
save();
|
||||
VaultRepository repo = new VaultRepository(_context, new Vault(), creds);
|
||||
repo.save();
|
||||
_repo = repo;
|
||||
|
||||
if (getVault().isEncryptionEnabled()) {
|
||||
startNotificationService();
|
||||
@@ -96,8 +69,6 @@ public class VaultManager {
|
||||
/**
|
||||
* Initializes the vault repository by decrypting the given vaultFile with the given
|
||||
* creds. It can only be called if isVaultLoaded() returns false.
|
||||
*
|
||||
* Calling this method removes the manager's internal reference to the raw vault file (if it had one).
|
||||
*/
|
||||
@NonNull
|
||||
public VaultRepository loadFrom(@NonNull VaultFile vaultFile, @Nullable VaultFileCredentials creds) throws VaultRepositoryException {
|
||||
@@ -105,8 +76,6 @@ public class VaultManager {
|
||||
throw new IllegalStateException("Vault manager is already initialized");
|
||||
}
|
||||
|
||||
_vaultFile = null;
|
||||
_vaultFileError = null;
|
||||
_repo = VaultRepository.fromFile(_context, vaultFile, creds);
|
||||
|
||||
if (getVault().isEncryptionEnabled()) {
|
||||
@@ -116,32 +85,9 @@ public class VaultManager {
|
||||
return getVault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the vault repository by loading and decrypting the vault file stored in
|
||||
* internal storage, with the given creds. It can only be called if isVaultLoaded()
|
||||
* returns false.
|
||||
*
|
||||
* Calling this method removes the manager's internal reference to the raw vault file (if it had one).
|
||||
*/
|
||||
@NonNull
|
||||
public VaultRepository load(@Nullable VaultFileCredentials creds) throws VaultRepositoryException {
|
||||
if (isVaultLoaded()) {
|
||||
throw new IllegalStateException("Vault manager is already initialized");
|
||||
}
|
||||
|
||||
loadVaultFile();
|
||||
if (isVaultLoaded()) {
|
||||
return _repo;
|
||||
}
|
||||
|
||||
return loadFrom(getVaultFile(), creds);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public VaultRepository unlock(@NonNull VaultFileCredentials creds) throws VaultRepositoryException {
|
||||
VaultRepository repo = loadFrom(getVaultFile(), creds);
|
||||
startNotificationService();
|
||||
return repo;
|
||||
public VaultRepository loadFrom(@NonNull VaultFile vaultFile) throws VaultRepositoryException {
|
||||
return loadFrom(vaultFile, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +102,6 @@ public class VaultManager {
|
||||
}
|
||||
|
||||
stopNotificationService();
|
||||
loadVaultFile();
|
||||
}
|
||||
|
||||
public void enableEncryption(VaultFileCredentials creds) throws VaultRepositoryException {
|
||||
@@ -269,12 +214,8 @@ public class VaultManager {
|
||||
return _repo != null;
|
||||
}
|
||||
|
||||
public boolean isVaultFileLoaded() {
|
||||
return _vaultFile != null;
|
||||
}
|
||||
|
||||
public boolean isVaultInitNeeded() {
|
||||
return !isVaultLoaded() && !isVaultFileLoaded() && getVaultFileError() == null;
|
||||
return !isVaultLoaded() && !VaultRepository.fileExists(_context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -286,29 +227,15 @@ public class VaultManager {
|
||||
return _repo;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public VaultFile getVaultFile() {
|
||||
if (_vaultFile == null) {
|
||||
throw new IllegalStateException("Vault file is not in memory");
|
||||
}
|
||||
|
||||
return _vaultFile;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public VaultRepositoryException getVaultFileError() {
|
||||
return _vaultFileError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an external activity, temporarily blocks automatic lock of Aegis and
|
||||
* shows an error dialog if the target activity is not found.
|
||||
*/
|
||||
public void startActivityForResult(Activity activity, Intent intent, int requestCode) {
|
||||
public void fireIntentLauncher(Activity activity, Intent intent, ActivityResultLauncher<Intent> resultLauncher) {
|
||||
setBlockAutoLock(true);
|
||||
|
||||
try {
|
||||
activity.startActivityForResult(intent, requestCode, null);
|
||||
resultLauncher.launch(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
@@ -324,19 +251,11 @@ public class VaultManager {
|
||||
* Starts an external activity, temporarily blocks automatic lock of Aegis and
|
||||
* shows an error dialog if the target activity is not found.
|
||||
*/
|
||||
public void startActivity(Fragment fragment, Intent intent) {
|
||||
startActivityForResult(fragment, intent, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an external activity, temporarily blocks automatic lock of Aegis and
|
||||
* shows an error dialog if the target activity is not found.
|
||||
*/
|
||||
public void startActivityForResult(Fragment fragment, Intent intent, int requestCode) {
|
||||
public void fireIntentLauncher(Fragment fragment, Intent intent, ActivityResultLauncher<Intent> resultLauncher) {
|
||||
setBlockAutoLock(true);
|
||||
|
||||
try {
|
||||
fragment.startActivityForResult(intent, requestCode, null);
|
||||
resultLauncher.launch(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.Collator;
|
||||
import java.util.Collection;
|
||||
import java.util.TreeSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -220,6 +220,8 @@ public class VaultRepository {
|
||||
}
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
// Entries added by importing a file may contain an old group that needs to be migrated
|
||||
_vault.migrateOldGroup(entry);
|
||||
_vault.getEntries().add(entry);
|
||||
}
|
||||
|
||||
@@ -231,8 +233,12 @@ public class VaultRepository {
|
||||
return _vault.getEntries().remove(entry);
|
||||
}
|
||||
|
||||
public void wipeEntries() {
|
||||
/**
|
||||
* Wipes all entries and groups from the vault.
|
||||
*/
|
||||
public void wipeContents() {
|
||||
_vault.getEntries().wipe();
|
||||
_vault.getGroups().wipe();
|
||||
}
|
||||
|
||||
public VaultEntry replaceEntry(VaultEntry entry) {
|
||||
@@ -240,8 +246,8 @@ public class VaultRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves entry1 to the position of entry2.
|
||||
*/
|
||||
* Moves entry1 to the position of entry2.
|
||||
*/
|
||||
public void moveEntry(VaultEntry entry1, VaultEntry entry2) {
|
||||
_vault.getEntries().move(entry1, entry2);
|
||||
}
|
||||
@@ -254,15 +260,54 @@ public class VaultRepository {
|
||||
return _vault.getEntries().getValues();
|
||||
}
|
||||
|
||||
public TreeSet<String> getGroups() {
|
||||
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
|
||||
public void addGroup(VaultGroup group) {
|
||||
_vault.getGroups().add(group);
|
||||
}
|
||||
|
||||
public VaultGroup getGroupByUUID(UUID uuid) {
|
||||
return _vault.getGroups().getByUUID(uuid);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public VaultGroup findGroupByUUID(UUID uuid) {
|
||||
return _vault.getGroups().has(uuid) ? _vault.getGroups().getByUUID(uuid) : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public VaultGroup findGroupByName(String name) {
|
||||
return _vault.getGroups().getValues()
|
||||
.stream()
|
||||
.filter(g -> g.getName().equals(name))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public void removeGroup(UUID groupUuid) {
|
||||
VaultGroup group = _vault.getGroups().getByUUID(groupUuid);
|
||||
removeGroup(group);
|
||||
}
|
||||
|
||||
public void removeGroup(VaultGroup group) {
|
||||
for (VaultEntry entry : getEntries()) {
|
||||
String group = entry.getGroup();
|
||||
if (group != null) {
|
||||
groups.add(group);
|
||||
}
|
||||
entry.removeGroup(group.getUUID());
|
||||
}
|
||||
return groups;
|
||||
|
||||
_vault.getGroups().remove(group);
|
||||
}
|
||||
|
||||
public Collection<VaultGroup> getGroups() {
|
||||
return _vault.getGroups().getValues();
|
||||
}
|
||||
|
||||
public Collection<VaultGroup> getUsedGroups() {
|
||||
Set<UUID> usedGroups = new HashSet<>();
|
||||
for (VaultEntry entry : getEntries()) {
|
||||
usedGroups.addAll(entry.getGroups());
|
||||
}
|
||||
|
||||
return getGroups().stream()
|
||||
.filter(vg -> usedGroups.contains(vg.getUUID()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public VaultFileCredentials getCredentials() {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
<translate
|
||||
android:duration="300"
|
||||
android:fromYDelta="0"
|
||||
android:toYDelta="100%" />
|
||||
android:fromYDelta="-200%"
|
||||
android:toYDelta="-100%" />
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="0.0"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#D7E3EF" android:state_checked="true" />
|
||||
<item android:color="@color/background" />
|
||||
</selector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/colorPrimary" android:state_checked="true" />
|
||||
<item android:color="@color/primary_text" />
|
||||
</selector>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/colorSecondary" android:state_enabled="true"/>
|
||||
<item android:alpha="0.38" android:color="@color/colorSecondary"/>
|
||||
<item android:color="?attr/colorSecondary" android:state_enabled="true"/>
|
||||
<item android:alpha="0.38" android:color="?attr/colorSecondary"/>
|
||||
</selector>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user