mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-06-30 12:49:55 +00:00
Compare commits
135 commits
Author | SHA1 | Date | |
---|---|---|---|
|
9d32a426f9 | ||
|
36bd8f945a | ||
|
ab9cdabddc | ||
|
d35c67d9d3 | ||
|
313b33793f | ||
|
94aabdcd88 | ||
|
d6b372bad5 | ||
|
af4e3d9566 | ||
|
8d667cd26c | ||
|
1335be6787 | ||
|
efaa6afda6 | ||
|
1c30557779 | ||
|
6f270144e2 | ||
|
62c0d273a5 | ||
|
3c4e8b44a0 | ||
|
63d2666230 | ||
|
6c40cfa748 | ||
|
6758ec498b | ||
|
819865230f | ||
|
97762fa146 | ||
|
60eea0bcb2 | ||
|
3818f9408d | ||
|
03c00d51ba | ||
|
afa1fbd3ae | ||
|
c81e08bf1f | ||
|
a5dc861336 | ||
|
fbfdd50069 | ||
|
19fe7bd4d2 | ||
|
7882ecc33a | ||
|
d39b44f0c3 | ||
|
7c6e3ae2a8 | ||
|
78ee38ba7d | ||
|
8ddf8c58da | ||
|
ce29d120a9 | ||
|
6bbb42fb83 | ||
|
e8d712ec71 | ||
|
ad2dc803fb | ||
|
3d50ab1b65 | ||
|
a4812c530d | ||
|
9ab949a59e | ||
|
e8bf7b0506 | ||
|
ec92fb2b31 | ||
|
919e6854e8 | ||
|
d98e23a1e5 | ||
|
e59df63e94 | ||
|
14643b4000 | ||
|
5439067e9f | ||
|
81a26ccad8 | ||
|
1fb36b0578 | ||
|
de74daef33 | ||
|
fe8b638818 | ||
|
05a415bb38 | ||
|
920df1d9be | ||
|
19a77209d8 | ||
|
aec16f22c1 | ||
|
a76f3394f3 | ||
|
6039cfa20e | ||
|
0eb1194578 | ||
|
170f626c9e | ||
|
c616a4f43c | ||
|
411a677fbf | ||
|
d48f2ead28 | ||
|
8147d07606 | ||
|
fa073371b5 | ||
|
3a9e27bacb | ||
|
51f656dd6b | ||
|
3efe74d375 | ||
|
f9ada47956 | ||
|
5213bafc97 | ||
|
503ce87c91 | ||
|
d2fcb24d79 | ||
|
337cb74f72 | ||
|
b5b29a4f84 | ||
|
3e3df919b2 | ||
|
79ba822ccf | ||
|
5f885cba29 | ||
|
5dba1db93d | ||
|
161b79f0d4 | ||
|
c250a17a19 | ||
|
843e5f1ab5 | ||
|
9d383b85d8 | ||
|
6d8eec0e21 | ||
|
fb8765f8f0 | ||
|
337d2c3507 | ||
|
8eabef2050 | ||
|
eb7b8881a0 | ||
|
b70654152d | ||
|
37ebcd3a4b | ||
|
9751a38ebd | ||
|
2ecde423a3 | ||
|
e8f06660dc | ||
|
c8d5be6462 | ||
|
bc29242f55 | ||
|
d395bbeb8d | ||
|
939fa0e1ec | ||
|
44358b3c95 | ||
|
413e793c7b | ||
|
e4c9a584f4 | ||
|
c9e8d4dbdf | ||
|
0573dbb2fc | ||
|
7753b482b1 | ||
|
cc5ce485b1 | ||
|
9131cae944 | ||
|
08d900c0c0 | ||
|
24d3d0ae8f | ||
|
45831e117c | ||
|
c559ed9e56 | ||
|
8e3279bb7e | ||
|
7ea2f5c4a5 | ||
|
9ef3315a70 | ||
|
4a9f189897 | ||
|
92de13b176 | ||
|
1e383463ae | ||
|
dbaec2d83f | ||
|
257a40eefa | ||
|
baa8068d51 | ||
|
d433957c2f | ||
|
6a54650635 | ||
|
bab59e8d04 | ||
|
83689a4c59 | ||
|
356fa8a36e | ||
|
58002c31ef | ||
|
d81d741fee | ||
|
17f106f70d | ||
|
91b632b9cf | ||
|
8b8e071831 | ||
|
9c151d83c1 | ||
|
e63ec4d1e8 | ||
|
f8603395fa | ||
|
df30e42318 | ||
|
6cb58789a2 | ||
|
eb6e26a8e4 | ||
|
8c1cc9a475 | ||
|
aab046ca04 | ||
|
d40e619cab |
221 changed files with 8484 additions and 2250 deletions
|
@ -1,10 +0,0 @@
|
|||
project_id: "372633"
|
||||
api_token: "<api-token-here>"
|
||||
base_path: "../app/src/main"
|
||||
base_url: "https://api.crowdin.com"
|
||||
preserve_hierarchy: true
|
||||
|
||||
files:
|
||||
- source: "res/values/strings.xml"
|
||||
dest: "strings.xml"
|
||||
translation: "res/values-%android_code%/%original_file_name%"
|
2
.github/workflows/build-app-workflow.yaml
vendored
2
.github/workflows/build-app-workflow.yaml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
|||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
- name: Tests
|
||||
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2
|
||||
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d
|
||||
with:
|
||||
api-level: 31
|
||||
arch: x86_64
|
||||
|
|
1
.github/workflows/codeql-analysis.yml
vendored
1
.github/workflows/codeql-analysis.yml
vendored
|
@ -14,6 +14,7 @@ jobs:
|
|||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
if: github.event_name != 'schedule' || github.repository == 'beemdevelopment/Aegis'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
|
14
.github/workflows/crowdin.yml
vendored
14
.github/workflows/crowdin.yml
vendored
|
@ -13,19 +13,13 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- name: Install crowdin-cli
|
||||
run: |
|
||||
wget https://github.com/crowdin/crowdin-cli/releases/download/3.7.2/crowdin-cli.zip
|
||||
echo "ee9f838b819ccedc33c9b2537055e5ba7d7934561b24df1e1a6274cbd6e27f2d crowdin-cli.zip" | sha256sum -c
|
||||
wget https://github.com/crowdin/crowdin-cli/releases/download/4.6.1/crowdin-cli.zip
|
||||
echo "7afd70de3a747ac631a5bad7866008163ae1d50c4606b5773f0b90a5481ffde2 crowdin-cli.zip" | sha256sum -c
|
||||
unzip crowdin-cli.zip -d crowdin-cli
|
||||
- name: Upload to Crowdin
|
||||
env:
|
||||
CROWDIN_TOKEN: "${{ secrets.CROWDIN_TOKEN }}"
|
||||
CROWDIN_PERSONAL_TOKEN: "${{ secrets.CROWDIN_TOKEN }}"
|
||||
run: |
|
||||
java -jar ./crowdin-cli/3.7.2/crowdin-cli.jar upload sources \
|
||||
java -jar ./crowdin-cli/4.6.1/crowdin-cli.jar upload sources \
|
||||
--no-progress \
|
||||
--token "$CROWDIN_TOKEN" \
|
||||
--project-id 372633 \
|
||||
--base-path app/src/main \
|
||||
--source res/values/strings.xml \
|
||||
--translation "res/values-%android_code%/%original_file_name%" \
|
||||
--dest strings.xml \
|
||||
--branch master
|
||||
|
|
8
FAQ.md
8
FAQ.md
|
@ -86,6 +86,14 @@ Another common setup is to configure Aegis to back up to a folder on local
|
|||
storage of your device and then have a separate app (like
|
||||
[Syncthing](https://syncthing.net/)) sync that folder anywhere you want.
|
||||
|
||||
## Encrypted Backups
|
||||
|
||||
### Why do I not get prompted to enter an encryption password when exporting?
|
||||
|
||||
Aegis uses the same password you have configured to encrypt your vault as the
|
||||
password which is used when exporting and importing your vault; so when prompted,
|
||||
you will enter that when importing your vault.
|
||||
|
||||
## Importing
|
||||
|
||||
### When importing from Authenticator Plus, an error is shown claiming that Accounts.txt is missing
|
||||
|
|
22
README.md
22
README.md
|
@ -123,24 +123,33 @@ documentation](docs/iconpacks.md).
|
|||
Unofficial monochrome-styled 2FA icons.
|
||||
|
||||
[<img width=500 alt="aegis-icons preview"
|
||||
src="https://raw.githubusercontent.com/aegis-icons/aegis-icons/master/showcase.png">](https://github.com/aegis-icons/aegis-icons)
|
||||
src="metadata/en-US/images/iconPacks/aegis-icons.png">](https://github.com/aegis-icons/aegis-icons)
|
||||
|
||||
- [delta-aegis-icons](https://github.com/Delta-Icons/aegis-icons)
|
||||
|
||||
Delta version of the unofficial monochrome-styled 2FA icon pack aegis-icons.
|
||||
|
||||
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons) *
|
||||
[<img width=500 alt="delta-icons preview"
|
||||
src="metadata/en-US/images/iconPacks/delta-icons.png">](https://github.com/Delta-Icons/aegis-icons)
|
||||
|
||||
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons) \*
|
||||
|
||||
This project periodically generates an icon pack for Aegis based on [Simple
|
||||
Icons](https://simpleicons.org/).
|
||||
|
||||
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) *
|
||||
[<img width=500 alt="aegis-simple-icons preview"
|
||||
src="metadata/en-US/images/iconPacks/aegis-simple-icons.png">](https://github.com/alexbakker/aegis-simple-icons)
|
||||
|
||||
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) \*
|
||||
|
||||
This is a variant on the aegis-simple-icons pack where the icons contain no solid background and just the outlines are being used.
|
||||
|
||||
|
||||
[<img width=500 alt="aegis-simple-icons-outlined preview"
|
||||
src="metadata/en-US/images/iconPacks/aegis-simple-icons-outlined.png">](https://github.com/michaelschattgen/aegis-simple-icons-outlined)
|
||||
|
||||
\* The icons are automatically generated, so
|
||||
not all of them are as high quality as the ones you'll find in
|
||||
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
|
||||
not all of them are as high quality as the ones you'll find in
|
||||
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@ -158,5 +167,6 @@ This project is licensed under the GNU General Public License v3.0. See the
|
|||
|
||||
A couple of libraries vendored in Aegis' repository are licensed under a
|
||||
different license:
|
||||
|
||||
- [TextDrawable](app/src/main/java/com/amulyakhare/textdrawable)
|
||||
- [TrustedIntents](app/src/main/java/info/guardianproject/trustedintents)
|
||||
|
|
|
@ -20,16 +20,16 @@ def fileProviderAuthority = "${packageName}.fileprovider"
|
|||
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
compileSdk 35
|
||||
|
||||
namespace packageName
|
||||
|
||||
defaultConfig {
|
||||
applicationId "${packageName}"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode 71
|
||||
versionName "3.2"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 35
|
||||
versionCode 79
|
||||
versionName "3.4"
|
||||
multiDexEnabled true
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
|
||||
|
@ -93,22 +93,32 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
// Required to make the APK reproducible
|
||||
aaptOptions {
|
||||
cruncherEnabled = false
|
||||
}
|
||||
defaultConfig {
|
||||
vectorDrawables.generatedDensities = []
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
// R8 doesn't remove these resources, so exclude them manually. This reduces APK size by 4MB.
|
||||
resources {
|
||||
excludes += ['/org/bouncycastle/pqc/**/*.properties']
|
||||
excludes += [
|
||||
'/org/bouncycastle/pqc/**/*.properties',
|
||||
'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility 1.8
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
lint {
|
||||
abortOnError true
|
||||
checkDependencies true
|
||||
disable 'MissingQuantity', 'MissingTranslation'
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
|
@ -142,32 +152,32 @@ aboutLibraries {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
def cameraxVersion = '1.3.4'
|
||||
def cameraxVersion = '1.4.2'
|
||||
def glideVersion = '4.16.0'
|
||||
def guavaVersion = '33.2.1'
|
||||
def hiltVersion = '2.52'
|
||||
def guavaVersion = '33.4.8'
|
||||
def hiltVersion = '2.56.2'
|
||||
def junitVersion = '4.13.2'
|
||||
def libsuVersion = '6.0.0'
|
||||
def roomVersion = "2.6.1"
|
||||
def roomVersion = '2.7.1'
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.8.2'
|
||||
annotationProcessor 'androidx.annotation:annotation:1.9.1'
|
||||
annotationProcessor "androidx.room:room-compiler:$roomVersion"
|
||||
annotationProcessor "com.google.dagger:hilt-compiler:$hiltVersion"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.activity:activity:1.9.1'
|
||||
implementation 'androidx.activity:activity:1.10.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.camera:camera-camera2:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-view:$cameraxVersion"
|
||||
implementation 'androidx.core:core:1.13.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.8.4'
|
||||
implementation 'androidx.core:core:1.16.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation 'androidx.documentfile:documentfile:1.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.9.0'
|
||||
implementation "androidx.preference:preference:1.2.1"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.4.0'
|
||||
implementation "androidx.room:room-runtime:$roomVersion"
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
|
@ -182,34 +192,34 @@ dependencies {
|
|||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
implementation "com.google.guava:guava:${guavaVersion}-android"
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:4.27.3'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:4.31.0'
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
implementation('com.mikepenz:aboutlibraries:11.2.2') {
|
||||
implementation('com.mikepenz:aboutlibraries:11.2.3') {
|
||||
exclude group: 'com.mikepenz', module: 'aboutlibraries-core'
|
||||
}
|
||||
implementation 'com.mikepenz:aboutlibraries-core-android:11.2.2'
|
||||
implementation 'com.mikepenz:aboutlibraries-core-android:11.2.3'
|
||||
implementation 'com.nulab-inc:zxcvbn:1.9.0'
|
||||
implementation 'net.lingala.zip4j:zip4j:2.11.5'
|
||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
|
||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.80'
|
||||
implementation 'org.simpleflatmapper:sfm-csv:8.2.3'
|
||||
|
||||
androidTestAnnotationProcessor "com.google.dagger:hilt-android-compiler:$hiltVersion"
|
||||
androidTestImplementation "com.google.dagger:hilt-android-testing:$hiltVersion"
|
||||
androidTestImplementation 'androidx.test:core:1.6.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.6.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
|
||||
androidTestImplementation "junit:junit:${junitVersion}"
|
||||
androidTestUtil 'androidx.test:orchestrator:1.5.0'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.5.1'
|
||||
|
||||
testImplementation 'androidx.test:core:1.6.1'
|
||||
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
|
||||
testImplementation "junit:junit:${junitVersion}"
|
||||
testImplementation 'org.json:json:20240303'
|
||||
testImplementation 'org.robolectric:robolectric:4.13'
|
||||
testImplementation 'org.json:json:20250517'
|
||||
testImplementation 'org.robolectric:robolectric:4.14.1'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||
}
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="MissingTranslation" severity="ignore" />
|
||||
<issue id="MissingQuantity" severity="ignore" />
|
||||
<issue id="InvalidPackage">
|
||||
<ignore regexp="X509LDAPCertStoreSpi" />
|
||||
</issue>
|
||||
<issue id="NotificationPermission">
|
||||
<ignore regexp="com.bumptech.glide.request.target.NotificationTarget" />
|
||||
</issue>
|
||||
<issue id="ResourceType" severity="Warning" />
|
||||
<issue id="UnusedResources" severity="error">
|
||||
<ignore path="res/raw/aboutlibraries.json" />
|
||||
<ignore regexp="res/mipmap.*/ic_launcher_debug.*.png" />
|
||||
</issue>
|
||||
</lint>
|
||||
|
|
|
@ -3,8 +3,8 @@ package com.beemdevelopment.aegis;
|
|||
import android.app.Application;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.runner.AndroidJUnitRunner;
|
||||
|
||||
|
|
|
@ -61,13 +61,20 @@ import org.junit.Test;
|
|||
import org.junit.rules.RuleChain;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
@ -183,7 +190,9 @@ public class BackupExportTest extends AegisTest {
|
|||
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withId(R.id.checkbox_accept)).perform(click());
|
||||
doExport();
|
||||
File file = doExport();
|
||||
|
||||
checkHtmlExport(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -196,7 +205,9 @@ public class BackupExportTest extends AegisTest {
|
|||
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(withId(R.id.checkbox_accept)).perform(click());
|
||||
doExport();
|
||||
File file = doExport();
|
||||
|
||||
checkHtmlExport(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -380,6 +391,26 @@ public class BackupExportTest extends AegisTest {
|
|||
checkReadEntries(entries);
|
||||
}
|
||||
|
||||
private void checkHtmlExport(File file) {
|
||||
try (InputStream inStream = new FileInputStream(file)) {
|
||||
Reader inReader = new InputStreamReader(inStream, StandardCharsets.UTF_8);
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
XmlPullParser parser = factory.newPullParser();
|
||||
parser.setInput(inReader);
|
||||
while (parser.getEventType() != XmlPullParser.START_TAG) {
|
||||
parser.next();
|
||||
}
|
||||
if (!parser.getName().toLowerCase(Locale.ROOT).equals("html")) {
|
||||
throw new RuntimeException("not an html document!");
|
||||
}
|
||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||
parser.next();
|
||||
}
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
throw new RuntimeException("Unable to read html export file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkReadEntries(Collection<VaultEntry> entries) {
|
||||
List<VaultEntry> vectors = VaultEntries.get();
|
||||
assertEquals(vectors.size(), entries.size());
|
||||
|
|
|
@ -177,12 +177,10 @@ public class OverallTest extends AegisTest {
|
|||
}
|
||||
|
||||
private void changeGroupFilter(String text) {
|
||||
onView(withId(R.id.chip_group)).perform(click());
|
||||
if (text == null) {
|
||||
onView(withId(R.id.btnClear)).perform(click());
|
||||
onView(allOf(withText(R.string.no_group), isDescendantOfA(withId(R.id.groupChipGroup)))).perform(click());
|
||||
} else {
|
||||
onView(withText(text)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
onView(allOf(withText(text), isDescendantOfA(withId(R.id.groupChipGroup)))).perform(click());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- NOTE: Disabled for now. See issue: #1047
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
@ -27,6 +28,7 @@
|
|||
android:icon="@mipmap/${iconName}"
|
||||
android:label="Aegis"
|
||||
android:supportsRtl="true"
|
||||
android:largeHeap="true"
|
||||
android:theme="@style/Theme.Aegis.Launch"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity android:name=".ui.TransferEntriesActivity"
|
||||
|
@ -149,7 +151,7 @@
|
|||
</application>
|
||||
|
||||
<queries>
|
||||
<package android:name="me.jmh.authenticatorpro" />
|
||||
<package android:name="com.stratumauth.app" />
|
||||
<package android:name="com.authy.authy" />
|
||||
<package android:name="org.fedorahosted.freeotp" />
|
||||
<package android:name="org.liberty.android.freeotpplus" />
|
||||
|
|
|
@ -31,6 +31,82 @@
|
|||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 3.4</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Haptic feedback when an entry is about to expire</li>
|
||||
<li>Brightness increase is now toggleable in the entry transfer view</li>
|
||||
<li>Filter on multiple groups simultaneously</li>
|
||||
<li>Color contrast on hidden codes has been improved</li>
|
||||
<li>Prompt before the user is about to save an entry with a duplicate name/issuer combination</li>
|
||||
<li>New languages: Estonian, Korean, Malayalam, Norwegian (Bokmål) and Serbian</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>A crash could occur if an entry with period 7 exists and code expiry indication is enabled</li>
|
||||
<li>The Portuguese (Brazilian) locale was used even if Portuguese was configured</li>
|
||||
<li>FreeOTP import would fail if the algorithm or digits field was not specified for an entry</li>
|
||||
<li>The divider between entries would be missing in certain filter configurations</li>
|
||||
<li>The snackbar in try entry importing view could obstruct the name of an entry</li>
|
||||
</ul>
|
||||
<h4>Miscellaneous</h4>
|
||||
<ul>
|
||||
<li>Android 6 or newer is now required the run the app</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.4</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Icons are now resized to 512x512 to reduce the size of the vault file and to reduce the chance of encountering out of memory conditions</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.3</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Some users ran into out of memory conditions due to large icons in their vault file. We've introduced a temporary measure that should help in most cases, but we'll follow up with a more comprehensive fix soon.</li>
|
||||
<li>Window insets were not always applied correctly, causing parts of the UI to appear off-screen</li>
|
||||
<li>The 2FAS importer did not tolerate spaces for secrets and was not always able to extract the issuer</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Find entries by searching in multiple fields simultaneously</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Entries would not actually be added to the Aegis vault in some cases when importing from Google Authenticator export QR codes</li>
|
||||
<li>The lock button was sometimes shown for unencrypted vaults</li>
|
||||
<li>The sort category menu item did not always reflect the current sorting</li>
|
||||
<li>The next code was not always easy to read because its color had low contrast with the background</li>
|
||||
<li>Entry selection was not cancelled when changing the group filter</li>
|
||||
</ul>
|
||||
<h3>Version 3.3.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Codes were not shown in case the tiles view mode was combined with hidden account names</li>
|
||||
</ul>
|
||||
<h3>Version 3.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Significant improvements to group filtering
|
||||
<ul>
|
||||
<li>Groups can now be filtered on straight from the main view instead of through a dialog</li>
|
||||
<li>Ability to assign multiple entries to a group in one go</li>
|
||||
<li>Support for reordering groups</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Codes now change color when they're about to expire</li>
|
||||
<li>Option to show the next code ahead of time</li>
|
||||
<li>Support for backing up to a single file (This enables support for more cloud providers, such as Google Drive)</li>
|
||||
<li>Various minor improvements to make QR code exports easier to scan</li>
|
||||
<li>Support for importing from Ente Auth</li>
|
||||
<li>Support for importing FreeOTP 2 backups</li>
|
||||
<li>Updated translations</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>QR codes exported for Google Authenticator could not be scanned on iOS</li>
|
||||
<li>The code would be copied after a single tap in case "Tap to reveal" and "Copy tokens to the clipboard" were enabled simultaneously</li>
|
||||
<li>Various other minor UI, stability and performance improvements</li>
|
||||
</ul>
|
||||
<h3>Version 3.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
|
|
|
@ -17,23 +17,14 @@ import androidx.lifecycle.Lifecycle;
|
|||
import androidx.lifecycle.LifecycleEventObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import androidx.room.Room;
|
||||
|
||||
import com.beemdevelopment.aegis.database.AppDatabase;
|
||||
import com.beemdevelopment.aegis.database.AuditLogEntry;
|
||||
import com.beemdevelopment.aegis.database.AuditLogRepository;
|
||||
import com.beemdevelopment.aegis.receivers.VaultLockReceiver;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import org.checkerframework.checker.units.qual.A;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import dagger.hilt.InstallIn;
|
||||
import dagger.hilt.android.EarlyEntryPoint;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum BackupsVersioningStrategy {
|
||||
UNDEFINED,
|
||||
MULTIPLE_BACKUPS,
|
||||
SINGLE_BACKUP
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum GroupPlaceholderType {
|
||||
ALL,
|
||||
NEW_GROUP,
|
||||
NO_GROUP;
|
||||
|
||||
public int getStringRes() {
|
||||
switch (this) {
|
||||
case ALL:
|
||||
return R.string.all;
|
||||
case NEW_GROUP:
|
||||
return R.string.new_group;
|
||||
case NO_GROUP:
|
||||
return R.string.no_group;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unexpected placeholder type: " + this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,9 +5,11 @@ import android.content.SharedPreferences;
|
|||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.provider.DocumentsContractCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.util.TimeUtils;
|
||||
|
@ -84,10 +86,18 @@ public class Preferences {
|
|||
return _prefs.getBoolean("pref_tap_to_reveal", false);
|
||||
}
|
||||
|
||||
public boolean isGroupMultiselectEnabled() {
|
||||
return _prefs.getBoolean("pref_groups_multiselect", false);
|
||||
}
|
||||
|
||||
public boolean isEntryHighlightEnabled() {
|
||||
return _prefs.getBoolean("pref_highlight_entry", false);
|
||||
}
|
||||
|
||||
public boolean isHapticFeedbackEnabled() {
|
||||
return _prefs.getBoolean("pref_haptic_feedback", true);
|
||||
}
|
||||
|
||||
public boolean isPauseFocusedEnabled() {
|
||||
boolean dependenciesEnabled = isTapToRevealEnabled() || isEntryHighlightEnabled();
|
||||
if (!dependenciesEnabled) return false;
|
||||
|
@ -153,6 +163,14 @@ public class Preferences {
|
|||
return _prefs.getBoolean("pref_show_icons", true);
|
||||
}
|
||||
|
||||
public boolean getShowNextCode() {
|
||||
return _prefs.getBoolean("pref_show_next_code", false);
|
||||
}
|
||||
|
||||
public boolean getShowExpirationState() {
|
||||
return _prefs.getBoolean("pref_expiration_state", true);
|
||||
}
|
||||
|
||||
public CodeGrouping getCodeGroupSize() {
|
||||
String value = _prefs.getString("pref_code_group_size_string", "GROUPING_THREES");
|
||||
|
||||
|
@ -589,6 +607,19 @@ public class Preferences {
|
|||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public BackupsVersioningStrategy getBackupVersioningStrategy() {
|
||||
Uri uri = getBackupsLocation();
|
||||
if (uri == null) {
|
||||
return BackupsVersioningStrategy.UNDEFINED;
|
||||
}
|
||||
if (DocumentsContractCompat.isTreeUri(uri)) {
|
||||
return BackupsVersioningStrategy.MULTIPLE_BACKUPS;
|
||||
} else {
|
||||
return BackupsVersioningStrategy.SINGLE_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
public static class BackupResult {
|
||||
private final Date _time;
|
||||
private boolean _isBuiltIn;
|
||||
|
|
|
@ -14,10 +14,4 @@ public class ThemeMap {
|
|||
Theme.DARK, R.style.Theme_Aegis_Dark,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_Amoled
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.Theme_Aegis_Light_Fullscreen,
|
||||
Theme.DARK, R.style.Theme_Aegis_Dark_Fullscreen,
|
||||
Theme.AMOLED, R.style.Theme_Aegis_Amoled_Fullscreen
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package com.beemdevelopment.aegis;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class VibrationPatterns {
|
||||
public static final long[] EXPIRING = {475, 20, 5, 20, 965, 20, 5, 20, 965, 20, 5, 20, 420};
|
||||
public static final long[] REFRESH_CODE = {0, 100};
|
||||
|
||||
public static long getLengthInMillis(long[] pattern) {
|
||||
return Arrays.stream(pattern).sum();
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.bc.SCrypt;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
@ -23,7 +21,6 @@ import javax.crypto.KeyGenerator;
|
|||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class CryptoUtils {
|
||||
|
@ -66,13 +63,7 @@ public class CryptoUtils {
|
|||
// generate the nonce if none is given
|
||||
// we are not allowed to do this ourselves as "setRandomizedEncryptionRequired" is set to true
|
||||
if (nonce != null) {
|
||||
AlgorithmParameterSpec spec;
|
||||
// apparently kitkat doesn't support GCMParameterSpec
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
spec = new IvParameterSpec(nonce);
|
||||
} else {
|
||||
spec = new GCMParameterSpec(CRYPTO_AEAD_TAG_SIZE * 8, nonce);
|
||||
}
|
||||
AlgorithmParameterSpec spec = new GCMParameterSpec(CRYPTO_AEAD_TAG_SIZE * 8, nonce);
|
||||
cipher.init(opmode, key, spec);
|
||||
} else {
|
||||
cipher.init(opmode, key);
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package com.beemdevelopment.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
|
@ -45,10 +42,6 @@ public class KeyStoreHandle {
|
|||
}
|
||||
|
||||
public SecretKey generateKey(String id) throws KeyStoreHandleException {
|
||||
if (!isSupported()) {
|
||||
throw new KeyStoreHandleException("Symmetric KeyStore keys are not supported in this version of Android");
|
||||
}
|
||||
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME);
|
||||
generator.init(new KeyGenParameterSpec.Builder(id,
|
||||
|
@ -87,14 +80,13 @@ public class KeyStoreHandle {
|
|||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
|
||||
if (isSupported() && isKeyPermanentlyInvalidated(key)) {
|
||||
if (isKeyPermanentlyInvalidated(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
|
||||
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
|
||||
try {
|
||||
|
@ -127,8 +119,4 @@ public class KeyStoreHandle {
|
|||
throw new KeyStoreHandleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,10 @@ public class YAOTP {
|
|||
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||
long seconds = System.currentTimeMillis() / 1000;
|
||||
return generateOTP(secret, pin, digits, otpAlgo, seconds, period);
|
||||
return generateOTP(secret, pin, digits, otpAlgo, period, seconds);
|
||||
}
|
||||
|
||||
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long seconds, long period)
|
||||
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period, long seconds)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||
byte[] pinWithHash;
|
||||
byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8);
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BitmapHelper {
|
||||
private BitmapHelper() {
|
||||
|
@ -28,4 +35,29 @@ public class BitmapHelper {
|
|||
|
||||
return Bitmap.createScaledBitmap(bitmap, width, height, true);
|
||||
}
|
||||
|
||||
public static boolean isVaultEntryIconOptimized(VaultEntryIcon icon) {
|
||||
BitmapFactory.Options opts = new BitmapFactory.Options();
|
||||
opts.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeByteArray(icon.getBytes(), 0, icon.getBytes().length, opts);
|
||||
return opts.outWidth <= VaultEntryIcon.MAX_DIMENS && opts.outHeight <= VaultEntryIcon.MAX_DIMENS;
|
||||
}
|
||||
|
||||
public static VaultEntryIcon toVaultEntryIcon(Bitmap bitmap, IconType iconType) {
|
||||
if (bitmap.getWidth() > VaultEntryIcon.MAX_DIMENS
|
||||
|| bitmap.getHeight() > VaultEntryIcon.MAX_DIMENS) {
|
||||
bitmap = resize(bitmap, VaultEntryIcon.MAX_DIMENS, VaultEntryIcon.MAX_DIMENS);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
if (Objects.equals(iconType, IconType.PNG)) {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
} else {
|
||||
iconType = IconType.JPEG;
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
}
|
||||
|
||||
byte[] data = stream.toByteArray();
|
||||
return new VaultEntryIcon(data, iconType);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||
int swipeFlags = 0;
|
||||
if (adapter.isPositionFooter(position)
|
||||
|| adapter.isPositionErrorCard(position)
|
||||
|| adapter.getEntryAtPos(position) != _selectedEntry
|
||||
|| adapter.getEntryAtPosition(position) != _selectedEntry
|
||||
|| !isLongPressDragEnabled()) {
|
||||
return makeMovementFlags(0, swipeFlags);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,10 @@ public class ThemeHelper {
|
|||
DynamicColorsOptions.Builder optsBuilder = new DynamicColorsOptions.Builder();
|
||||
if (getConfiguredTheme().equals(Theme.AMOLED)) {
|
||||
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Amoled);
|
||||
} else if (getConfiguredTheme().equals(Theme.DARK)) {
|
||||
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Dark);
|
||||
}
|
||||
|
||||
DynamicColors.applyToActivityIfAvailable(_activity, optsBuilder.build());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.helpers;
|
|||
|
||||
import android.os.Handler;
|
||||
|
||||
import com.beemdevelopment.aegis.VibrationPatterns;
|
||||
|
||||
public class UiRefresher {
|
||||
private boolean _running;
|
||||
private Listener _listener;
|
||||
|
@ -23,7 +25,6 @@ public class UiRefresher {
|
|||
}
|
||||
_running = true;
|
||||
|
||||
_listener.onRefresh();
|
||||
_handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
@ -31,6 +32,27 @@ public class UiRefresher {
|
|||
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
|
||||
}
|
||||
}, _listener.getMillisTillNextRefresh());
|
||||
|
||||
_handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
_listener.onExpiring();
|
||||
_handler.postDelayed(this, getNextRun());
|
||||
}
|
||||
}, getInitialRun());
|
||||
}
|
||||
|
||||
private long getInitialRun() {
|
||||
long sum = _listener.getMillisTillNextRefresh() - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
|
||||
if (sum < 0) {
|
||||
return getNextRun();
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
private long getNextRun() {
|
||||
return (_listener.getMillisTillNextRefresh() + _listener.getPeriodMillis()) - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
|
@ -40,6 +62,8 @@ public class UiRefresher {
|
|||
|
||||
public interface Listener {
|
||||
void onRefresh();
|
||||
void onExpiring();
|
||||
long getMillisTillNextRefresh();
|
||||
long getPeriodMillis();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.os.VibratorManager;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
|
||||
public class VibrationHelper {
|
||||
private Preferences _preferences;
|
||||
|
||||
public VibrationHelper(Context context) {
|
||||
_preferences = new Preferences(context);
|
||||
}
|
||||
|
||||
public void vibratePattern(Context context, long[] pattern) {
|
||||
if (!isHapticFeedbackEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
VibratorManager vibratorManager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
|
||||
if (vibratorManager != null) {
|
||||
Vibrator vibrator = vibratorManager.getDefaultVibrator();
|
||||
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
|
||||
vibrator.vibrate(effect);
|
||||
}
|
||||
} else {
|
||||
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
if (vibrator != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
|
||||
vibrator.vibrate(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isHapticFeedbackEnabled() {
|
||||
return _preferences.isHapticFeedbackEnabled();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
|
||||
public class ViewHelper {
|
||||
private ViewHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static void setupAppBarInsets(AppBarLayout appBar) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(appBar, (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
targetView.setPadding(
|
||||
insets.left,
|
||||
insets.top,
|
||||
insets.right,
|
||||
0
|
||||
);
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -34,17 +34,18 @@ public abstract class DatabaseImporter {
|
|||
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
|
||||
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
|
||||
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
|
||||
_importers.add(new Definition("Authenticator Pro", AuthenticatorProImporter.class, R.string.importer_help_authenticator_pro, true));
|
||||
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
|
||||
_importers.add(new Definition("Battle.net Authenticator", BattleNetImporter.class, R.string.importer_help_battle_net_authenticator, true));
|
||||
_importers.add(new Definition("Bitwarden", BitwardenImporter.class, R.string.importer_help_bitwarden, false));
|
||||
_importers.add(new Definition("Duo", DuoImporter.class, R.string.importer_help_duo, true));
|
||||
_importers.add(new Definition("FreeOTP (1.x)", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
|
||||
_importers.add(new Definition("Ente Auth", EnteAuthImporter.class, R.string.importer_help_ente_auth, false));
|
||||
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
|
||||
_importers.add(new Definition("FreeOTP+ (JSON)", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
|
||||
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
|
||||
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
|
||||
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
|
||||
_importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true));
|
||||
_importers.add(new Definition("Stratum (Authenticator Pro)", StratumImporter.class, R.string.importer_help_stratum, true));
|
||||
_importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
|
||||
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class EnteAuthImporter extends DatabaseImporter {
|
||||
public EnteAuthImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] bytes = IOUtils.readAll(stream);
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(requireContext());
|
||||
return importer.read(new ByteArrayInputStream(bytes), isInternal);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,26 +4,53 @@ import android.content.Context;
|
|||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.ContextHelper;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Encodable;
|
||||
import org.bouncycastle.asn1.ASN1OctetString;
|
||||
import org.bouncycastle.asn1.ASN1Primitive;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class FreeOtpImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs/tokens.xml";
|
||||
|
@ -40,6 +67,24 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
|
||||
@Override
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try (BufferedInputStream bufInStream = new BufferedInputStream(stream);
|
||||
DataInputStream dataInStream = new DataInputStream(bufInStream)) {
|
||||
|
||||
dataInStream.mark(2);
|
||||
int magic = dataInStream.readUnsignedShort();
|
||||
dataInStream.reset();
|
||||
|
||||
if (magic == SerializedHashMapParser.MAGIC) {
|
||||
return readV2(dataInStream);
|
||||
} else {
|
||||
return readV1(bufInStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private DecryptedStateV1 readV1(InputStream stream) throws DatabaseImporterException {
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
|
@ -52,16 +97,184 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
entries.add(new JSONObject(entry.Value));
|
||||
}
|
||||
}
|
||||
return new State(entries);
|
||||
return new DecryptedStateV1(entries);
|
||||
} catch (XmlPullParserException | IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private List<JSONObject> _entries;
|
||||
private EncryptedState readV2(DataInputStream stream) throws DatabaseImporterException {
|
||||
try {
|
||||
Map<String, String> entries = SerializedHashMapParser.parse(stream);
|
||||
JSONObject mkObj = new JSONObject(entries.get("masterKey"));
|
||||
return new EncryptedState(mkObj, entries);
|
||||
} catch (IOException | JSONException | SerializedHashMapParser.ParseException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public State(List<JSONObject> entries) {
|
||||
public static class EncryptedState extends State {
|
||||
private static final int MASTER_KEY_SIZE = 32 * 8;
|
||||
|
||||
private final String _mkAlgo;
|
||||
private final String _mkCipher;
|
||||
private final byte[] _mkCipherText;
|
||||
private final byte[] _mkParameters;
|
||||
private final byte[] _mkToken;
|
||||
private final byte[] _mkSalt;
|
||||
private final int _mkIterations;
|
||||
private final Map<String, String> _entries;
|
||||
|
||||
private EncryptedState(JSONObject mkObj, Map<String, String> entries)
|
||||
throws DatabaseImporterException, JSONException {
|
||||
super(true);
|
||||
|
||||
_mkAlgo = mkObj.getString("mAlgorithm");
|
||||
if (!_mkAlgo.equals("PBKDF2withHmacSHA1") && !_mkAlgo.equals("PBKDF2withHmacSHA512")) {
|
||||
throw new DatabaseImporterException(String.format("Unexpected master key KDF: %s", _mkAlgo));
|
||||
}
|
||||
JSONObject keyObj = mkObj.getJSONObject("mEncryptedKey");
|
||||
_mkCipher = keyObj.getString("mCipher");
|
||||
if (!_mkCipher.equals("AES/GCM/NoPadding")) {
|
||||
throw new DatabaseImporterException(String.format("Unexpected master key cipher: %s", _mkCipher));
|
||||
}
|
||||
_mkCipherText = toBytes(keyObj.getJSONArray("mCipherText"));
|
||||
_mkParameters = toBytes(keyObj.getJSONArray("mParameters"));
|
||||
_mkToken = keyObj.getString("mToken").getBytes(StandardCharsets.UTF_8);
|
||||
_mkSalt = toBytes(mkObj.getJSONArray("mSalt"));
|
||||
_mkIterations = mkObj.getInt("mIterations");
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
public State decrypt(char[] password) throws DatabaseImporterException {
|
||||
PBKDFTask.Params params = new PBKDFTask.Params(_mkAlgo, MASTER_KEY_SIZE, password, _mkSalt, _mkIterations);
|
||||
SecretKey passKey = PBKDFTask.deriveKey(params);
|
||||
return decrypt(passKey);
|
||||
}
|
||||
|
||||
public State decrypt(SecretKey passKey) throws DatabaseImporterException {
|
||||
byte[] masterKeyBytes;
|
||||
try {
|
||||
byte[] nonce = parseNonce(_mkParameters);
|
||||
IvParameterSpec spec = new IvParameterSpec(nonce);
|
||||
Cipher cipher = Cipher.getInstance(_mkCipher);
|
||||
cipher.init(Cipher.DECRYPT_MODE, passKey, spec);
|
||||
cipher.updateAAD(_mkToken);
|
||||
masterKeyBytes = cipher.doFinal(_mkCipherText);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException |
|
||||
IllegalBlockSizeException | InvalidKeyException |
|
||||
InvalidAlgorithmParameterException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
SecretKey masterKey = new SecretKeySpec(masterKeyBytes, 0, masterKeyBytes.length, "AES");
|
||||
return new DecryptedStateV2(_entries, masterKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
|
||||
.setTitle(R.string.importer_warning_title_freeotp2)
|
||||
.setMessage(R.string.importer_warning_message_freeotp2)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, password -> {
|
||||
PBKDFTask.Params params = getKeyDerivationParams(password, _mkAlgo);
|
||||
PBKDFTask task = new PBKDFTask(context, key -> {
|
||||
try {
|
||||
State state = decrypt(key);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
|
||||
task.execute(lifecycle, params);
|
||||
}, dialog1 -> listener.onCanceled());
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
private PBKDFTask.Params getKeyDerivationParams(char[] password, String algo) {
|
||||
return new PBKDFTask.Params(algo, MASTER_KEY_SIZE, password, _mkSalt, _mkIterations);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedStateV2 extends DatabaseImporter.State {
|
||||
private final Map<String, String> _entries;
|
||||
private final SecretKey _masterKey;
|
||||
|
||||
public DecryptedStateV2(Map<String, String> entries, SecretKey masterKey) {
|
||||
super(false);
|
||||
_entries = entries;
|
||||
_masterKey = masterKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() throws DatabaseImporterException {
|
||||
Result result = new Result();
|
||||
|
||||
for (Map.Entry<String, String> entry : _entries.entrySet()) {
|
||||
if (entry.getKey().endsWith("-token") || entry.getKey().equals("masterKey")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject encObj = new JSONObject(entry.getValue());
|
||||
String tokenKey = String.format("%s-token", entry.getKey());
|
||||
JSONObject tokenObj = new JSONObject(_entries.get(tokenKey));
|
||||
|
||||
VaultEntry vaultEntry = convertEntry(encObj, tokenObj);
|
||||
result.addEntry(vaultEntry);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private VaultEntry convertEntry(JSONObject encObj, JSONObject tokenObj)
|
||||
throws DatabaseImporterEntryException {
|
||||
try {
|
||||
JSONObject keyObj = new JSONObject(encObj.getString("key"));
|
||||
String cipherName = keyObj.getString("mCipher");
|
||||
if (!cipherName.equals("AES/GCM/NoPadding")) {
|
||||
throw new DatabaseImporterException(String.format("Unexpected cipher: %s", cipherName));
|
||||
}
|
||||
byte[] cipherText = toBytes(keyObj.getJSONArray("mCipherText"));
|
||||
byte[] parameters = toBytes(keyObj.getJSONArray("mParameters"));
|
||||
byte[] token = keyObj.getString("mToken").getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] nonce = parseNonce(parameters);
|
||||
IvParameterSpec spec = new IvParameterSpec(nonce);
|
||||
Cipher cipher = Cipher.getInstance(cipherName);
|
||||
cipher.init(Cipher.DECRYPT_MODE, _masterKey, spec);
|
||||
cipher.updateAAD(token);
|
||||
byte[] secretBytes = cipher.doFinal(cipherText);
|
||||
|
||||
JSONArray secretArray = new JSONArray();
|
||||
for (byte b : secretBytes) {
|
||||
secretArray.put(b);
|
||||
}
|
||||
tokenObj.put("secret", secretArray);
|
||||
|
||||
return DecryptedStateV1.convertEntry(tokenObj);
|
||||
} catch (DatabaseImporterException | JSONException | NoSuchAlgorithmException |
|
||||
NoSuchPaddingException | InvalidAlgorithmParameterException |
|
||||
InvalidKeyException | BadPaddingException | IllegalBlockSizeException |
|
||||
IOException e) {
|
||||
throw new DatabaseImporterEntryException(e, tokenObj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class DecryptedStateV1 extends DatabaseImporter.State {
|
||||
private final List<JSONObject> _entries;
|
||||
|
||||
public DecryptedStateV1(List<JSONObject> entries) {
|
||||
super(false);
|
||||
_entries = entries;
|
||||
}
|
||||
|
@ -85,8 +298,8 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
String type = obj.getString("type").toLowerCase(Locale.ROOT);
|
||||
String algo = obj.getString("algo");
|
||||
int digits = obj.getInt("digits");
|
||||
String algo = obj.optString("algo", OtpInfo.DEFAULT_ALGORITHM);
|
||||
int digits = obj.optInt("digits", OtpInfo.DEFAULT_DIGITS);
|
||||
byte[] secret = toBytes(obj.getJSONArray("secret"));
|
||||
|
||||
String issuer = obj.getString("issuerExt");
|
||||
|
@ -95,7 +308,7 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
OtpInfo info;
|
||||
switch (type) {
|
||||
case "totp":
|
||||
int period = obj.getInt("period");
|
||||
int period = obj.optInt("period", TotpInfo.DEFAULT_PERIOD);
|
||||
if (issuer.equals("Steam")) {
|
||||
info = new SteamInfo(secret, algo, digits, period);
|
||||
} else {
|
||||
|
@ -116,6 +329,23 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
}
|
||||
}
|
||||
|
||||
private static byte[] parseNonce(byte[] parameters) throws IOException {
|
||||
ASN1Primitive prim = ASN1Sequence.fromByteArray(parameters);
|
||||
if (prim instanceof ASN1OctetString) {
|
||||
return ((ASN1OctetString) prim).getOctets();
|
||||
}
|
||||
|
||||
if (prim instanceof ASN1Sequence) {
|
||||
for (ASN1Encodable enc : (ASN1Sequence) prim) {
|
||||
if (enc instanceof ASN1OctetString) {
|
||||
return ((ASN1OctetString) enc).getOctets();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("Unable to find nonce in parameters");
|
||||
}
|
||||
|
||||
private static byte[] toBytes(JSONArray array) throws JSONException {
|
||||
byte[] bytes = new byte[array.length()];
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
|
@ -123,4 +353,119 @@ public class FreeOtpImporter extends DatabaseImporter {
|
|||
}
|
||||
return bytes;
|
||||
}
|
||||
private static class SerializedHashMapParser {
|
||||
private static final int MAGIC = 0xaced;
|
||||
private static final int VERSION = 5;
|
||||
private static final long SERIAL_VERSION_UID = 362498820763181265L;
|
||||
|
||||
private static final byte TC_NULL = 0x70;
|
||||
private static final byte TC_CLASSDESC = 0x72;
|
||||
private static final byte TC_OBJECT = 0x73;
|
||||
private static final byte TC_STRING = 0x74;
|
||||
|
||||
private SerializedHashMapParser() {
|
||||
|
||||
}
|
||||
|
||||
public static Map<String, String> parse(DataInputStream inStream)
|
||||
throws IOException, ParseException {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
|
||||
// Read/validate the magic number and version
|
||||
int magic = inStream.readUnsignedShort();
|
||||
int version = inStream.readUnsignedShort();
|
||||
if (magic != MAGIC || version != VERSION) {
|
||||
throw new ParseException("Not a serialized Java Object");
|
||||
}
|
||||
|
||||
// Read the class descriptor info for HashMap
|
||||
byte b = inStream.readByte();
|
||||
if (b != TC_OBJECT) {
|
||||
throw new ParseException("Expected an object, found: " + b);
|
||||
}
|
||||
b = inStream.readByte();
|
||||
if (b != TC_CLASSDESC) {
|
||||
throw new ParseException("Expected a class desc, found: " + b);
|
||||
}
|
||||
parseClassDescriptor(inStream);
|
||||
|
||||
// Not interested in the capacity of the map
|
||||
inStream.readInt();
|
||||
// Read the number of elements in the HashMap
|
||||
int size = inStream.readInt();
|
||||
|
||||
// Parse each key-value pair in the map
|
||||
for (int i = 0; i < size; i++) {
|
||||
String key = parseStringObject(inStream);
|
||||
String value = parseStringObject(inStream);
|
||||
map.put(key, value);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void parseClassDescriptor(DataInputStream inputStream)
|
||||
throws IOException, ParseException {
|
||||
// Check whether we're dealing with a HashMap and a version we support
|
||||
String className = parseUTF(inputStream);
|
||||
if (!className.equals(HashMap.class.getName())) {
|
||||
throw new ParseException(String.format("Unexpected class name: %s", className));
|
||||
}
|
||||
long serialVersionUID = inputStream.readLong();
|
||||
if (serialVersionUID != SERIAL_VERSION_UID) {
|
||||
throw new ParseException(String.format("Unexpected serial version UID: %d", serialVersionUID));
|
||||
}
|
||||
|
||||
// Read past all of the fields in the class
|
||||
byte fieldDescriptor = inputStream.readByte();
|
||||
if (fieldDescriptor == TC_NULL) {
|
||||
return;
|
||||
}
|
||||
int totalFieldSkip = 0;
|
||||
int fieldCount = inputStream.readUnsignedShort();
|
||||
for (int i = 0; i < fieldCount; i++) {
|
||||
char fieldType = (char) inputStream.readByte();
|
||||
parseUTF(inputStream);
|
||||
switch (fieldType) {
|
||||
case 'F': // float (4 bytes)
|
||||
case 'I': // int (4 bytes)
|
||||
totalFieldSkip += 4;
|
||||
break;
|
||||
default:
|
||||
throw new ParseException(String.format("Unexpected field type: %s", fieldType));
|
||||
}
|
||||
}
|
||||
inputStream.skipBytes(totalFieldSkip);
|
||||
|
||||
// Not sure what these bytes are, just skip them
|
||||
inputStream.skipBytes(4);
|
||||
}
|
||||
|
||||
private static String parseStringObject(DataInputStream inputStream)
|
||||
throws IOException, ParseException {
|
||||
byte objectType = inputStream.readByte();
|
||||
if (objectType != TC_STRING) {
|
||||
throw new ParseException(String.format("Expected a string object, found: %d", objectType));
|
||||
}
|
||||
|
||||
int length = inputStream.readUnsignedShort();
|
||||
byte[] strBytes = new byte[length];
|
||||
inputStream.readFully(strBytes);
|
||||
|
||||
return new String(strBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String parseUTF(DataInputStream inputStream) throws IOException {
|
||||
int length = inputStream.readUnsignedShort();
|
||||
byte[] strBytes = new byte[length];
|
||||
inputStream.readFully(strBytes);
|
||||
return new String(strBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static class ParseException extends Exception {
|
||||
public ParseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ public class FreeOtpPlusImporter extends DatabaseImporter {
|
|||
entries.add(array.getJSONObject(i));
|
||||
}
|
||||
|
||||
state = new FreeOtpImporter.State(entries);
|
||||
state = new FreeOtpImporter.DecryptedStateV1(entries);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
|
|
@ -45,11 +45,11 @@ import javax.crypto.NoSuchPaddingException;
|
|||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
public class AuthenticatorProImporter extends DatabaseImporter {
|
||||
public class StratumImporter extends DatabaseImporter {
|
||||
private static final String HEADER = "AUTHENTICATORPRO";
|
||||
private static final String HEADER_LEGACY = "AuthenticatorPro";
|
||||
private static final String PKG_NAME = "me.jmh.authenticatorpro";
|
||||
private static final String PKG_DB_PATH = "files/proauth.db3";
|
||||
private static final String PKG_NAME = "com.stratumauth.app";
|
||||
private static final String PKG_DB_PATH = "databases/authenticator.db3";
|
||||
|
||||
private enum Algorithm {
|
||||
SHA1,
|
||||
|
@ -57,7 +57,7 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
|||
SHA512
|
||||
}
|
||||
|
||||
public AuthenticatorProImporter(Context context) {
|
||||
public StratumImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
|
@ -169,7 +169,7 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
|||
Argon2Task.Params params = getKeyDerivationParams(password);
|
||||
Argon2Task task = new Argon2Task(context, key -> {
|
||||
try {
|
||||
AuthenticatorProImporter.JsonState state = decrypt(key);
|
||||
StratumImporter.JsonState state = decrypt(key);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
|
@ -244,7 +244,7 @@ public class AuthenticatorProImporter extends DatabaseImporter {
|
|||
PBKDFTask.Params params = getKeyDerivationParams(password);
|
||||
PBKDFTask task = new PBKDFTask(context, key -> {
|
||||
try {
|
||||
AuthenticatorProImporter.JsonState state = decrypt(key);
|
||||
StratumImporter.JsonState state = decrypt(key);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
|
@ -4,9 +4,9 @@ import android.content.Context;
|
|||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
|
@ -16,6 +16,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
|||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.common.base.Strings;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
|
@ -173,9 +174,12 @@ public class TwoFASImporter extends DatabaseImporter {
|
|||
|
||||
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret = Base32.decode(obj.getString("secret"));
|
||||
byte[] secret = GoogleAuthInfo.parseSecret(obj.getString("secret"));
|
||||
JSONObject info = obj.getJSONObject("otp");
|
||||
String issuer = info.optString("issuer");
|
||||
String issuer = obj.optString("name");
|
||||
if (Strings.isNullOrEmpty(issuer)) {
|
||||
issuer = info.optString("issuer");
|
||||
}
|
||||
String name = info.optString("account");
|
||||
int digits = info.optInt("digits", TotpInfo.DEFAULT_DIGITS);
|
||||
String algorithm = info.optString("algorithm", TotpInfo.DEFAULT_ALGORITHM);
|
||||
|
|
|
@ -30,20 +30,6 @@ public class MotpInfo extends TotpInfo {
|
|||
setPin(pin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOtp() {
|
||||
if (_pin == null) {
|
||||
throw new IllegalStateException("PIN must be set before generating an OTP");
|
||||
}
|
||||
|
||||
try {
|
||||
MOTP otp = MOTP.generateOTP(getSecret(), getAlgorithm(false), getDigits(), getPeriod(), getPin());
|
||||
return otp.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOtp(long time) {
|
||||
if (_pin == null) {
|
||||
|
|
|
@ -20,11 +20,11 @@ public class SteamInfo extends TotpInfo {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getOtp() throws OtpInfoException {
|
||||
public String getOtp(long time) throws OtpInfoException {
|
||||
checkSecret();
|
||||
|
||||
try {
|
||||
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
|
||||
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
|
||||
return otp.toSteamString();
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
|
@ -27,17 +27,12 @@ public class TotpInfo extends OtpInfo {
|
|||
|
||||
@Override
|
||||
public String getOtp() throws OtpInfoException {
|
||||
checkSecret();
|
||||
|
||||
try {
|
||||
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
|
||||
return otp.toString();
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return getOtp(System.currentTimeMillis() / 1000);
|
||||
}
|
||||
|
||||
public String getOtp(long time) {
|
||||
public String getOtp(long time) throws OtpInfoException {
|
||||
checkSecret();
|
||||
|
||||
try {
|
||||
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
|
||||
return otp.toString();
|
||||
|
|
|
@ -38,13 +38,13 @@ public class YandexInfo extends TotpInfo {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getOtp() {
|
||||
public String getOtp(long time) {
|
||||
if (_pin == null) {
|
||||
throw new IllegalStateException("PIN must be set before generating an OTP");
|
||||
}
|
||||
|
||||
try {
|
||||
YAOTP otp = YAOTP.generateOTP(getSecret(), getPin(), getDigits(), getAlgorithm(true), getPeriod());
|
||||
YAOTP otp = YAOTP.generateOTP(getSecret(), getPin(), getDigits(), getAlgorithm(true), getPeriod(), time);
|
||||
return otp.toString();
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.annotation.SuppressLint;
|
|||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -29,11 +28,7 @@ public class NotificationService extends Service {
|
|||
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
public void serviceMethod() {
|
||||
int flags = PendingIntent.FLAG_ONE_SHOT;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags |= PendingIntent.FLAG_IMMUTABLE;
|
||||
}
|
||||
|
||||
int flags = PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE;
|
||||
Intent intent = new Intent(this, VaultLockReceiver.class);
|
||||
intent.setAction(VaultLockReceiver.ACTION_LOCK_VAULT);
|
||||
intent.setPackage(BuildConfig.APPLICATION_ID);
|
||||
|
|
|
@ -13,11 +13,15 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.ChangelogDialog;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.LicenseDialog;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
public class AboutActivity extends AegisActivity {
|
||||
|
@ -39,6 +43,7 @@ public class AboutActivity extends AegisActivity {
|
|||
|
||||
setContentView(R.layout.activity_about);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
@ -90,6 +95,17 @@ public class AboutActivity extends AegisActivity {
|
|||
.setTheme(_themeHelper.getConfiguredTheme())
|
||||
.show(getSupportFragmentManager(), null);
|
||||
});
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.about_scroll_view), (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
targetView.setPadding(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
insets.bottom
|
||||
);
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
}
|
||||
|
||||
private static String getCurrentAppVersion() {
|
||||
|
|
|
@ -4,7 +4,7 @@ import android.annotation.SuppressLint;
|
|||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -12,7 +12,6 @@ import android.view.WindowManager;
|
|||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
|
@ -26,6 +25,7 @@ import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
|||
import com.beemdevelopment.aegis.icons.IconPackManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
@ -186,13 +186,9 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
|
|||
private class ActionModeStatusGuardHack {
|
||||
private Field _fadeAnimField;
|
||||
private Field _actionModeViewField;
|
||||
|
||||
@ColorInt
|
||||
private final int _statusBarColor;
|
||||
private Drawable _appBarBackground;
|
||||
|
||||
private ActionModeStatusGuardHack() {
|
||||
_statusBarColor = getWindow().getStatusBarColor();
|
||||
|
||||
try {
|
||||
_fadeAnimField = getDelegate().getClass().getDeclaredField("mFadeAnim");
|
||||
_fadeAnimField.setAccessible(true);
|
||||
|
@ -216,20 +212,26 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
|
|||
return;
|
||||
}
|
||||
|
||||
if (fadeAnim == null || actionModeView == null) {
|
||||
AppBarLayout appBarLayout = findViewById(R.id.app_bar_layout);
|
||||
if (appBarLayout != null && _appBarBackground == null) {
|
||||
_appBarBackground = appBarLayout.getBackground();
|
||||
}
|
||||
|
||||
if (fadeAnim == null || actionModeView == null || appBarLayout == null || _appBarBackground == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fadeAnim.cancel();
|
||||
|
||||
actionModeView.setVisibility(visibility);
|
||||
actionModeView.setAlpha(visibility == View.VISIBLE ? 1f : 0f);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
int statusBarColor = visibility == View.VISIBLE
|
||||
? MaterialColors.getColor(actionModeView, com.google.android.material.R.attr.colorSurfaceContainer)
|
||||
: _statusBarColor;
|
||||
getWindow().setStatusBarColor(statusBarColor);
|
||||
if (visibility == View.VISIBLE) {
|
||||
actionModeView.setVisibility(visibility);
|
||||
actionModeView.setAlpha(1f);
|
||||
int color = MaterialColors.getColor(appBarLayout, com.google.android.material.R.attr.colorSurfaceContainer);
|
||||
appBarLayout.setBackgroundColor(color);
|
||||
} else {
|
||||
actionModeView.setVisibility(visibility);
|
||||
actionModeView.setAlpha(0f);
|
||||
appBarLayout.setBackground(_appBarBackground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
|
|||
import com.beemdevelopment.aegis.ui.views.AssignIconAdapter;
|
||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.bumptech.glide.Glide;
|
||||
|
@ -61,6 +62,7 @@ public class AssignIconsActivity extends AegisActivity implements AssignIconAdap
|
|||
|
||||
setContentView(R.layout.activity_assign_icons);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
@ -27,6 +28,7 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.avito.android.krop.KropView;
|
||||
|
@ -35,12 +37,14 @@ import com.beemdevelopment.aegis.encoding.Base32;
|
|||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.BitmapHelper;
|
||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SafHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleTextWatcher;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
|
@ -75,7 +79,6 @@ import com.google.android.material.imageview.ShapeableImageView;
|
|||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -86,6 +89,7 @@ import java.util.Collections;
|
|||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
@ -102,6 +106,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
// keep track of icon changes separately as the generated jpeg's are not deterministic
|
||||
private boolean _hasChangedIcon = false;
|
||||
private IconPack.Icon _selectedIcon;
|
||||
private String _pickedMimeType;
|
||||
private ShapeableImageView _iconView;
|
||||
private ImageView _saveImageButton;
|
||||
|
||||
|
@ -139,8 +144,8 @@ public class EditEntryActivity extends AegisActivity {
|
|||
if (activityResult.getResultCode() != RESULT_OK || data == null || data.getData() == null) {
|
||||
return;
|
||||
}
|
||||
String fileType = SafHelper.getMimeType(this, data.getData());
|
||||
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
|
||||
_pickedMimeType = SafHelper.getMimeType(this, data.getData());
|
||||
if (_pickedMimeType != null && _pickedMimeType.equals(IconType.SVG.toMimeType())) {
|
||||
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
|
||||
ImportFileTask task = new ImportFileTask(this, result -> {
|
||||
if (result.getError() == null) {
|
||||
|
@ -164,6 +169,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
}
|
||||
setContentView(R.layout.activity_edit_entry);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
|
||||
_groups = _vaultManager.getVault().getGroups();
|
||||
|
||||
|
@ -802,11 +808,12 @@ public class EditEntryActivity extends AegisActivity {
|
|||
VaultEntryIcon icon;
|
||||
if (_selectedIcon == null) {
|
||||
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
// the quality parameter is ignored for PNG
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
byte[] data = stream.toByteArray();
|
||||
icon = new VaultEntryIcon(data, IconType.PNG);
|
||||
IconType iconType = _pickedMimeType == null
|
||||
? IconType.INVALID : IconType.fromMimeType(_pickedMimeType);
|
||||
if (iconType == IconType.INVALID) {
|
||||
iconType = bitmap.hasAlpha() ? IconType.PNG : IconType.JPEG;
|
||||
}
|
||||
icon = BitmapHelper.toVaultEntryIcon(bitmap, iconType);
|
||||
} else {
|
||||
byte[] iconBytes;
|
||||
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
|
||||
|
@ -848,10 +855,92 @@ public class EditEntryActivity extends AegisActivity {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (_isNew) {
|
||||
for (VaultEntry existing : _vaultManager.getVault().getEntries()) {
|
||||
if (entry.hasSameNameAndIssuer(existing)) {
|
||||
showDuplicateBottomSheet(entry);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addAndFinish(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showDuplicateBottomSheet(VaultEntry newEntry) {
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(this);
|
||||
View view = getLayoutInflater().inflate(R.layout.dialog_duplicate_entry, null);
|
||||
dialog.setContentView(view);
|
||||
|
||||
dialog.setCancelable(false);
|
||||
|
||||
View overwrite = view.findViewById(R.id.overwrite_entry);
|
||||
View addSuffix = view.findViewById(R.id.create_new_entry);
|
||||
View cancel = view.findViewById(R.id.cancel_save);
|
||||
|
||||
TextView suffixSubtext = view.findViewById(R.id.duplicate_suffix_subtitle);
|
||||
|
||||
String baseName = newEntry.getName();
|
||||
Set<String> existingNames = new HashSet<>();
|
||||
for (VaultEntry e : _vaultManager.getVault().getEntries()) {
|
||||
if (e.getIssuer().equals(newEntry.getIssuer())) {
|
||||
existingNames.add(e.getName());
|
||||
}
|
||||
}
|
||||
|
||||
int counter = 2;
|
||||
String newName;
|
||||
do {
|
||||
newName = baseName + " #" + counter++;
|
||||
} while (existingNames.contains(newName));
|
||||
|
||||
suffixSubtext.setText(getString(R.string.dialog_duplicate_entry_suffix_subtitle, newName));
|
||||
|
||||
overwrite.setOnClickListener(v -> {
|
||||
List<VaultEntry> duplicates = new ArrayList<>();
|
||||
for (VaultEntry existing : _vaultManager.getVault().getEntries()) {
|
||||
if (existing.hasSameNameAndIssuer(newEntry)) {
|
||||
duplicates.add(existing);
|
||||
}
|
||||
}
|
||||
|
||||
Resources res = getResources();
|
||||
String message = res.getQuantityString(
|
||||
R.plurals.dialog_duplicate_entry_overwrite_dialog_message,
|
||||
duplicates.size(),
|
||||
duplicates.size(),
|
||||
newEntry.getIssuer(),
|
||||
newEntry.getName()
|
||||
);
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_duplicate_entry_overwrite_dialog_title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.action_delete, (d, which) -> {
|
||||
for (VaultEntry dup : duplicates) {
|
||||
_vaultManager.getVault().removeEntry(dup);
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
addAndFinish(newEntry);
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
});
|
||||
|
||||
String finalNewName = newName;
|
||||
addSuffix.setOnClickListener(v -> {
|
||||
newEntry.setName(finalNewName);
|
||||
dialog.dismiss();
|
||||
addAndFinish(newEntry);
|
||||
});
|
||||
|
||||
cancel.setOnClickListener(v -> dialog.dismiss());
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private static void setViewEnabled(View view, boolean enabled) {
|
||||
view.setEnabled(enabled);
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.view.View;
|
|||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
|
@ -14,13 +15,10 @@ import com.beemdevelopment.aegis.R;
|
|||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryException;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
@ -30,7 +28,6 @@ import java.util.UUID;
|
|||
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
|
||||
private GroupAdapter _adapter;
|
||||
private HashSet<UUID> _removedGroups;
|
||||
private HashSet<VaultGroup> _renamedGroups;
|
||||
private RecyclerView _groupsView;
|
||||
private View _emptyStateView;
|
||||
private BackPressHandler _backPressHandler;
|
||||
|
@ -43,6 +40,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
}
|
||||
setContentView(R.layout.activity_groups);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
@ -51,32 +49,45 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
|
||||
|
||||
_removedGroups = new HashSet<>();
|
||||
_renamedGroups = new HashSet<>();
|
||||
if (savedInstanceState != null) {
|
||||
List<String> removedGroups = savedInstanceState.getStringArrayList("removedGroups");
|
||||
List<String> renamedGroups = savedInstanceState.getStringArrayList("renamedGroups");
|
||||
if (removedGroups != null) {
|
||||
for (String uuid : removedGroups) {
|
||||
_removedGroups.add(UUID.fromString(uuid));
|
||||
}
|
||||
}
|
||||
if (renamedGroups != null) {
|
||||
for (String groupObject : renamedGroups) {
|
||||
try {
|
||||
_renamedGroups.add(VaultGroup.fromJson(new JSONObject(groupObject)));
|
||||
} catch (VaultEntryException | JSONException ignored) {
|
||||
// This is intentionally ignored since the json object is valid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ItemTouchHelper touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||
@Override
|
||||
public int getMovementFlags(
|
||||
@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
|
||||
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
|
||||
int draggedItemIndex = viewHolder.getBindingAdapterPosition();
|
||||
int targetIndex = target.getBindingAdapterPosition();
|
||||
|
||||
_adapter.onItemMove(draggedItemIndex, targetIndex);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { }
|
||||
});
|
||||
|
||||
_adapter = new GroupAdapter(this);
|
||||
_groupsView = findViewById(R.id.list_groups);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
_groupsView.setLayoutManager(layoutManager);
|
||||
_groupsView.setAdapter(_adapter);
|
||||
_groupsView.setNestedScrollingEnabled(false);
|
||||
touchHelper.attachToRecyclerView(_groupsView);
|
||||
|
||||
for (VaultGroup group : _vaultManager.getVault().getGroups()) {
|
||||
if (!_removedGroups.contains(group.getUUID())) {
|
||||
|
@ -84,11 +95,6 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
}
|
||||
}
|
||||
|
||||
for(VaultGroup group: _renamedGroups) {
|
||||
_adapter.replaceGroup(group.getUUID(), group);
|
||||
}
|
||||
|
||||
|
||||
_emptyStateView = findViewById(R.id.vEmptyList);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
@ -100,12 +106,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
for (UUID uuid : _removedGroups) {
|
||||
removed.add(uuid.toString());
|
||||
}
|
||||
ArrayList<String> renamed = new ArrayList<>();
|
||||
for (VaultGroup group : _renamedGroups) {
|
||||
renamed.add(group.toJson().toString());
|
||||
}
|
||||
|
||||
outState.putStringArrayList("renamedGroups", renamed);
|
||||
outState.putStringArrayList("removedGroups", removed);
|
||||
}
|
||||
|
||||
|
@ -116,7 +117,6 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
if (!newGroupName.isEmpty()) {
|
||||
VaultGroup newGroup = Cloner.clone(group);
|
||||
newGroup.setName(newGroupName);
|
||||
_renamedGroups.add(newGroup);
|
||||
_adapter.replaceGroup(group.getUUID(), newGroup);
|
||||
_backPressHandler.setEnabled(true);
|
||||
}
|
||||
|
@ -166,23 +166,16 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
|||
for (UUID uuid : _removedGroups) {
|
||||
_vaultManager.getVault().removeGroup(uuid);
|
||||
}
|
||||
|
||||
saveAndBackupVault();
|
||||
}
|
||||
if (!_renamedGroups.isEmpty()) {
|
||||
_renamedGroups.removeIf(group -> _removedGroups.contains(group.getUUID()));
|
||||
for (VaultGroup renamedGroup : _renamedGroups) {
|
||||
_vaultManager.getVault().renameGroup(renamedGroup);
|
||||
}
|
||||
|
||||
saveAndBackupVault();
|
||||
}
|
||||
_vaultManager.getVault().replaceGroups(_adapter.getGroups());
|
||||
saveAndBackupVault();
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private void discardAndFinish() {
|
||||
if (_removedGroups.isEmpty() && _renamedGroups.isEmpty()) {
|
||||
if (_removedGroups.isEmpty()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -15,16 +15,21 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.BitmapHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
import com.beemdevelopment.aegis.ui.tasks.IconOptimizationTask;
|
||||
import com.beemdevelopment.aegis.ui.tasks.RootShellTask;
|
||||
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
@ -39,12 +44,15 @@ import java.io.InputStream;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ImportEntriesActivity extends AegisActivity {
|
||||
private View _view;
|
||||
private Menu _menu;
|
||||
private RecyclerView _entriesView;
|
||||
private ImportEntriesAdapter _adapter;
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
|
@ -58,6 +66,7 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
}
|
||||
setContentView(R.layout.activity_import_entries);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
|
||||
_view = findViewById(R.id.importEntriesRootView);
|
||||
|
||||
|
@ -66,8 +75,8 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
_adapter = new ImportEntriesAdapter();
|
||||
RecyclerView entriesView = findViewById(R.id.list_entries);
|
||||
entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
_entriesView = findViewById(R.id.list_entries);
|
||||
_entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
|
@ -76,9 +85,9 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
});
|
||||
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
entriesView.setLayoutManager(layoutManager);
|
||||
entriesView.setAdapter(_adapter);
|
||||
entriesView.setNestedScrollingEnabled(false);
|
||||
_entriesView.setLayoutManager(layoutManager);
|
||||
_entriesView.setAdapter(_adapter);
|
||||
_entriesView.setNestedScrollingEnabled(false);
|
||||
|
||||
FloatingActionButton fab = findViewById(R.id.fab);
|
||||
fab.setOnClickListener(v -> {
|
||||
|
@ -170,7 +179,7 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
state.decrypt(this, new DatabaseImporter.DecryptListener() {
|
||||
@Override
|
||||
public void onStateDecrypted(DatabaseImporter.State state) {
|
||||
importDatabase(state);
|
||||
processDecryptedImporterState(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -185,7 +194,7 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
importDatabase(state);
|
||||
processDecryptedImporterState(state);
|
||||
}
|
||||
} catch (DatabaseImporterException e) {
|
||||
e.printStackTrace();
|
||||
|
@ -193,8 +202,7 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private void importDatabase(DatabaseImporter.State state) {
|
||||
List<ImportEntry> importEntries = new ArrayList<>();
|
||||
private void processDecryptedImporterState(DatabaseImporter.State state) {
|
||||
DatabaseImporter.Result result;
|
||||
try {
|
||||
result = state.convert();
|
||||
|
@ -204,8 +212,29 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
UUIDMap<VaultEntry> entries = result.getEntries();
|
||||
for (VaultEntry entry : entries.getValues()) {
|
||||
Map<UUID, VaultEntryIcon> icons = result.getEntries().getValues().stream()
|
||||
.filter(e -> e.getIcon() != null
|
||||
&& !e.getIcon().getType().equals(IconType.SVG)
|
||||
&& !BitmapHelper.isVaultEntryIconOptimized(e.getIcon()))
|
||||
.collect(Collectors.toMap(VaultEntry::getUUID, VaultEntry::getIcon));
|
||||
if (!icons.isEmpty()) {
|
||||
IconOptimizationTask task = new IconOptimizationTask(this, newIcons -> {
|
||||
for (Map.Entry<UUID, VaultEntryIcon> mapEntry : newIcons.entrySet()) {
|
||||
VaultEntry entry = result.getEntries().getByUUID(mapEntry.getKey());
|
||||
entry.setIcon(mapEntry.getValue());
|
||||
}
|
||||
|
||||
processImporterResult(result);
|
||||
});
|
||||
task.execute(getLifecycle(), icons);
|
||||
} else {
|
||||
processImporterResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
private void processImporterResult(DatabaseImporter.Result result) {
|
||||
List<ImportEntry> importEntries = new ArrayList<>();
|
||||
for (VaultEntry entry : result.getEntries().getValues()) {
|
||||
ImportEntry importEntry = new ImportEntry(entry);
|
||||
_adapter.addEntry(importEntry);
|
||||
importEntries.add(importEntry);
|
||||
|
@ -330,6 +359,31 @@ public class ImportEntriesActivity extends AegisActivity {
|
|||
|
||||
_adapter.setCheckboxStates(duplicateEntries, false);
|
||||
Snackbar snackbar = Snackbar.make(_view, getResources().getQuantityString(R.plurals.import_duplicate_toast, duplicateEntries.size(), duplicateEntries.size()), Snackbar.LENGTH_INDEFINITE);
|
||||
snackbar.addCallback(new Snackbar.Callback() {
|
||||
@Override
|
||||
public void onShown(Snackbar sb) {
|
||||
int snackbarHeight = sb.getView().getHeight();
|
||||
|
||||
_entriesView.setPadding(
|
||||
_entriesView.getPaddingLeft(),
|
||||
_entriesView.getPaddingTop(),
|
||||
_entriesView.getPaddingRight(),
|
||||
_entriesView.getPaddingBottom() + snackbarHeight * 2
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissed(Snackbar sb, int event) {
|
||||
int snackbarHeight = sb.getView().getHeight();
|
||||
|
||||
_entriesView.setPadding(
|
||||
_entriesView.getPaddingLeft(),
|
||||
_entriesView.getPaddingTop(),
|
||||
_entriesView.getPaddingRight(),
|
||||
_entriesView.getPaddingBottom() - snackbarHeight * 2
|
||||
);
|
||||
}
|
||||
});
|
||||
snackbar.setAction(R.string.undo, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.view.MenuInflater;
|
|||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Toast;
|
||||
|
@ -32,17 +33,23 @@ import androidx.activity.OnBackPressedCallback;
|
|||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.GroupPlaceholderType;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.helpers.BitmapHelper;
|
||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.helpers.PermissionHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
|
@ -50,26 +57,39 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
|||
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
|
||||
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
|
||||
import com.beemdevelopment.aegis.ui.tasks.IconOptimizationTask;
|
||||
import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask;
|
||||
import com.beemdevelopment.aegis.ui.views.EntryListView;
|
||||
import com.beemdevelopment.aegis.util.TimeUtils;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MainActivity extends AegisActivity implements EntryListView.Listener {
|
||||
|
@ -91,6 +111,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
private SearchView _searchView;
|
||||
private EntryListView _entryListView;
|
||||
|
||||
private Collection<VaultGroup> _groups;
|
||||
private ChipGroup _groupChip;
|
||||
private Set<UUID> _groupFilter;
|
||||
private Set<UUID> _prefGroupFilter;
|
||||
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
private ActionMode _actionMode;
|
||||
|
@ -129,7 +154,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||
return;
|
||||
}
|
||||
onAssignEntriesResult(activityResult.getData());
|
||||
onAssignIconsResult();
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> preferenceResultLauncher =
|
||||
|
@ -140,7 +165,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
|
||||
return;
|
||||
}
|
||||
onEditEntryResult(activityResult.getData());
|
||||
onEditEntryResult();
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> addEntryResultLauncher =
|
||||
|
@ -163,6 +188,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
_loaded = false;
|
||||
_isDPadPressed = false;
|
||||
_isDoingIntro = false;
|
||||
|
@ -187,16 +213,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
_entryListView.setCodeGroupSize(_prefs.getCodeGroupSize());
|
||||
_entryListView.setAccountNamePosition(_prefs.getAccountNamePosition());
|
||||
_entryListView.setShowIcon(_prefs.isIconVisible());
|
||||
_entryListView.setShowExpirationState(_prefs.getShowExpirationState());
|
||||
_entryListView.setShowNextCode(_prefs.getShowNextCode());
|
||||
_entryListView.setOnlyShowNecessaryAccountNames(_prefs.onlyShowNecessaryAccountNames());
|
||||
_entryListView.setHighlightEntry(_prefs.isEntryHighlightEnabled());
|
||||
_entryListView.setPauseFocused(_prefs.isPauseFocusedEnabled());
|
||||
_entryListView.setTapToReveal(_prefs.isTapToRevealEnabled());
|
||||
_entryListView.setTapToRevealTime(_prefs.getTapToRevealTime());
|
||||
_entryListView.setSortCategory(_prefs.getCurrentSortCategory(), false);
|
||||
_entryListView.setViewMode(_prefs.getCurrentViewMode());
|
||||
_entryListView.setSortCategory(_prefs.getCurrentSortCategory(), false);
|
||||
_entryListView.setCopyBehavior(_prefs.getCopyBehavior());
|
||||
_entryListView.setSearchBehaviorMask(_prefs.getSearchBehaviorMask());
|
||||
_entryListView.setPrefGroupFilter(_prefs.getGroupFilter());
|
||||
_prefGroupFilter = _prefs.getGroupFilter();
|
||||
|
||||
FloatingActionButton fab = findViewById(R.id.fab);
|
||||
fab.setOnClickListener(v -> {
|
||||
|
@ -220,10 +248,140 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
Dialogs.showSecureDialog(dialog);
|
||||
});
|
||||
|
||||
_groupChip = findViewById(R.id.groupChipGroup);
|
||||
_fabScrollHelper = new FabScrollHelper(fab);
|
||||
_selectedEntries = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void setGroups(Collection<VaultGroup> groups) {
|
||||
_groups = groups;
|
||||
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
|
||||
if (_prefGroupFilter != null) {
|
||||
Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter);
|
||||
_prefGroupFilter = null;
|
||||
if (!groupFilter.isEmpty()) {
|
||||
_groupFilter = groupFilter;
|
||||
_entryListView.setGroupFilter(groupFilter);
|
||||
}
|
||||
} else if (_groupFilter != null) {
|
||||
Set<UUID> groupFilter = cleanGroupFilter(_groupFilter);
|
||||
if (!_groupFilter.equals(groupFilter)) {
|
||||
_groupFilter = groupFilter;
|
||||
_entryListView.setGroupFilter(groupFilter);
|
||||
}
|
||||
}
|
||||
|
||||
_entryListView.setGroups(groups);
|
||||
initializeGroups();
|
||||
}
|
||||
|
||||
private void initializeGroups() {
|
||||
_groupChip.removeAllViews();
|
||||
_groupChip.setSingleSelection(!_prefs.isGroupMultiselectEnabled());
|
||||
|
||||
for (VaultGroup group : _groups) {
|
||||
addChipTo(_groupChip, new VaultGroupModel(group));
|
||||
}
|
||||
|
||||
GroupPlaceholderType placeholderType = GroupPlaceholderType.NO_GROUP;
|
||||
addChipTo(_groupChip, new VaultGroupModel(this, placeholderType));
|
||||
addSaveChip(_groupChip);
|
||||
}
|
||||
|
||||
private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) {
|
||||
Set<UUID> groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet());
|
||||
|
||||
return groupFilter.stream()
|
||||
.filter(g -> g == null || groupUuids.contains(g))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) {
|
||||
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
|
||||
chip.setText(group.getName());
|
||||
chip.setCheckable(true);
|
||||
chip.setCheckedIconVisible(false);
|
||||
chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID()));
|
||||
|
||||
if (group.isPlaceholder()) {
|
||||
GroupPlaceholderType groupPlaceholderType = group.getPlaceholderType();
|
||||
chip.setTag(groupPlaceholderType);
|
||||
|
||||
if (groupPlaceholderType == GroupPlaceholderType.ALL) {
|
||||
chip.setChecked(_groupFilter == null);
|
||||
} else if (groupPlaceholderType == GroupPlaceholderType.NO_GROUP) {
|
||||
chip.setChecked(_groupFilter != null && _groupFilter.contains(null));
|
||||
}
|
||||
} else {
|
||||
chip.setTag(group);
|
||||
}
|
||||
|
||||
chip.setOnCheckedChangeListener((group1, isChecked) -> {
|
||||
if (_actionMode != null) {
|
||||
_actionMode.finish();
|
||||
}
|
||||
|
||||
setSaveChipVisibility(true);
|
||||
|
||||
// Reset group filter if last checked group gets unchecked
|
||||
if (!isChecked && _groupFilter.size() == 1) {
|
||||
Set<UUID> groupFilter = new HashSet<>();
|
||||
|
||||
chipGroup.clearCheck();
|
||||
_groupFilter = groupFilter;
|
||||
_entryListView.setGroupFilter(groupFilter);
|
||||
return;
|
||||
}
|
||||
|
||||
_groupFilter = getGroupFilter(chipGroup);
|
||||
_entryListView.setGroupFilter(_groupFilter);
|
||||
});
|
||||
|
||||
chipGroup.addView(chip);
|
||||
}
|
||||
|
||||
private void addSaveChip(ChipGroup chipGroup) {
|
||||
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
|
||||
|
||||
chip.setText(getString(R.string.save));
|
||||
chip.setVisibility(View.GONE);
|
||||
chip.setChipStrokeWidth(0);
|
||||
chip.setCheckable(false);
|
||||
chip.setChipBackgroundColorResource(android.R.color.transparent);
|
||||
chip.setTextColor(MaterialColors.getColor(chip.getRootView(), com.google.android.material.R.attr.colorSecondary));
|
||||
chip.setClickable(true);
|
||||
chip.setCheckedIconVisible(false);
|
||||
chip.setOnClickListener(v -> {
|
||||
onSaveGroupFilter(_groupFilter);
|
||||
setSaveChipVisibility(false);
|
||||
});
|
||||
|
||||
chipGroup.addView(chip);
|
||||
}
|
||||
|
||||
private void setSaveChipVisibility(boolean visible) {
|
||||
Chip saveChip = (Chip) _groupChip.getChildAt(_groupChip.getChildCount() - 1);
|
||||
saveChip.setChecked(false);
|
||||
saveChip.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private static Set<UUID> getGroupFilter(ChipGroup chipGroup) {
|
||||
return chipGroup.getCheckedChipIds().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(i -> {
|
||||
Chip chip = chipGroup.findViewById(i);
|
||||
|
||||
if (chip.getTag() instanceof VaultGroupModel) {
|
||||
VaultGroupModel group = (VaultGroupModel) chip.getTag();
|
||||
return group.getUUID();
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
_entryListView.setListener(null);
|
||||
|
@ -252,6 +410,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
instance.putString("submittedSearchQuery", _submittedSearchQuery);
|
||||
instance.putBoolean("isDoingIntro", _isDoingIntro);
|
||||
instance.putBoolean("isAuthenticating", _isAuthenticating);
|
||||
|
||||
if (_groupFilter != null) {
|
||||
instance.putSerializable("prefGroupFilter", new HashSet<>(_groupFilter));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -330,6 +492,76 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
assignIconsResultLauncher.launch(assignIconIntent);
|
||||
}
|
||||
|
||||
private void startAssignGroupsDialog() {
|
||||
View view = LayoutInflater.from(this).inflate(R.layout.dialog_select_group, null);
|
||||
TextInputLayout groupSelectionLayout = view.findViewById(R.id.group_selection_layout);
|
||||
AutoCompleteTextView groupsSelection = view.findViewById(R.id.group_selection_dropdown);
|
||||
TextInputLayout newGroupLayout = view.findViewById(R.id.text_group_name_layout);
|
||||
TextInputEditText newGroupText = view.findViewById(R.id.text_group_name);
|
||||
|
||||
Collection<VaultGroup> groups = _vaultManager.getVault().getUsedGroups();
|
||||
List<VaultGroupModel> groupModels = new ArrayList<>();
|
||||
groupModels.add(new VaultGroupModel(this, GroupPlaceholderType.NEW_GROUP));
|
||||
groupModels.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
|
||||
DropdownHelper.fillDropdown(this, groupsSelection, groupModels);
|
||||
|
||||
AtomicReference<VaultGroupModel> groupModelRef = new AtomicReference<>();
|
||||
groupsSelection.setOnItemClickListener((parent, view1, position, id) -> {
|
||||
VaultGroupModel groupModel = (VaultGroupModel) parent.getItemAtPosition(position);
|
||||
groupModelRef.set(groupModel);
|
||||
|
||||
if (groupModel.isPlaceholder()) {
|
||||
newGroupLayout.setVisibility(View.VISIBLE);
|
||||
newGroupText.requestFocus();
|
||||
} else {
|
||||
newGroupLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
groupSelectionLayout.setError(null);
|
||||
});
|
||||
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.assign_groups)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
btnPos.setOnClickListener(v -> {
|
||||
VaultGroupModel groupModel = groupModelRef.get();
|
||||
if (groupModel == null) {
|
||||
groupSelectionLayout.setError(getString(R.string.error_required_field));
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupModel.isPlaceholder()) {
|
||||
String newGroupName = newGroupText.getText().toString().trim();
|
||||
if (newGroupName.isEmpty()) {
|
||||
newGroupLayout.setError(getString(R.string.error_required_field));
|
||||
return;
|
||||
}
|
||||
|
||||
VaultGroup group = new VaultGroup(newGroupName);
|
||||
_vaultManager.getVault().addGroup(group);
|
||||
groupModel = new VaultGroupModel(group);
|
||||
}
|
||||
|
||||
for (VaultEntry selectedEntry : _selectedEntries) {
|
||||
selectedEntry.addGroup(groupModel.getUUID());
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
saveAndBackupVault();
|
||||
_actionMode.finish();
|
||||
setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
});
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private void startIntroActivity() {
|
||||
if (!_isDoingIntro) {
|
||||
Intent intro = new Intent(this, IntroActivity.class);
|
||||
|
@ -349,31 +581,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
if (_loaded) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
|
||||
_entryListView.addEntry(entry, true);
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
_entryListView.onEntryAdded(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditEntryResult(Intent data) {
|
||||
private void onEditEntryResult() {
|
||||
if (_loaded) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
_entryListView.removeEntry(entryUUID);
|
||||
} else {
|
||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
|
||||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
}
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
}
|
||||
}
|
||||
|
||||
private void onAssignEntriesResult(Intent data) {
|
||||
private void onAssignIconsResult() {
|
||||
if (_loaded) {
|
||||
ArrayList<UUID> entryUUIDs = (ArrayList<UUID>) data.getSerializableExtra("entryUUIDs");
|
||||
|
||||
for (UUID entryUUID: entryUUIDs) {
|
||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
|
||||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
}
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -473,18 +694,21 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
} else if (entries.size() > 1) {
|
||||
for (VaultEntry entry: entries) {
|
||||
_vaultManager.getVault().addEntry(entry);
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
|
||||
if (saveAndBackupVault()) {
|
||||
Toast.makeText(this, getResources().getQuantityString(R.plurals.added_new_entries, entries.size(), entries.size()), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSortCategoryMenu() {
|
||||
SortCategory category = _prefs.getCurrentSortCategory();
|
||||
_menu.findItem(category.getMenuItem()).setChecked(true);
|
||||
if (_menu != null) {
|
||||
SortCategory category = _prefs.getCurrentSortCategory();
|
||||
_menu.findItem(category.getMenuItem()).setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onIntroResult() {
|
||||
|
@ -501,6 +725,37 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
}
|
||||
}
|
||||
|
||||
private void checkIconOptimization() {
|
||||
if (!_vaultManager.getVault().areIconsOptimized()) {
|
||||
Map<UUID, VaultEntryIcon> oldIcons = _vaultManager.getVault().getEntries().stream()
|
||||
.filter(e -> e.getIcon() != null
|
||||
&& !e.getIcon().getType().equals(IconType.SVG)
|
||||
&& !BitmapHelper.isVaultEntryIconOptimized(e.getIcon()))
|
||||
.collect(Collectors.toMap(VaultEntry::getUUID, VaultEntry::getIcon));
|
||||
|
||||
if (!oldIcons.isEmpty()) {
|
||||
IconOptimizationTask task = new IconOptimizationTask(this, this::onIconsOptimized);
|
||||
task.execute(getLifecycle(), oldIcons);
|
||||
} else {
|
||||
onIconsOptimized(Collections.emptyMap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onIconsOptimized(Map<UUID, VaultEntryIcon> newIcons) {
|
||||
for (Map.Entry<UUID, VaultEntryIcon> mapEntry : newIcons.entrySet()) {
|
||||
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(mapEntry.getKey());
|
||||
entry.setIcon(mapEntry.getValue());
|
||||
}
|
||||
|
||||
_vaultManager.getVault().setIconsOptimized(true);
|
||||
saveAndBackupVault();
|
||||
|
||||
if (!newIcons.isEmpty()) {
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
}
|
||||
}
|
||||
|
||||
private void onDecryptResult() {
|
||||
_auditLogRepository.addVaultUnlockedEvent();
|
||||
|
||||
|
@ -635,6 +890,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
_entryListView.onRefreshStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
@ -677,7 +939,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
startAuthActivity(false);
|
||||
} else if (_loaded) {
|
||||
// update the list of groups in the entry list view so that the chip gets updated
|
||||
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
|
||||
// update the usage counts in case they are edited outside of the EntryListView
|
||||
_entryListView.setUsageCounts(_prefs.getUsageCounts());
|
||||
|
@ -686,43 +948,36 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
|
||||
// refresh all codes to prevent showing old ones
|
||||
_entryListView.refresh(false);
|
||||
|
||||
_entryListView.onRefreshStart();
|
||||
} else {
|
||||
loadEntries();
|
||||
checkTimeSyncSetting();
|
||||
checkIconOptimization();
|
||||
|
||||
_entryListView.onRefreshStart();
|
||||
}
|
||||
|
||||
_lockBackPressHandler.setEnabled(
|
||||
_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)
|
||||
_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)
|
||||
);
|
||||
|
||||
handleIncomingIntent();
|
||||
updateLockIcon();
|
||||
updateSortCategoryMenu();
|
||||
doShortcutActions();
|
||||
updateErrorCard();
|
||||
}
|
||||
|
||||
private void deleteEntries(List<VaultEntry> entries) {
|
||||
for (VaultEntry entry: entries) {
|
||||
VaultEntry oldEntry = _vaultManager.getVault().removeEntry(entry);
|
||||
_entryListView.removeEntry(oldEntry);
|
||||
}
|
||||
|
||||
saveAndBackupVault();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
_menu = menu;
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
|
||||
updateLockIcon();
|
||||
if (_loaded) {
|
||||
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
updateSortCategoryMenu();
|
||||
}
|
||||
updateSortCategoryMenu();
|
||||
|
||||
MenuItem searchViewMenuItem = menu.findItem(R.id.mi_search);
|
||||
|
||||
_searchView = (SearchView) searchViewMenuItem.getActionView();
|
||||
_searchView.setMaxWidth(Integer.MAX_VALUE);
|
||||
_searchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> {
|
||||
|
@ -732,6 +987,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
_searchView.setOnCloseListener(() -> {
|
||||
boolean enabled = _submittedSearchQuery != null;
|
||||
_searchViewBackPressHandler.setEnabled(enabled);
|
||||
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
return false;
|
||||
});
|
||||
|
||||
|
@ -765,6 +1021,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
});
|
||||
_searchView.setOnSearchClickListener(v -> {
|
||||
String query = _submittedSearchQuery != null ? _submittedSearchQuery : _pendingSearchQuery;
|
||||
_groupChip.setVisibility(View.GONE);
|
||||
_searchView.setQuery(query, false);
|
||||
});
|
||||
|
||||
|
@ -830,16 +1087,17 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
}
|
||||
|
||||
private void collapseSearchView() {
|
||||
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
_searchView.setQuery(null, false);
|
||||
_searchView.setIconified(true);
|
||||
}
|
||||
|
||||
private void loadEntries() {
|
||||
if (!_loaded) {
|
||||
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
_entryListView.setUsageCounts(_prefs.getUsageCounts());
|
||||
_entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps());
|
||||
_entryListView.addEntries(_vaultManager.getVault().getEntries());
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
if (!_isRecreated) {
|
||||
_entryListView.runEntriesAnimation();
|
||||
}
|
||||
|
@ -930,6 +1188,19 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(@Nullable Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
if (savedInstanceState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<UUID> filter = (HashSet<UUID>) savedInstanceState.getSerializable("prefGroupFilter");
|
||||
if (filter != null) {
|
||||
_prefGroupFilter = filter;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryClick(VaultEntry entry) {
|
||||
if (_actionMode != null) {
|
||||
|
@ -1054,6 +1325,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean saveAndBackupVault() {
|
||||
boolean res = super.saveAndBackupVault();
|
||||
updateErrorCard();
|
||||
return res;
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private void copyEntryCode(VaultEntry entry) {
|
||||
String otp;
|
||||
|
@ -1150,12 +1428,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
mode.finish();
|
||||
} else if (itemId == R.id.action_toggle_favorite) {
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
entry.setIsFavorite(!entry.isFavorite());
|
||||
_entryListView.replaceEntry(entry.getUUID(), entry);
|
||||
_vaultManager.getVault().editEntry(entry, newEntry -> {
|
||||
newEntry.setIsFavorite(!newEntry.isFavorite());
|
||||
});
|
||||
}
|
||||
_entryListView.refresh(true);
|
||||
|
||||
saveAndBackupVault();
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
mode.finish();
|
||||
} else if (itemId == R.id.action_share_qr) {
|
||||
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
|
||||
|
@ -1173,8 +1452,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
mode.finish();
|
||||
} else if (itemId == R.id.action_delete) {
|
||||
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
|
||||
deleteEntries(_selectedEntries);
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
_vaultManager.getVault().removeEntry(entry);
|
||||
}
|
||||
saveAndBackupVault();
|
||||
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
|
||||
_entryListView.setEntries(_vaultManager.getVault().getEntries());
|
||||
mode.finish();
|
||||
});
|
||||
} else if (itemId == R.id.action_select_all) {
|
||||
|
@ -1184,6 +1467,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
} else if (itemId == R.id.action_assign_icons) {
|
||||
startAssignIconsActivity(_selectedEntries);
|
||||
mode.finish();
|
||||
} else if (itemId == R.id.action_assign_groups) {
|
||||
startAssignGroupsDialog();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.beemdevelopment.aegis.R;
|
|||
import com.beemdevelopment.aegis.ui.fragments.preferences.AppearancePreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.fragments.preferences.MainPreferencesFragment;
|
||||
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
|
||||
public class PreferencesActivity extends AegisActivity implements
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
|
@ -27,6 +28,7 @@ public class PreferencesActivity extends AegisActivity implements
|
|||
}
|
||||
setContentView(R.layout.activity_preferences);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
getSupportFragmentManager()
|
||||
.registerFragmentLifecycleCallbacks(new FragmentResumeListener(), true);
|
||||
|
||||
|
|
|
@ -18,11 +18,11 @@ import androidx.camera.view.PreviewView;
|
|||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.zxing.Result;
|
||||
|
@ -57,6 +57,7 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
|||
}
|
||||
setContentView(R.layout.activity_scanner);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
|
||||
_entries = new ArrayList<>();
|
||||
_lenses = new ArrayList<>();
|
||||
|
@ -87,11 +88,6 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
|
|||
}, ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
_themeHelper.setTheme(ThemeMap.FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (_executor != null) {
|
||||
|
|
|
@ -10,15 +10,17 @@ import android.graphics.Color;
|
|||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.PersistableBundle;
|
||||
import android.util.TypedValue;
|
||||
import android.provider.Settings;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
|
@ -27,6 +29,9 @@ import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
|||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.otp.Transferable;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.helpers.ViewHelper;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.imageview.ShapeableImageView;
|
||||
import com.google.zxing.WriterException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -34,7 +39,7 @@ import java.util.List;
|
|||
|
||||
public class TransferEntriesActivity extends AegisActivity {
|
||||
private List<Transferable> _authInfos;
|
||||
private ImageView _qrImage;
|
||||
private ShapeableImageView _qrImage;
|
||||
private TextView _description;
|
||||
private TextView _issuer;
|
||||
private TextView _accountName;
|
||||
|
@ -43,6 +48,8 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
private Button _previousButton;
|
||||
private Button _copyButton;
|
||||
private int _currentEntryCount = 1;
|
||||
private float _deviceBrightness;
|
||||
private boolean _isMaxBrightnessSet = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -52,6 +59,7 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
}
|
||||
setContentView(R.layout.activity_share_entry);
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
|
||||
|
||||
_qrImage = findViewById(R.id.ivQrCode);
|
||||
_description = findViewById(R.id.tvDescription);
|
||||
|
@ -62,7 +70,7 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
_previousButton = findViewById(R.id.btnPrevious);
|
||||
_copyButton = findViewById(R.id.btnCopyClipboard);
|
||||
|
||||
if (getSupportActionBar() != null){
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
@ -88,7 +96,7 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
});
|
||||
|
||||
_previousButton.setOnClickListener(v -> {
|
||||
if (_currentEntryCount > 1 ) {
|
||||
if (_currentEntryCount > 1) {
|
||||
_nextButton.setText(R.string.next);
|
||||
_currentEntryCount--;
|
||||
generateQR();
|
||||
|
@ -116,14 +124,59 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
if (clipboard != null) {
|
||||
clipboard.setPrimaryClip(clip);
|
||||
}
|
||||
Toast.makeText(this,R.string.uri_copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, R.string.uri_copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
||||
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_copy_uri_to_clipboard, e);
|
||||
}
|
||||
});
|
||||
|
||||
generateQR();
|
||||
// Calculate sensible dimensions for the QR code depending on whether we're in landscape
|
||||
_qrImage.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
ConstraintLayout layout = findViewById(R.id.layoutShareEntry);
|
||||
if (layout.getWidth() > layout.getHeight()) {
|
||||
int squareSize = (int) (0.5 * layout.getHeight());
|
||||
ViewGroup.LayoutParams params = _qrImage.getLayoutParams();
|
||||
params.width = squareSize;
|
||||
params.height = squareSize;
|
||||
_qrImage.setLayoutParams(params);
|
||||
}
|
||||
|
||||
generateQR();
|
||||
|
||||
_qrImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});
|
||||
|
||||
_deviceBrightness = getSystemBrightness();
|
||||
_qrImage.setOnClickListener(v -> {
|
||||
if (!_isMaxBrightnessSet) {
|
||||
setBrightness(1f);
|
||||
_isMaxBrightnessSet = true;
|
||||
} else {
|
||||
setBrightness(_deviceBrightness);
|
||||
_isMaxBrightnessSet = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private float getSystemBrightness() {
|
||||
int brightness = 0;
|
||||
try {
|
||||
brightness = Settings.System.getInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS);
|
||||
} catch (Settings.SettingNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return brightness / 255f;
|
||||
}
|
||||
|
||||
private void setBrightness(float brightnessAmount) {
|
||||
WindowManager.LayoutParams attrs = getWindow().getAttributes();
|
||||
attrs.screenBrightness = brightnessAmount;
|
||||
getWindow().setAttributes(attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -151,16 +204,13 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
|
||||
_entriesCount.setText(getResources().getQuantityString(R.plurals.qr_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
|
||||
|
||||
@ColorInt int backgroundColor = Color.WHITE;
|
||||
if (_themeHelper.getConfiguredTheme() == Theme.LIGHT) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
getTheme().resolveAttribute(androidx.appcompat.R.attr.background, typedValue, true);
|
||||
backgroundColor = typedValue.data;
|
||||
}
|
||||
int backgroundColor = _themeHelper.getConfiguredTheme() == Theme.LIGHT
|
||||
? MaterialColors.getColor(_qrImage, com.google.android.material.R.attr.colorSurfaceContainer)
|
||||
: Color.WHITE;
|
||||
|
||||
Bitmap bitmap;
|
||||
try {
|
||||
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), 512, 512, backgroundColor);
|
||||
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), _qrImage.getWidth(), _qrImage.getWidth(), backgroundColor);
|
||||
} catch (WriterException | GoogleAuthInfoException e) {
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
|
||||
return;
|
||||
|
|
|
@ -19,6 +19,8 @@ import android.widget.EditText;
|
|||
import android.widget.ListView;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
@ -27,6 +29,7 @@ import androidx.annotation.Nullable;
|
|||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
|
@ -554,7 +557,7 @@ public class Dialogs {
|
|||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(context.getString(R.string.import_partial_export_anyway, entries), (dialog, which) -> {
|
||||
.setPositiveButton(context.getResources().getQuantityString(R.plurals.import_partial_export_anyway, entries, entries), (dialog, which) -> {
|
||||
dismissHandler.onClick(dialog, which);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
|
@ -577,6 +580,54 @@ public class Dialogs {
|
|||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showBackupsVersioningStrategy(Context context, BackupsVersioningStrategy currentStrategy, BackupsVersioningStrategyListener listener) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_backups_versioning_strategy, null);
|
||||
RadioGroup radioGroup = view.findViewById(R.id.radio_group);
|
||||
RadioButton keepXVersionsButton = view.findViewById(R.id.keep_x_versions_button);
|
||||
RadioButton singleBackupButton = view.findViewById(R.id.single_backup_button);
|
||||
TextView warningText = view.findViewById(R.id.warning_text);
|
||||
CheckBox riskAccept = view.findViewById(R.id.risk_accept);
|
||||
final AtomicReference<Button> positiveButtonRef = new AtomicReference<>();
|
||||
radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
Button positiveButton = positiveButtonRef.get();
|
||||
if (positiveButton != null) {
|
||||
positiveButton.setEnabled(checkedId == keepXVersionsButton.getId());
|
||||
}
|
||||
int visibility = checkedId == singleBackupButton.getId() ? View.VISIBLE : View.GONE;
|
||||
warningText.setVisibility(visibility);
|
||||
riskAccept.setVisibility(visibility);
|
||||
});
|
||||
riskAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
Button positiveButton = positiveButtonRef.get();
|
||||
if (positiveButton != null) {
|
||||
positiveButton.setEnabled(isChecked);
|
||||
}
|
||||
});
|
||||
AlertDialog alertDialog = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.pref_backups_versioning_strategy_dialog_title)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int checkedId = radioGroup.getCheckedRadioButtonId();
|
||||
if (checkedId == keepXVersionsButton.getId()) {
|
||||
listener.onStrategySelectionResult(BackupsVersioningStrategy.MULTIPLE_BACKUPS);
|
||||
} else if (checkedId == singleBackupButton.getId()) {
|
||||
listener.onStrategySelectionResult(BackupsVersioningStrategy.SINGLE_BACKUP);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
alertDialog.setOnShowListener(dialog -> {
|
||||
Button positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
positiveButtonRef.set(positiveButton);
|
||||
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
radioGroup.check(keepXVersionsButton.getId());
|
||||
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
radioGroup.check(singleBackupButton.getId());
|
||||
}
|
||||
});
|
||||
showSecureDialog(alertDialog);
|
||||
}
|
||||
|
||||
private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) {
|
||||
if (isDirect) {
|
||||
view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName()));
|
||||
|
@ -605,4 +656,8 @@ public class Dialogs {
|
|||
public interface ImporterListener {
|
||||
void onImporterSelectionResult(DatabaseImporter.Definition definition);
|
||||
}
|
||||
|
||||
public interface BackupsVersioningStrategyListener {
|
||||
void onStrategySelectionResult(BackupsVersioningStrategy strategy);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
|||
});
|
||||
|
||||
Preference langPreference = requirePreference("pref_lang");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
||||
String[] langs = getResources().getStringArray(R.array.pref_lang_values);
|
||||
String[] langNames = getResources().getStringArray(R.array.pref_lang_entries);
|
||||
List<String> langList = Arrays.asList(langs);
|
||||
|
@ -125,6 +125,11 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
|
|||
return true;
|
||||
});
|
||||
|
||||
Preference showExpirationStatePreference = requirePreference("pref_expiration_state");
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
showExpirationStatePreference.setSummary(getString(R.string.pref_expiration_state_fallback));
|
||||
}
|
||||
|
||||
String[] codeGroupings = getResources().getStringArray(R.array.pref_code_groupings_values);
|
||||
String[] codeGroupingNames = getResources().getStringArray(R.array.pref_code_groupings);
|
||||
Preference codeDigitGroupingPreference = requirePreference("pref_code_group_size_string");
|
||||
|
|
|
@ -6,6 +6,9 @@ import android.view.View;
|
|||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
@ -58,6 +61,17 @@ public class AuditLogPreferencesFragment extends Fragment {
|
|||
_auditLogRecyclerView.setAdapter(_adapter);
|
||||
_auditLogRecyclerView.setNestedScrollingEnabled(false);
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(_auditLogRecyclerView, (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
targetView.setPadding(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
insets.bottom
|
||||
);
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
entries.observe(getViewLifecycleOwner(), entries1 -> {
|
||||
_noAuditLogsView.setVisibility(entries1.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
|
||||
|
|
|
@ -17,9 +17,11 @@ import androidx.annotation.Nullable;
|
|||
import androidx.preference.Preference;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.vault.VaultBackupManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
|
@ -27,6 +29,7 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
private SwitchPreferenceCompat _androidBackupsPreference;
|
||||
private SwitchPreferenceCompat _backupsPreference;
|
||||
private SwitchPreferenceCompat _backupReminderPreference;
|
||||
private Preference _versioningStrategyPreference;
|
||||
private Preference _backupsLocationPreference;
|
||||
private Preference _backupsTriggerPreference;
|
||||
private Preference _backupsVersionsPreference;
|
||||
|
@ -75,7 +78,13 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
_backupsPreference = requirePreference("pref_backups");
|
||||
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if ((boolean) newValue) {
|
||||
selectBackupsLocation();
|
||||
Dialogs.showBackupsVersioningStrategy(requireContext(), BackupsVersioningStrategy.MULTIPLE_BACKUPS, strategy -> {
|
||||
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
selectBackupsLocation();
|
||||
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
createBackupFile();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_prefs.setIsBackupsEnabled(false);
|
||||
updateBackupPreference();
|
||||
|
@ -100,6 +109,24 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
return false;
|
||||
});
|
||||
|
||||
_versioningStrategyPreference = requirePreference("pref_versioning_strategy");
|
||||
updateBackupsVersioningStrategySummary();
|
||||
_versioningStrategyPreference.setOnPreferenceClickListener(preference -> {
|
||||
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
|
||||
Dialogs.showBackupsVersioningStrategy(requireContext(), currentStrategy, strategy -> {
|
||||
if (strategy == currentStrategy) {
|
||||
return;
|
||||
}
|
||||
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
selectBackupsLocation();
|
||||
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
createBackupFile();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
_androidBackupsPreference = requirePreference("pref_android_backups");
|
||||
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_prefs.setIsAndroidBackupsEnabled((boolean) newValue);
|
||||
|
@ -110,13 +137,15 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
return false;
|
||||
});
|
||||
|
||||
Uri backupLocation = _prefs.getBackupsLocation();
|
||||
_backupsLocationPreference = requirePreference("pref_backups_location");
|
||||
if (backupLocation != null) {
|
||||
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
|
||||
}
|
||||
updateBackupsLocationSummary();
|
||||
_backupsLocationPreference.setOnPreferenceClickListener(preference -> {
|
||||
selectBackupsLocation();
|
||||
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
|
||||
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
selectBackupsLocation();
|
||||
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
createBackupFile();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
|
@ -158,9 +187,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
|
||||
_prefs.setBackupsLocation(uri);
|
||||
_prefs.setIsBackupsEnabled(true);
|
||||
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
|
||||
updateBackupPreference();
|
||||
scheduleBackup();
|
||||
updateBackupsVersioningStrategySummary();
|
||||
updateBackupsLocationSummary();
|
||||
}
|
||||
|
||||
private void updateBackupPreference() {
|
||||
|
@ -174,9 +204,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
_backupsPreference.setChecked(backupEnabled);
|
||||
_backupsPreference.setEnabled(encrypted);
|
||||
_backupReminderPreference.setChecked(backupReminderEnabled);
|
||||
_versioningStrategyPreference.setVisible(backupEnabled);
|
||||
_backupsLocationPreference.setVisible(backupEnabled);
|
||||
_backupsTriggerPreference.setVisible(backupEnabled);
|
||||
_backupsVersionsPreference.setVisible(backupEnabled);
|
||||
_backupsVersionsPreference.setVisible(backupEnabled && _prefs.getBackupVersioningStrategy() != BackupsVersioningStrategy.SINGLE_BACKUP);
|
||||
if (backupEnabled) {
|
||||
updateBackupStatus(_builtinBackupStatusPreference, _prefs.getBuiltInBackupResult());
|
||||
}
|
||||
|
@ -221,6 +252,14 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
return spannable;
|
||||
}
|
||||
|
||||
private void createBackupFile() {
|
||||
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType("application/json")
|
||||
.putExtra(Intent.EXTRA_TITLE, VaultBackupManager.FILENAME_SINGLE);
|
||||
_vaultManager.fireIntentLauncher(this, intent, backupsResultLauncher);
|
||||
}
|
||||
|
||||
private void selectBackupsLocation() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
@ -241,6 +280,30 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateBackupsVersioningStrategySummary() {
|
||||
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
|
||||
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_keep_x_versions);
|
||||
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_single_backup);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBackupsLocationSummary() {
|
||||
Uri backupsLocation = _prefs.getBackupsLocation();
|
||||
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
|
||||
String text;
|
||||
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
text = getString(R.string.pref_backups_location_summary);
|
||||
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
text = getString(R.string.pref_backup_location_summary);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
String summary = String.format("%s: %s", text, Uri.decode(backupsLocation.toString()));
|
||||
_backupsLocationPreference.setSummary(summary);
|
||||
}
|
||||
|
||||
private void updateBackupsVersionsSummary() {
|
||||
int count = _prefs.getBackupsVersionCount();
|
||||
if (count == Preferences.BACKUPS_VERSIONS_INFINITE) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.net.Uri;
|
|||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup.MarginLayoutParams;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
@ -14,6 +15,9 @@ import androidx.activity.result.ActivityResultLauncher;
|
|||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
@ -67,6 +71,19 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
|
|||
fab.setOnClickListener(v -> startImportIconPack());
|
||||
_fabScrollHelper = new FabScrollHelper(fab);
|
||||
|
||||
final MarginLayoutParams fabInitialMargin = (MarginLayoutParams) fab.getLayoutParams();
|
||||
ViewCompat.setOnApplyWindowInsetsListener(fab, (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
|
||||
MarginLayoutParams marginParams = (MarginLayoutParams) targetView.getLayoutParams();
|
||||
marginParams.leftMargin = fabInitialMargin.leftMargin + insets.left;
|
||||
marginParams.bottomMargin = fabInitialMargin.bottomMargin + insets.bottom;
|
||||
marginParams.rightMargin = fabInitialMargin.rightMargin + insets.right;
|
||||
targetView.setLayoutParams(marginParams);
|
||||
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
_noIconPacksView = view.findViewById(R.id.vEmptyList);
|
||||
((TextView) view.findViewById(R.id.txt_no_icon_packs)).setMovementMethod(LinkMovementMethod.getInstance());
|
||||
_adapter = new IconPackAdapter(this);
|
||||
|
@ -107,15 +124,7 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
|
|||
.setMessage(R.string.remove_icon_pack_description)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
try {
|
||||
_iconPackManager.removeIconPack(pack);
|
||||
} catch (IconPackException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(requireContext(), R.string.icon_pack_delete_error, e);
|
||||
return;
|
||||
}
|
||||
_adapter.removeIconPack(pack);
|
||||
updateEmptyState();
|
||||
removeIconPack(pack);
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
|
|
|
@ -22,6 +22,7 @@ import androidx.core.content.FileProvider;
|
|||
import androidx.preference.Preference;
|
||||
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.GroupPlaceholderType;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
|
@ -190,7 +191,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
|||
checkBoxExportAllGroups.setVisibility(View.VISIBLE);
|
||||
|
||||
ArrayList<VaultGroupModel> groupsArray = new ArrayList<>();
|
||||
groupsArray.add(new VaultGroupModel(getString(R.string.no_group)));
|
||||
groupsArray.add(new VaultGroupModel(requireContext(), GroupPlaceholderType.NO_GROUP));
|
||||
groupsArray.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
|
||||
|
||||
groupsSelection.setCheckedItemsCountTextRes(R.plurals.export_groups_selected_count);
|
||||
|
@ -295,7 +296,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
|||
boolean encrypt = checkBoxEncrypt.isChecked();
|
||||
try {
|
||||
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, encrypt);
|
||||
file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir());
|
||||
file = new File(getExportCacheDir(), fileInfo.toString());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(requireContext(), R.string.exporting_vault_error, e);
|
||||
|
@ -517,11 +518,10 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
|||
file = File.createTempFile(VaultRepository.FILENAME_PREFIX_EXPORT + "-", ".json", getExportCacheDir());
|
||||
outStream = new FileOutputStream(file);
|
||||
cb.exportVault(outStream);
|
||||
|
||||
new ExportTask(requireContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri));
|
||||
} catch (VaultRepositoryException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(requireContext(), R.string.exporting_vault_error, e);
|
||||
return;
|
||||
} finally {
|
||||
try {
|
||||
if (outStream != null) {
|
||||
|
@ -531,6 +531,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
|||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
new ExportTask(requireContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri));
|
||||
}, _exportFilter);
|
||||
_exportFilter = null;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
package com.beemdevelopment.aegis.ui.fragments.preferences;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
@ -61,6 +68,23 @@ public abstract class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
return super.onCreateAnimation(transit, enter, nextAnim);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView onCreateRecyclerView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent, @Nullable Bundle savedInstanceState) {
|
||||
RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(recyclerView, (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
targetView.setPadding(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
insets.bottom
|
||||
);
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
return recyclerView;
|
||||
}
|
||||
|
||||
protected boolean saveAndBackupVault() {
|
||||
try {
|
||||
_vaultManager.saveAndBackup();
|
||||
|
|
|
@ -2,6 +2,10 @@ package com.beemdevelopment.aegis.ui.models;
|
|||
|
||||
import android.view.View;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ErrorCardInfo {
|
||||
private final String _message;
|
||||
private final View.OnClickListener _listener;
|
||||
|
@ -18,4 +22,23 @@ public class ErrorCardInfo {
|
|||
public View.OnClickListener getListener() {
|
||||
return _listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return HashCode.fromString(_message).asInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof ErrorCardInfo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This equality check purposefully ignores the onclick listener
|
||||
ErrorCardInfo info = (ErrorCardInfo) o;
|
||||
return Objects.equals(getMessage(), info.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,31 @@
|
|||
package com.beemdevelopment.aegis.ui.models;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.GroupPlaceholderType;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
public class VaultGroupModel implements Serializable {
|
||||
private final VaultGroup _group;
|
||||
private final String _placeholderName;
|
||||
private final GroupPlaceholderType _placeholderType;
|
||||
private final String _placeholderText;
|
||||
|
||||
public VaultGroupModel(VaultGroup group) {
|
||||
_group = group;
|
||||
_placeholderName = null;
|
||||
_placeholderText = null;
|
||||
_placeholderType = null;
|
||||
}
|
||||
|
||||
public VaultGroupModel(String placeholderName) {
|
||||
public VaultGroupModel(Context context, GroupPlaceholderType placeholderType) {
|
||||
_group = null;
|
||||
_placeholderName = placeholderName;
|
||||
_placeholderType = placeholderType;
|
||||
_placeholderText = context.getString(placeholderType.getStringRes());
|
||||
}
|
||||
|
||||
public VaultGroup getGroup() {
|
||||
|
@ -25,11 +33,15 @@ public class VaultGroupModel implements Serializable {
|
|||
}
|
||||
|
||||
public String getName() {
|
||||
return _group != null ? _group.getName() : _placeholderName;
|
||||
return _group != null ? _group.getName() : _placeholderText;
|
||||
}
|
||||
|
||||
public GroupPlaceholderType getPlaceholderType() {
|
||||
return _placeholderType;
|
||||
}
|
||||
|
||||
public boolean isPlaceholder() {
|
||||
return _group == null;
|
||||
return _placeholderType != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
package com.beemdevelopment.aegis.ui.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
public class SwitchPreference extends SwitchPreferenceCompat {
|
||||
private Preference.OnPreferenceChangeListener _listener;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ public class ExportTask extends ProgressDialogTask<ExportTask.Params, Exception>
|
|||
return null;
|
||||
} catch (IOException e) {
|
||||
return e;
|
||||
} finally {
|
||||
boolean ignored = params.getFile().delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package com.beemdevelopment.aegis.ui.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.BitmapHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class IconOptimizationTask extends ProgressDialogTask<Map<UUID, VaultEntryIcon>, Map<UUID, VaultEntryIcon>> {
|
||||
private final Callback _cb;
|
||||
|
||||
public IconOptimizationTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.optimizing_icon));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<UUID, VaultEntryIcon> doInBackground(Map<UUID, VaultEntryIcon>... params) {
|
||||
Map<UUID, VaultEntryIcon> res = new HashMap<>();
|
||||
Context context = getDialog().getContext();
|
||||
|
||||
int i = 0;
|
||||
Map<UUID, VaultEntryIcon> icons = params[0];
|
||||
for (Map.Entry<UUID, VaultEntryIcon> entry : icons.entrySet()) {
|
||||
if (icons.size() > 1) {
|
||||
publishProgress(context.getString(R.string.optimizing_icon_multiple, i + 1, icons.size()));
|
||||
}
|
||||
i++;
|
||||
|
||||
VaultEntryIcon oldIcon = entry.getValue();
|
||||
if (oldIcon == null || oldIcon.getType().equals(IconType.SVG)) {
|
||||
continue;
|
||||
}
|
||||
if (BitmapHelper.isVaultEntryIconOptimized(oldIcon)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(oldIcon.getBytes(), 0, oldIcon.getBytes().length);
|
||||
VaultEntryIcon newIcon = BitmapHelper.toVaultEntryIcon(bitmap, oldIcon.getType());
|
||||
bitmap.recycle();
|
||||
res.put(entry.getKey(), newIcon);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Map<UUID, VaultEntryIcon> results) {
|
||||
super.onPostExecute(results);
|
||||
_cb.onTaskFinished(results);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(Map<UUID, VaultEntryIcon> results);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,11 @@ package com.beemdevelopment.aegis.ui.tasks;
|
|||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
|
||||
import org.bouncycastle.crypto.digests.SHA512Digest;
|
||||
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
|
@ -31,6 +36,15 @@ public class PBKDFTask extends ProgressDialogTask<PBKDFTask.Params, SecretKey> {
|
|||
|
||||
public static SecretKey deriveKey(Params params) {
|
||||
try {
|
||||
// Some older versions of Android (< 26) do not support PBKDF2withHmacSHA512, so use
|
||||
// BouncyCastle's implementation instead.
|
||||
if (params.getAlgorithm().equals("PBKDF2withHmacSHA512")) {
|
||||
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
|
||||
gen.init(CryptoUtils.toBytes(params.getPassword()), params.getSalt(), params.getIterations());
|
||||
byte[] key = ((KeyParameter) gen.generateDerivedParameters(params.getKeySize())).getKey();
|
||||
return new SecretKeySpec(key, "AES");
|
||||
}
|
||||
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance(params.getAlgorithm());
|
||||
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), params.getKeySize());
|
||||
SecretKey key = factory.generateSecret(spec);
|
||||
|
|
|
@ -15,6 +15,8 @@ import android.view.ViewGroup;
|
|||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.AccountNamePosition;
|
||||
|
@ -35,6 +37,7 @@ import com.beemdevelopment.aegis.vault.VaultEntry;
|
|||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
@ -49,8 +52,7 @@ import java.util.UUID;
|
|||
|
||||
public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ItemTouchHelperAdapter {
|
||||
private EntryListView _view;
|
||||
private List<VaultEntry> _entries;
|
||||
private List<VaultEntry> _shownEntries;
|
||||
private EntryList _entryList;
|
||||
private List<VaultEntry> _selectedEntries;
|
||||
private Collection<VaultGroup> _groups;
|
||||
private Map<UUID, Integer> _usageCounts;
|
||||
|
@ -60,6 +62,8 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
private Preferences.CodeGrouping _codeGroupSize;
|
||||
private AccountNamePosition _accountNamePosition;
|
||||
private boolean _showIcon;
|
||||
private boolean _showNextCode;
|
||||
private boolean _showExpirationState;
|
||||
private boolean _onlyShowNecessaryAccountNames;
|
||||
private boolean _highlightEntry;
|
||||
private boolean _tempHighlightEntry;
|
||||
|
@ -76,14 +80,12 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
private Handler _dimHandler;
|
||||
private Handler _doubleTapHandler;
|
||||
private boolean _pauseFocused;
|
||||
private ErrorCardInfo _errorCardInfo;
|
||||
|
||||
// keeps track of the EntryHolders that are currently bound
|
||||
private List<EntryHolder> _holders;
|
||||
|
||||
public EntryAdapter(EntryListView view) {
|
||||
_entries = new ArrayList<>();
|
||||
_shownEntries = new ArrayList<>();
|
||||
_entryList = new EntryList();
|
||||
_selectedEntries = new ArrayList<>();
|
||||
_groupFilter = new TreeSet<>();
|
||||
_holders = new ArrayList<>();
|
||||
|
@ -115,6 +117,14 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
_showIcon = showIcon;
|
||||
}
|
||||
|
||||
public void setShowNextCode(boolean showNextCode) {
|
||||
_showNextCode = showNextCode;
|
||||
}
|
||||
|
||||
public void setShowExpirationState(boolean showExpirationState) {
|
||||
_showExpirationState = showExpirationState;
|
||||
}
|
||||
|
||||
public void setTapToReveal(boolean tapToReveal) {
|
||||
_tapToReveal = tapToReveal;
|
||||
}
|
||||
|
@ -140,173 +150,45 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
}
|
||||
|
||||
public void setErrorCardInfo(ErrorCardInfo info) {
|
||||
ErrorCardInfo oldInfo = _errorCardInfo;
|
||||
_errorCardInfo = info;
|
||||
|
||||
if (oldInfo == null && info != null) {
|
||||
notifyItemInserted(0);
|
||||
} else if (oldInfo != null && info == null) {
|
||||
notifyItemRemoved(0);
|
||||
} else {
|
||||
notifyItemChanged(0);
|
||||
if (Objects.equals(info, _entryList.getErrorCardInfo())) {
|
||||
return;
|
||||
}
|
||||
|
||||
replaceEntryList(new EntryList(
|
||||
_entryList.getEntries(),
|
||||
_entryList.getShownEntries(),
|
||||
info
|
||||
));
|
||||
}
|
||||
|
||||
public VaultEntry getEntryAtPos(int position) {
|
||||
return _shownEntries.get(translateEntryPosToIndex(position));
|
||||
public VaultEntry getEntryAtPosition(int position) {
|
||||
return _entryList.getShownEntries().get(_entryList.translateEntryPosToIndex(position));
|
||||
}
|
||||
|
||||
public int addEntry(VaultEntry entry) {
|
||||
_entries.add(entry);
|
||||
if (isEntryFiltered(entry)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int position = -1;
|
||||
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
|
||||
if (comparator != null) {
|
||||
// insert the entry in the correct order
|
||||
// note: this assumes that _shownEntries has already been sorted
|
||||
for (int i = getShownFavoritesCount(); i < _shownEntries.size(); i++) {
|
||||
if (comparator.compare(_shownEntries.get(i), entry) > 0) {
|
||||
_shownEntries.add(i, entry);
|
||||
position = translateEntryIndexToPos(i);
|
||||
notifyItemInserted(position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (position < 0) {
|
||||
_shownEntries.add(entry);
|
||||
|
||||
position = translateEntryIndexToPos(getShownEntriesCount() - 1);
|
||||
if (position == 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
}
|
||||
|
||||
_view.onListChange();
|
||||
checkPeriodUniformity();
|
||||
updateFooter();
|
||||
return position;
|
||||
public int getEntryPosition(VaultEntry entry) {
|
||||
return _entryList.translateEntryIndexToPos(_entryList.getShownEntries().indexOf(entry));
|
||||
}
|
||||
|
||||
public void addEntries(Collection<VaultEntry> entries) {
|
||||
for (VaultEntry entry: entries) {
|
||||
public void setEntries(List<VaultEntry> entries) {
|
||||
// TODO: Move these fields to separate dedicated model for the UI
|
||||
for (VaultEntry entry : entries) {
|
||||
entry.setUsageCount(_usageCounts.containsKey(entry.getUUID()) ? _usageCounts.get(entry.getUUID()) : 0);
|
||||
entry.setLastUsedTimestamp(_lastUsedTimestamps.containsKey(entry.getUUID()) ? _lastUsedTimestamps.get(entry.getUUID()) : 0);
|
||||
}
|
||||
|
||||
_entries.addAll(entries);
|
||||
updateShownEntries();
|
||||
checkPeriodUniformity(true);
|
||||
}
|
||||
|
||||
public void removeEntry(VaultEntry entry) {
|
||||
_entries.remove(entry);
|
||||
|
||||
if (_shownEntries.contains(entry)) {
|
||||
int index = _shownEntries.indexOf(entry);
|
||||
_shownEntries.remove(index);
|
||||
|
||||
int position = translateEntryIndexToPos(index);
|
||||
notifyItemRemoved(position);
|
||||
|
||||
updateFooter();
|
||||
}
|
||||
|
||||
_view.onListChange();
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void removeEntry(UUID uuid) {
|
||||
VaultEntry entry = getEntryByUUID(uuid);
|
||||
removeEntry(entry);
|
||||
replaceEntryList(new EntryList(
|
||||
entries,
|
||||
calculateShownEntries(entries),
|
||||
_entryList.getErrorCardInfo()
|
||||
));
|
||||
}
|
||||
|
||||
public void clearEntries() {
|
||||
_entries.clear();
|
||||
_shownEntries.clear();
|
||||
notifyDataSetChanged();
|
||||
checkPeriodUniformity();
|
||||
replaceEntryList(new EntryList());
|
||||
}
|
||||
|
||||
public void replaceEntry(UUID uuid, VaultEntry newEntry) {
|
||||
VaultEntry oldEntry = getEntryByUUID(uuid);
|
||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||
|
||||
if (_shownEntries.contains(oldEntry)) {
|
||||
int index = _shownEntries.indexOf(oldEntry);
|
||||
int position = translateEntryIndexToPos(index);
|
||||
if (isEntryFiltered(newEntry)) {
|
||||
_shownEntries.remove(index);
|
||||
notifyItemRemoved(position);
|
||||
} else {
|
||||
_shownEntries.set(index, newEntry);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
sortShownEntries();
|
||||
int newIndex = _shownEntries.indexOf(newEntry);
|
||||
int newPosition = translateEntryIndexToPos(newIndex);
|
||||
if (newPosition != NO_POSITION && position != newPosition) {
|
||||
notifyItemMoved(position, newPosition);
|
||||
}
|
||||
} else if (!isEntryFiltered(newEntry)) {
|
||||
// NOTE: This logic is wrong, because sorting is not taken into account. This code
|
||||
// path is currently never hit though, because it is not possible to edit an entry
|
||||
// that is not shown.
|
||||
_shownEntries.add(newEntry);
|
||||
|
||||
int position = getItemCount() - 1;
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
|
||||
checkPeriodUniformity();
|
||||
updateFooter();
|
||||
}
|
||||
|
||||
private VaultEntry getEntryByUUID(UUID uuid) {
|
||||
for (VaultEntry entry : _entries) {
|
||||
if (entry.getUUID().equals(uuid)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given entry position in the recycler view, to its index in the shown entries list.
|
||||
*/
|
||||
public int translateEntryPosToIndex(int position) {
|
||||
if (position == NO_POSITION) {
|
||||
return NO_POSITION;
|
||||
}
|
||||
|
||||
if (isErrorCardShown()) {
|
||||
position -= 1;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given entry index in the shown entries list, to its position in the recycler view.
|
||||
*/
|
||||
private int translateEntryIndexToPos(int index) {
|
||||
if (index == NO_POSITION) {
|
||||
return NO_POSITION;
|
||||
}
|
||||
|
||||
if (isErrorCardShown()) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return index;
|
||||
return _entryList.translateEntryPosToIndex(position);
|
||||
}
|
||||
|
||||
private boolean isEntryFiltered(VaultEntry entry) {
|
||||
|
@ -315,6 +197,19 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
String name = entry.getName().toLowerCase();
|
||||
String note = entry.getNote().toLowerCase();
|
||||
|
||||
if (_searchFilter != null) {
|
||||
String[] tokens = _searchFilter.toLowerCase().split("\\s+");
|
||||
|
||||
// Return true if not all tokens match at least one of the relevant fields
|
||||
return !Arrays.stream(tokens)
|
||||
.allMatch(token ->
|
||||
((_searchBehaviorMask & Preferences.SEARCH_IN_ISSUER) != 0 && issuer.contains(token)) ||
|
||||
((_searchBehaviorMask & Preferences.SEARCH_IN_NAME) != 0 && name.contains(token)) ||
|
||||
((_searchBehaviorMask & Preferences.SEARCH_IN_NOTE) != 0 && note.contains(token)) ||
|
||||
((_searchBehaviorMask & Preferences.SEARCH_IN_GROUPS) != 0 && doesAnyGroupMatchSearchFilter(groups, token))
|
||||
);
|
||||
}
|
||||
|
||||
if (!_groupFilter.isEmpty()) {
|
||||
if (groups.isEmpty() && !_groupFilter.contains(null)) {
|
||||
return true;
|
||||
|
@ -324,14 +219,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
}
|
||||
}
|
||||
|
||||
if (_searchFilter == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((_searchBehaviorMask & Preferences.SEARCH_IN_ISSUER) == 0 || !issuer.contains(_searchFilter))
|
||||
&& ((_searchBehaviorMask & Preferences.SEARCH_IN_NAME) == 0 || !name.contains(_searchFilter))
|
||||
&& ((_searchBehaviorMask & Preferences.SEARCH_IN_NOTE) == 0 || !note.contains(_searchFilter))
|
||||
&& ((_searchBehaviorMask & Preferences.SEARCH_IN_GROUPS) == 0 || !doesAnyGroupMatchSearchFilter(entry.getGroups(), _searchFilter));
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean doesAnyGroupMatchSearchFilter(Set<UUID> entryGroupUUIDs, String searchFilter) {
|
||||
|
@ -343,7 +231,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
|
||||
public void refresh(boolean hard) {
|
||||
if (hard) {
|
||||
updateShownEntries();
|
||||
refreshEntryList();
|
||||
} else {
|
||||
for (EntryHolder holder : _holders) {
|
||||
holder.refresh();
|
||||
|
@ -358,8 +246,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
}
|
||||
|
||||
_groupFilter = groups;
|
||||
updateShownEntries();
|
||||
checkPeriodUniformity();
|
||||
refreshEntryList();
|
||||
}
|
||||
|
||||
public void setSortCategory(SortCategory category, boolean apply) {
|
||||
|
@ -369,7 +256,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
|
||||
_sortCategory = category;
|
||||
if (apply) {
|
||||
updateShownEntries();
|
||||
refreshEntryList();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -378,25 +265,59 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
}
|
||||
|
||||
public void setSearchFilter(String search) {
|
||||
_searchFilter = (search != null && !search.isEmpty()) ? search.toLowerCase().trim() : null;
|
||||
updateShownEntries();
|
||||
String newSearchFilter = (search != null && !search.isEmpty())
|
||||
? search.toLowerCase().trim() : null;
|
||||
|
||||
if (!Objects.equals(_searchFilter, newSearchFilter)) {
|
||||
_searchFilter = newSearchFilter;
|
||||
refreshEntryList();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShownEntries() {
|
||||
// clear the list of shown entries first
|
||||
_shownEntries.clear();
|
||||
private void refreshEntryList() {
|
||||
replaceEntryList(new EntryList(
|
||||
_entryList.getEntries(),
|
||||
calculateShownEntries(_entryList.getEntries()),
|
||||
_entryList.getErrorCardInfo()
|
||||
));
|
||||
}
|
||||
|
||||
// add entries back that are not filtered out
|
||||
for (VaultEntry entry : _entries) {
|
||||
private void replaceEntryList(EntryList newEntryList) {
|
||||
DiffUtil.DiffResult diffRes = DiffUtil.calculateDiff(new DiffCallback(_entryList, newEntryList));
|
||||
_entryList = newEntryList;
|
||||
updatePeriodUniformity();
|
||||
|
||||
// This scroll position trick is required in order to not have the recycler view
|
||||
// jump to some random position after a large change (like resorting entries)
|
||||
// Related: https://issuetracker.google.com/issues/70149059
|
||||
int scrollPos = _view.getScrollPosition();
|
||||
diffRes.dispatchUpdatesTo(this);
|
||||
_view.scrollToPosition(scrollPos);
|
||||
_view.onListChange();
|
||||
}
|
||||
|
||||
private List<VaultEntry> calculateShownEntries(List<VaultEntry> entries) {
|
||||
List<VaultEntry> res = new ArrayList<>();
|
||||
for (VaultEntry entry : entries) {
|
||||
if (!isEntryFiltered(entry)) {
|
||||
_shownEntries.add(entry);
|
||||
res.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
sortShownEntries();
|
||||
checkPeriodUniformity();
|
||||
_view.onListChange();
|
||||
notifyDataSetChanged();
|
||||
sortEntries(res, _sortCategory);
|
||||
return res;
|
||||
}
|
||||
|
||||
private static void sortEntries(List<VaultEntry> entries, SortCategory sortCategory) {
|
||||
if (sortCategory != null) {
|
||||
Comparator<VaultEntry> comparator = sortCategory.getComparator();
|
||||
if (comparator != null) {
|
||||
Collections.sort(entries, comparator);
|
||||
}
|
||||
}
|
||||
|
||||
Comparator<VaultEntry> favoriteComparator = new FavoriteComparator();
|
||||
Collections.sort(entries, favoriteComparator);
|
||||
}
|
||||
|
||||
private boolean isEntryDraggable(VaultEntry entry) {
|
||||
|
@ -407,18 +328,6 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
&& _selectedEntries.get(0) == entry;
|
||||
}
|
||||
|
||||
private void sortShownEntries() {
|
||||
if (_sortCategory != null) {
|
||||
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
|
||||
if (comparator != null) {
|
||||
Collections.sort(_shownEntries, comparator);
|
||||
}
|
||||
}
|
||||
|
||||
Comparator<VaultEntry> favoriteComparator = new FavoriteComparator();
|
||||
Collections.sort(_shownEntries, favoriteComparator);
|
||||
}
|
||||
|
||||
public void setViewMode(ViewMode viewMode) {
|
||||
_viewMode = viewMode;
|
||||
}
|
||||
|
@ -434,7 +343,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
public Map<UUID, Long> getLastUsedTimestamps() { return _lastUsedTimestamps; }
|
||||
|
||||
public int getShownFavoritesCount() {
|
||||
return (int) _shownEntries.stream().filter(VaultEntry::isFavorite).count();
|
||||
return (int) _entryList.getShownEntries().stream().filter(VaultEntry::isFavorite).count();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -446,43 +355,48 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
public void onItemDrop(int position) {
|
||||
// moving entries is not allowed when a filter is applied
|
||||
// footer cant be moved, nor can items be moved below it
|
||||
if (!_groupFilter.isEmpty() || isPositionFooter(position) || isPositionErrorCard(position)) {
|
||||
if (!_groupFilter.isEmpty() || _entryList.isPositionFooter(position) || _entryList.isPositionErrorCard(position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int index = translateEntryPosToIndex(position);
|
||||
_view.onEntryDrop(_shownEntries.get(index));
|
||||
int index = _entryList.translateEntryPosToIndex(position);
|
||||
_view.onEntryDrop(_entryList.getShownEntries().get(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMove(int firstPosition, int secondPosition) {
|
||||
// moving entries is not allowed when a filter is applied
|
||||
// footer cant be moved, nor can items be moved below it
|
||||
// Moving entries is not allowed when a filter is applied. The footer can't be
|
||||
// moved, nor can items be moved below it
|
||||
if (!_groupFilter.isEmpty()
|
||||
|| isPositionFooter(firstPosition) || isPositionFooter(secondPosition)
|
||||
|| isPositionErrorCard(firstPosition) || isPositionErrorCard(secondPosition)) {
|
||||
|| _entryList.isPositionFooter(firstPosition) || _entryList.isPositionFooter(secondPosition)
|
||||
|| _entryList.isPositionErrorCard(firstPosition) || _entryList.isPositionErrorCard(secondPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// notify the vault first
|
||||
int firstIndex = translateEntryPosToIndex(firstPosition);
|
||||
int secondIndex = translateEntryPosToIndex(secondPosition);
|
||||
_view.onEntryMove(_entries.get(firstIndex), _entries.get(secondIndex));
|
||||
// Notify the vault about the entry position change first
|
||||
int firstIndex = _entryList.translateEntryPosToIndex(firstPosition);
|
||||
int secondIndex = _entryList.translateEntryPosToIndex(secondPosition);
|
||||
VaultEntry firstEntry = _entryList.getShownEntries().get(firstIndex);
|
||||
VaultEntry secondEntry = _entryList.getShownEntries().get(secondIndex);
|
||||
_view.onEntryMove(firstEntry, secondEntry);
|
||||
|
||||
// then update our end
|
||||
CollectionUtils.move(_entries, firstIndex, secondIndex);
|
||||
CollectionUtils.move(_shownEntries, firstIndex, secondIndex);
|
||||
|
||||
notifyItemMoved(firstPosition, secondPosition);
|
||||
// Then update the visual end
|
||||
List<VaultEntry> newEntries = new ArrayList<>(_entryList.getEntries());
|
||||
CollectionUtils.move(newEntries, newEntries.indexOf(firstEntry), newEntries.indexOf(secondEntry));
|
||||
replaceEntryList(new EntryList(
|
||||
newEntries,
|
||||
calculateShownEntries(newEntries),
|
||||
_entryList.getErrorCardInfo()
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (isPositionErrorCard(position)) {
|
||||
if (_entryList.isPositionErrorCard(position)) {
|
||||
return R.layout.card_error;
|
||||
}
|
||||
|
||||
if (isPositionFooter(position)) {
|
||||
if (_entryList.isPositionFooter(position)) {
|
||||
return R.layout.card_footer;
|
||||
}
|
||||
|
||||
|
@ -497,7 +411,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
View view = inflater.inflate(viewType, parent, false);
|
||||
|
||||
if (viewType == R.layout.card_error) {
|
||||
holder = new ErrorCardHolder(view, _errorCardInfo);
|
||||
holder = new ErrorCardHolder(view, Objects.requireNonNull(_entryList.getErrorCardInfo()));
|
||||
} else if (viewType == R.layout.card_footer) {
|
||||
holder = new FooterView(view);
|
||||
} else {
|
||||
|
@ -523,23 +437,23 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
|
||||
if (holder instanceof EntryHolder) {
|
||||
EntryHolder entryHolder = (EntryHolder) holder;
|
||||
int index = translateEntryPosToIndex(position);
|
||||
VaultEntry entry = _shownEntries.get(index);
|
||||
int index = _entryList.translateEntryPosToIndex(position);
|
||||
VaultEntry entry = _entryList.getShownEntries().get(index);
|
||||
|
||||
boolean hidden = _tapToReveal && entry != _focusedEntry;
|
||||
boolean paused = _pauseFocused && entry == _focusedEntry;
|
||||
boolean dimmed = (_highlightEntry || _tempHighlightEntry) && _focusedEntry != null && _focusedEntry != entry;
|
||||
boolean hidden = _tapToReveal && !entry.equals(_focusedEntry);
|
||||
boolean paused = _pauseFocused && entry.equals(_focusedEntry);
|
||||
boolean dimmed = (_highlightEntry || _tempHighlightEntry) && _focusedEntry != null && !_focusedEntry.equals(entry);
|
||||
boolean showProgress = entry.getInfo() instanceof TotpInfo && ((TotpInfo) entry.getInfo()).getPeriod() != getMostFrequentPeriod();
|
||||
boolean showAccountName = true;
|
||||
if (_onlyShowNecessaryAccountNames) {
|
||||
// Only show account name when there's multiple entries found with the same issuer.
|
||||
showAccountName = _entries.stream()
|
||||
showAccountName = _entryList.getEntries().stream()
|
||||
.filter(x -> x.getIssuer().equals(entry.getIssuer()))
|
||||
.count() > 1;
|
||||
}
|
||||
|
||||
AccountNamePosition accountNamePosition = showAccountName ? _accountNamePosition : AccountNamePosition.HIDDEN;
|
||||
entryHolder.setData(entry, _codeGroupSize, _viewMode, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed);
|
||||
entryHolder.setData(entry, _codeGroupSize, _viewMode, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed, _showExpirationState, _showNextCode);
|
||||
entryHolder.setFocused(_selectedEntries.contains(entry));
|
||||
entryHolder.setShowDragHandle(isEntryDraggable(entry));
|
||||
|
||||
|
@ -554,13 +468,13 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
|
||||
if (_selectedEntries.isEmpty()) {
|
||||
if (_highlightEntry || _tempHighlightEntry || _tapToReveal) {
|
||||
if (_focusedEntry == entry) {
|
||||
if (_focusedEntry != null && _focusedEntry.equals(entry)) {
|
||||
resetFocus();
|
||||
|
||||
// Prevent copying when singletap is set and focus is reset
|
||||
handled = _copyBehavior == CopyBehavior.SINGLETAP;
|
||||
} else {
|
||||
focusEntry(entry, _tapToRevealTime);
|
||||
|
||||
// Prevent copying when singletap is set and the entry is being revealed
|
||||
handled = _copyBehavior == CopyBehavior.SINGLETAP && _tapToReveal;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -611,8 +525,8 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
entryHolder.setFocusedAndAnimate(true);
|
||||
}
|
||||
|
||||
int index = translateEntryPosToIndex(position);
|
||||
boolean returnVal = _view.onLongEntryClick(_shownEntries.get(index));
|
||||
int index = _entryList.translateEntryPosToIndex(position);
|
||||
boolean returnVal = _view.onLongEntryClick(_entryList.getShownEntries().get(index));
|
||||
if (_selectedEntries.size() == 0 || isEntryDraggable(entry)) {
|
||||
_view.startDrag(entryHolder);
|
||||
}
|
||||
|
@ -658,15 +572,10 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
}
|
||||
}
|
||||
|
||||
private void checkPeriodUniformity() {
|
||||
checkPeriodUniformity(false);
|
||||
}
|
||||
|
||||
private void checkPeriodUniformity(boolean force) {
|
||||
private void updatePeriodUniformity() {
|
||||
int mostFrequentPeriod = getMostFrequentPeriod();
|
||||
boolean uniform = isPeriodUniform();
|
||||
|
||||
if (!force && uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) {
|
||||
if (uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -684,7 +593,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
|
||||
public int getMostFrequentPeriod() {
|
||||
List<TotpInfo> infos = new ArrayList<>();
|
||||
for (VaultEntry entry : _shownEntries) {
|
||||
for (VaultEntry entry : _entryList.getShownEntries()) {
|
||||
OtpInfo info = entry.getInfo();
|
||||
if (info instanceof TotpInfo) {
|
||||
infos.add((TotpInfo) info);
|
||||
|
@ -726,7 +635,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
_dimHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
for (EntryHolder holder : _holders) {
|
||||
if (holder.getEntry() != _focusedEntry) {
|
||||
if (!holder.getEntry().equals(_focusedEntry)) {
|
||||
if (_highlightEntry || _tempHighlightEntry) {
|
||||
holder.dim();
|
||||
}
|
||||
|
@ -799,9 +708,9 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
public List<VaultEntry> selectAllEntries() {
|
||||
_selectedEntries.clear();
|
||||
|
||||
for (VaultEntry entry: _shownEntries) {
|
||||
for (VaultEntry entry: _entryList.getShownEntries()) {
|
||||
for (EntryHolder holder: _holders) {
|
||||
if (holder.getEntry() == entry) {
|
||||
if (holder.getEntry().equals(entry)) {
|
||||
holder.setFocused(true);
|
||||
}
|
||||
}
|
||||
|
@ -816,7 +725,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
public void deselectAllEntries() {
|
||||
for (VaultEntry entry: _selectedEntries) {
|
||||
for (EntryHolder holder : _holders) {
|
||||
if (holder.getEntry() == entry) {
|
||||
if (holder.getEntry().equals(entry)) {
|
||||
holder.setFocusedAndAnimate(false);
|
||||
break;
|
||||
}
|
||||
|
@ -853,34 +762,23 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
// Always at least one item because of the footer
|
||||
// Two in case there's also an error card
|
||||
int baseCount = 1;
|
||||
if (isErrorCardShown()) {
|
||||
baseCount++;
|
||||
}
|
||||
|
||||
return baseCount + getShownEntriesCount();
|
||||
return _entryList.getItemCount();
|
||||
}
|
||||
|
||||
public int getShownEntriesCount() {
|
||||
return _shownEntries.size();
|
||||
return _entryList.getShownEntries().size();
|
||||
}
|
||||
|
||||
public boolean isPositionFooter(int position) {
|
||||
return position == (getItemCount() - 1);
|
||||
return _entryList.isPositionFooter(position);
|
||||
}
|
||||
|
||||
public boolean isPositionErrorCard(int position) {
|
||||
return isErrorCardShown() && position == 0;
|
||||
return _entryList.isPositionErrorCard(position);
|
||||
}
|
||||
|
||||
public boolean isErrorCardShown() {
|
||||
return _errorCardInfo != null;
|
||||
}
|
||||
|
||||
private void updateFooter() {
|
||||
notifyItemChanged(getItemCount() - 1);
|
||||
return _entryList.isErrorCardShown();
|
||||
}
|
||||
|
||||
private class FooterView extends RecyclerView.ViewHolder {
|
||||
|
@ -907,6 +805,151 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
}
|
||||
}
|
||||
|
||||
private static class EntryList {
|
||||
private final List<VaultEntry> _entries;
|
||||
private final List<VaultEntry> _shownEntries;
|
||||
private final ErrorCardInfo _errorCardInfo;
|
||||
|
||||
public EntryList() {
|
||||
this(new ArrayList<>(), new ArrayList<>(), null);
|
||||
}
|
||||
|
||||
public EntryList(
|
||||
@NonNull List<VaultEntry> entries,
|
||||
@NonNull List<VaultEntry> shownEntries,
|
||||
@Nullable ErrorCardInfo errorCardInfo
|
||||
) {
|
||||
_entries = entries;
|
||||
_shownEntries = shownEntries;
|
||||
_errorCardInfo = errorCardInfo;
|
||||
}
|
||||
|
||||
public List<VaultEntry> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
public List<VaultEntry> getShownEntries() {
|
||||
return _shownEntries;
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
// Always at least one item because of the footer
|
||||
// Two in case there's also an error card
|
||||
int baseCount = 1;
|
||||
if (isErrorCardShown()) {
|
||||
baseCount++;
|
||||
}
|
||||
|
||||
return baseCount + getShownEntries().size();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ErrorCardInfo getErrorCardInfo() {
|
||||
return _errorCardInfo;
|
||||
}
|
||||
|
||||
public boolean isErrorCardShown() {
|
||||
return _errorCardInfo != null;
|
||||
}
|
||||
|
||||
public boolean isPositionErrorCard(int position) {
|
||||
return isErrorCardShown() && position == 0;
|
||||
}
|
||||
|
||||
public boolean isPositionFooter(int position) {
|
||||
return position == (getItemCount() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given entry position in the recycler view, to its index in the shown entries list.
|
||||
*/
|
||||
public int translateEntryPosToIndex(int position) {
|
||||
if (position == NO_POSITION) {
|
||||
return NO_POSITION;
|
||||
}
|
||||
|
||||
if (isErrorCardShown()) {
|
||||
position -= 1;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given entry index in the shown entries list, to its position in the recycler view.
|
||||
*/
|
||||
public int translateEntryIndexToPos(int index) {
|
||||
if (index == NO_POSITION) {
|
||||
return NO_POSITION;
|
||||
}
|
||||
|
||||
if (isErrorCardShown()) {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class DiffCallback extends DiffUtil.Callback {
|
||||
private final EntryList _old;
|
||||
private final EntryList _new;
|
||||
|
||||
public DiffCallback(EntryList oldList, EntryList newList) {
|
||||
_old = oldList;
|
||||
_new = newList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOldListSize() {
|
||||
return _old.getItemCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewListSize() {
|
||||
return _new.getItemCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
if (_old.isPositionErrorCard(oldItemPosition) != _new.isPositionErrorCard(newItemPosition)
|
||||
|| _old.isPositionFooter(oldItemPosition) != _new.isPositionFooter(newItemPosition)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((_old.isPositionFooter(oldItemPosition) && _new.isPositionFooter(newItemPosition))
|
||||
|| (_old.isPositionErrorCard(oldItemPosition) && _new.isPositionErrorCard(newItemPosition))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int oldEntryIndex = _old.translateEntryPosToIndex(oldItemPosition);
|
||||
int newEntryIndex = _new.translateEntryPosToIndex(newItemPosition);
|
||||
if (oldEntryIndex < 0 || newEntryIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _old.getShownEntries().get(oldEntryIndex).getUUID()
|
||||
.equals(_new.getShownEntries().get(newEntryIndex).getUUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
if (_old.isPositionFooter(oldItemPosition) && _new.isPositionFooter(newItemPosition)) {
|
||||
return _old.getShownEntries().size() == _new.getShownEntries().size();
|
||||
}
|
||||
|
||||
if (_old.isPositionErrorCard(oldItemPosition) && _new.isPositionErrorCard(newItemPosition)) {
|
||||
return Objects.equals(_old.getErrorCardInfo(), _new.getErrorCardInfo());
|
||||
}
|
||||
|
||||
int oldEntryIndex = _old.translateEntryPosToIndex(oldItemPosition);
|
||||
int newEntryIndex = _new.translateEntryPosToIndex(newItemPosition);
|
||||
return _old.getShownEntries().get(oldEntryIndex)
|
||||
.equals(_new.getShownEntries().get(newEntryIndex));
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onEntryClick(VaultEntry entry);
|
||||
boolean onLongEntryClick(VaultEntry entry);
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
|
@ -35,6 +40,7 @@ import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
|||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private static final float DEFAULT_ALPHA = 1.0f;
|
||||
|
@ -44,6 +50,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
private View _favoriteIndicator;
|
||||
private TextView _profileName;
|
||||
private TextView _profileCode;
|
||||
private TextView _nextProfileCode;
|
||||
private TextView _profileIssuer;
|
||||
private TextView _profileCopied;
|
||||
private ImageView _profileDrawable;
|
||||
|
@ -52,21 +59,23 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
private RelativeLayout _description;
|
||||
private ImageView _dragHandle;
|
||||
private ViewMode _viewMode;
|
||||
|
||||
|
||||
private final ImageView _selected;
|
||||
private final Handler _selectedHandler;
|
||||
|
||||
private Preferences.CodeGrouping _codeGrouping = Preferences.CodeGrouping.NO_GROUPING;
|
||||
private AccountNamePosition _accountNamePosition = AccountNamePosition.HIDDEN;
|
||||
|
||||
private boolean _hidden;
|
||||
private boolean _paused;
|
||||
|
||||
private TotpProgressBar _progressBar;
|
||||
private MaterialCardView _view;
|
||||
|
||||
private UiRefresher _refresher;
|
||||
private Handler _animationHandler;
|
||||
private Handler _copyAnimationHandler;
|
||||
private Handler _expirationHandler;
|
||||
private AnimatorSet _expirationAnimSet;
|
||||
private boolean _showNextCode;
|
||||
private boolean _showExpirationState;
|
||||
|
||||
private Animation _scaleIn;
|
||||
private Animation _scaleOut;
|
||||
|
@ -77,6 +86,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
_view = (MaterialCardView) view;
|
||||
_profileName = view.findViewById(R.id.profile_account_name);
|
||||
_profileCode = view.findViewById(R.id.profile_code);
|
||||
_nextProfileCode = view.findViewById(R.id.next_profile_code);
|
||||
_profileIssuer = view.findViewById(R.id.profile_issuer);
|
||||
_profileCopied = view.findViewById(R.id.profile_copied);
|
||||
_description = view.findViewById(R.id.description);
|
||||
|
@ -86,8 +96,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
_dragHandle = view.findViewById(R.id.drag_handle);
|
||||
_favoriteIndicator = view.findViewById(R.id.favorite_indicator);
|
||||
|
||||
_selectedHandler = new Handler();
|
||||
_animationHandler = new Handler();
|
||||
_copyAnimationHandler = new Handler();
|
||||
_expirationHandler = new Handler();
|
||||
|
||||
_progressBar = view.findViewById(R.id.progressBar);
|
||||
|
||||
|
@ -97,24 +107,31 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
_refresher = new UiRefresher(new UiRefresher.Listener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
if (!_hidden && !_paused) {
|
||||
refreshCode();
|
||||
}
|
||||
refreshCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExpiring() { }
|
||||
|
||||
@Override
|
||||
public long getMillisTillNextRefresh() {
|
||||
return ((TotpInfo) _entry.getInfo()).getMillisTillNextRotation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPeriodMillis() {
|
||||
return ((TotpInfo) _entry.getInfo()).getPeriod() * 1000L;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed) {
|
||||
public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean nonUniform, boolean hidden, boolean paused, boolean dimmed, boolean showExpirationState, boolean showNextCode) {
|
||||
_entry = entry;
|
||||
_hidden = hidden;
|
||||
_paused = paused;
|
||||
_codeGrouping = groupSize;
|
||||
_viewMode = viewMode;
|
||||
|
||||
_accountNamePosition = accountNamePosition;
|
||||
if (viewMode.equals(ViewMode.TILES) && _accountNamePosition == AccountNamePosition.END) {
|
||||
_accountNamePosition = AccountNamePosition.BELOW;
|
||||
|
@ -122,16 +139,19 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
_selected.clearAnimation();
|
||||
_selected.setVisibility(View.GONE);
|
||||
_selectedHandler.removeCallbacksAndMessages(null);
|
||||
_animationHandler.removeCallbacksAndMessages(null);
|
||||
_copyAnimationHandler.removeCallbacksAndMessages(null);
|
||||
_expirationHandler.removeCallbacksAndMessages(null);
|
||||
_showNextCode = entry.getInfo() instanceof TotpInfo && showNextCode;
|
||||
_showExpirationState = _entry.getInfo() instanceof TotpInfo && showExpirationState;
|
||||
|
||||
_favoriteIndicator.setVisibility(_entry.isFavorite() ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
// only show the progress bar if there is no uniform period and the entry type is TotpInfo
|
||||
setShowProgress(showProgress);
|
||||
setShowProgress(nonUniform);
|
||||
|
||||
// only show the button if this entry is of type HotpInfo
|
||||
_buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE);
|
||||
_nextProfileCode.setVisibility(_showNextCode ? View.VISIBLE : View.GONE);
|
||||
|
||||
String profileIssuer = entry.getIssuer();
|
||||
String profileName = entry.getName();
|
||||
|
@ -149,7 +169,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
showIcon(showIcon);
|
||||
|
||||
itemView.setAlpha(dimmed ? DIMMED_ALPHA : DEFAULT_ALPHA);
|
||||
}
|
||||
|
||||
|
@ -277,15 +296,24 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
public void refreshCode() {
|
||||
if (!_hidden && !_paused) {
|
||||
updateCode();
|
||||
updateCodes();
|
||||
startExpirationAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCode() {
|
||||
private void updateCodes() {
|
||||
_profileCode.setText(getOtp());
|
||||
|
||||
if (_showNextCode) {
|
||||
_nextProfileCode.setText(getOtp(1));
|
||||
}
|
||||
}
|
||||
|
||||
private String getOtp() {
|
||||
return getOtp(0);
|
||||
}
|
||||
|
||||
private String getOtp(int offset) {
|
||||
OtpInfo info = _entry.getInfo();
|
||||
|
||||
// In previous versions of Aegis, it was possible to import entries with an empty
|
||||
|
@ -294,7 +322,12 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
// the OTP, instead of crashing.
|
||||
String otp;
|
||||
try {
|
||||
otp = info.getOtp();
|
||||
if (info instanceof TotpInfo) {
|
||||
otp = ((TotpInfo)info).getOtp((System.currentTimeMillis() / 1000) + ((long) (offset) * ((TotpInfo) _entry.getInfo()).getPeriod()));
|
||||
} else {
|
||||
otp = info.getOtp();
|
||||
}
|
||||
|
||||
if (!(info instanceof SteamInfo || info instanceof YandexInfo)) {
|
||||
otp = formatCode(otp);
|
||||
}
|
||||
|
@ -335,14 +368,18 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
public void revealCode() {
|
||||
updateCode();
|
||||
updateCodes();
|
||||
startExpirationAnimation();
|
||||
_hidden = false;
|
||||
}
|
||||
|
||||
public void hideCode() {
|
||||
String code = getOtp();
|
||||
String hiddenText = code.replaceAll("\\S", Character.toString(HIDDEN_CHAR));
|
||||
stopExpirationAnimation();
|
||||
|
||||
updateTextViewWithDots(_profileCode, hiddenText, code);
|
||||
updateTextViewWithDots(_nextProfileCode, hiddenText, code);
|
||||
|
||||
_hidden = true;
|
||||
}
|
||||
|
@ -356,6 +393,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
float dotsWidth = paint.measureText(hiddenCode);
|
||||
float scaleFactor = codeWidth / dotsWidth;
|
||||
scaleFactor = (float)(Math.round(scaleFactor * 10.0) / 10.0);
|
||||
textView.setTextColor(MaterialColors.getColor(textView, R.attr.colorCodeHidden));
|
||||
|
||||
// If scale is higher or equal to 0.8, do nothing and proceed with the normal text rendering
|
||||
if (scaleFactor >= 0.8) {
|
||||
|
@ -387,6 +425,73 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
textView.setText(dotsString);
|
||||
}
|
||||
|
||||
public void startExpirationAnimation() {
|
||||
stopExpirationAnimation();
|
||||
if (!_showExpirationState) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int totalStateDuration = 7000;
|
||||
TotpInfo info = (TotpInfo) _entry.getInfo();
|
||||
if (info.getPeriod() * 1000 <= totalStateDuration) {
|
||||
_profileCode.setTextColor(MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError));
|
||||
return;
|
||||
}
|
||||
|
||||
// Workaround for when animations are disabled or Android version being too old
|
||||
float durationScale = AnimationsHelper.Scale.ANIMATOR.getValue(itemView.getContext());
|
||||
if (durationScale == 0.0 || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
int color = MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError);
|
||||
if (info.getMillisTillNextRotation() < totalStateDuration) {
|
||||
_profileCode.setTextColor(color);
|
||||
} else {
|
||||
_expirationHandler.postDelayed(() -> {
|
||||
_profileCode.setTextColor(color);
|
||||
}, info.getMillisTillNextRotation() - totalStateDuration);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final int colorShiftDuration = 300;
|
||||
long delayAnimDuration = info.getPeriod() * 1000L - totalStateDuration - colorShiftDuration;
|
||||
ValueAnimator delayAnim = ValueAnimator.ofFloat(0f, 0f);
|
||||
delayAnim.setDuration((long) (delayAnimDuration / durationScale));
|
||||
|
||||
int colorFrom = _profileCode.getCurrentTextColor();
|
||||
int colorTo = MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError);
|
||||
ValueAnimator colorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
|
||||
colorAnim.setDuration((long) (colorShiftDuration / durationScale));
|
||||
colorAnim.addUpdateListener(a -> _profileCode.setTextColor((int) a.getAnimatedValue()));
|
||||
|
||||
final int blinkDuration = 3000;
|
||||
ValueAnimator delayAnim2 = ValueAnimator.ofFloat(0f, 0f);
|
||||
delayAnim2.setDuration((long) ((totalStateDuration - blinkDuration) / durationScale));
|
||||
|
||||
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(_profileCode, "alpha", 1f, .5f);
|
||||
alphaAnim.setDuration((long) (500 / durationScale));
|
||||
alphaAnim.setRepeatCount(blinkDuration / 500 - 1);
|
||||
alphaAnim.setRepeatMode(ValueAnimator.REVERSE);
|
||||
|
||||
_expirationAnimSet = new AnimatorSet();
|
||||
_expirationAnimSet.playSequentially(delayAnim, colorAnim, delayAnim2, alphaAnim);
|
||||
_expirationAnimSet.start();
|
||||
long currentPlayTime = (info.getPeriod() * 1000L) - info.getMillisTillNextRotation();
|
||||
_expirationAnimSet.setCurrentPlayTime((long) (currentPlayTime / durationScale));
|
||||
}
|
||||
|
||||
private void stopExpirationAnimation() {
|
||||
_expirationHandler.removeCallbacksAndMessages(null);
|
||||
if (_expirationAnimSet != null) {
|
||||
_expirationAnimSet.cancel();
|
||||
_expirationAnimSet = null;
|
||||
}
|
||||
|
||||
int colorTo = MaterialColors.getColor(_profileCode, R.attr.colorCode);
|
||||
_profileCode.setTextColor(colorTo);
|
||||
_profileCode.setAlpha(1f);
|
||||
}
|
||||
|
||||
public void showIcon(boolean show) {
|
||||
if (show) {
|
||||
_profileDrawable.setVisibility(View.VISIBLE);
|
||||
|
@ -402,8 +507,11 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
public void setPaused(boolean paused) {
|
||||
_paused = paused;
|
||||
|
||||
if (!_hidden && !_paused) {
|
||||
updateCode();
|
||||
if (_paused) {
|
||||
stopExpirationAnimation();
|
||||
} else if (!_hidden) {
|
||||
updateCodes();
|
||||
startExpirationAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -416,7 +524,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
public void animateCopyText() {
|
||||
_animationHandler.removeCallbacksAndMessages(null);
|
||||
_copyAnimationHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
Animation slideDownFadeIn = AnimationsHelper.loadScaledAnimation(itemView.getContext(), R.anim.slide_down_fade_in);
|
||||
Animation slideDownFadeOut = AnimationsHelper.loadScaledAnimation(itemView.getContext(), R.anim.slide_down_fade_out);
|
||||
|
@ -429,7 +537,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
View fadeOutView = (_accountNamePosition == AccountNamePosition.BELOW) ? _profileName : _description;
|
||||
fadeOutView.startAnimation(slideDownFadeOut);
|
||||
|
||||
_animationHandler.postDelayed(() -> {
|
||||
_copyAnimationHandler.postDelayed(() -> {
|
||||
_profileCopied.startAnimation(fadeOut);
|
||||
fadeOutView.startAnimation(fadeIn);
|
||||
}, 3000);
|
||||
|
@ -439,7 +547,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
_profileCopied.startAnimation(fadeIn);
|
||||
visibleProfileText.startAnimation(fadeOut);
|
||||
|
||||
_animationHandler.postDelayed(() -> {
|
||||
_copyAnimationHandler.postDelayed(() -> {
|
||||
_profileCopied.startAnimation(fadeOut);
|
||||
visibleProfileText.startAnimation(fadeIn);
|
||||
}, 3000);
|
||||
|
|
|
@ -13,15 +13,15 @@ import android.view.MotionEvent;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.LayoutAnimationController;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.widget.NestedScrollView;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
|
@ -33,17 +33,16 @@ import com.beemdevelopment.aegis.CopyBehavior;
|
|||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.VibrationPatterns;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.MetricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
import com.beemdevelopment.aegis.helpers.VibrationHelper;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
|
||||
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
|
||||
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
import com.bumptech.glide.Glide;
|
||||
|
@ -51,29 +50,25 @@ import com.bumptech.glide.ListPreloader;
|
|||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.android.material.shape.CornerFamily;
|
||||
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private EntryAdapter _adapter;
|
||||
private Listener _listener;
|
||||
private SimpleItemTouchHelperCallback _touchCallback;
|
||||
private ItemTouchHelper _touchHelper;
|
||||
private VibrationHelper _vibrationHelper;
|
||||
|
||||
private RecyclerView _recyclerView;
|
||||
private RecyclerView.ItemDecoration _itemDecoration;
|
||||
|
@ -81,11 +76,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
private TotpProgressBar _progressBar;
|
||||
private boolean _showProgress;
|
||||
private ViewMode _viewMode;
|
||||
private Collection<VaultGroup> _groups;
|
||||
private LinearLayout _emptyStateView;
|
||||
private Chip _groupChip;
|
||||
private Set<UUID> _groupFilter;
|
||||
private Set<UUID> _prefGroupFilter;
|
||||
|
||||
private UiRefresher _refresher;
|
||||
|
||||
|
@ -106,8 +97,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
|
||||
_progressBar = view.findViewById(R.id.progressBar);
|
||||
_groupChip = view.findViewById(R.id.chip_group);
|
||||
initializeGroupChip();
|
||||
_vibrationHelper = new VibrationHelper(getContext());
|
||||
|
||||
// set up the recycler view
|
||||
_recyclerView = view.findViewById(R.id.rvKeyProfiles);
|
||||
|
@ -157,12 +147,40 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
@Override
|
||||
public void onRefresh() {
|
||||
refresh(false);
|
||||
_vibrationHelper.vibratePattern(getContext(), VibrationPatterns.REFRESH_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExpiring() {
|
||||
_vibrationHelper.vibratePattern(getContext(), VibrationPatterns.EXPIRING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMillisTillNextRefresh() {
|
||||
return TotpInfo.getMillisTillNextRotation(_adapter.getMostFrequentPeriod());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPeriodMillis() {
|
||||
return _adapter.getMostFrequentPeriod() * 1000L;
|
||||
}
|
||||
});
|
||||
|
||||
final int rvInitialPaddingLeft = _recyclerView.getPaddingLeft();
|
||||
final int rvInitialPaddingTop = _recyclerView.getPaddingTop();
|
||||
final int rvInitialPaddingRight = _recyclerView.getPaddingRight();
|
||||
final int rvInitialPaddingBottom = _recyclerView.getPaddingBottom();
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(_recyclerView, (targetView, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
|
||||
// left and right padding seems to be handled by fitsSystemWindows="true" on the CoordinatorLayout in activity_main.xml
|
||||
targetView.setPadding(
|
||||
rvInitialPaddingLeft,
|
||||
rvInitialPaddingTop,
|
||||
rvInitialPaddingRight,
|
||||
rvInitialPaddingBottom + insets.bottom
|
||||
);
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
_emptyStateView = view.findViewById(R.id.vEmptyList);
|
||||
|
@ -173,27 +191,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
_preloadSizeProvider.setView(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
if (savedInstanceState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<UUID> filter = (HashSet<UUID>) savedInstanceState.getSerializable("prefGroupFilter");
|
||||
if (filter != null) {
|
||||
_prefGroupFilter = filter;
|
||||
}
|
||||
public int getScrollPosition() {
|
||||
return ((LinearLayoutManager) _recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
// user can apply _groupFilter without saving
|
||||
// restore _groupFilter as _prefGroupFilter in order to reapply correct filter after screen rotate
|
||||
if (_groupFilter != null) {
|
||||
outState.putSerializable("prefGroupFilter", new HashSet<>(_groupFilter));
|
||||
}
|
||||
public void scrollToPosition(int position) {
|
||||
_recyclerView.getLayoutManager().scrollToPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -202,16 +205,26 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
super.onDestroyView();
|
||||
}
|
||||
|
||||
public void setGroupFilter(Set<UUID> groups, boolean animate) {
|
||||
_groupFilter = groups;
|
||||
public void onRefreshStop() {
|
||||
_refresher.stop();
|
||||
}
|
||||
|
||||
public void onRefreshStart() {
|
||||
if (_adapter.getMostFrequentPeriod() != -1){
|
||||
_refresher.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void setGroups(Collection<VaultGroup> groups) {
|
||||
_adapter.setGroups(groups);
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setGroupFilter(Set<UUID> groups) {
|
||||
_adapter.setGroupFilter(groups);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
updateEmptyState();
|
||||
updateGroupChip();
|
||||
|
||||
if (animate) {
|
||||
runEntriesAnimation();
|
||||
}
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setIsLongPressDragEnabled(boolean enabled) {
|
||||
|
@ -244,10 +257,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
public void setSortCategory(SortCategory sortCategory, boolean apply) {
|
||||
_adapter.setSortCategory(sortCategory, apply);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
|
||||
if (apply) {
|
||||
runEntriesAnimation();
|
||||
}
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setUsageCounts(Map<UUID, Integer> usageCounts) {
|
||||
|
@ -269,8 +279,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
public void setSearchFilter(String search) {
|
||||
_adapter.setSearchFilter(search);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
|
||||
updateEmptyState();
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setSelectedEntry(VaultEntry entry) {
|
||||
|
@ -369,11 +379,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
_progressBar.setVisibility(View.VISIBLE);
|
||||
_progressBar.setPeriod(period);
|
||||
_progressBar.start();
|
||||
_refresher.start();
|
||||
onRefreshStart();
|
||||
} else {
|
||||
_progressBar.setVisibility(View.GONE);
|
||||
_progressBar.stop();
|
||||
_refresher.stop();
|
||||
onRefreshStop();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -384,10 +394,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
}
|
||||
}
|
||||
|
||||
public void setPrefGroupFilter(Set<UUID> groupFilter) {
|
||||
_prefGroupFilter = groupFilter;
|
||||
}
|
||||
|
||||
public void setCodeGroupSize(Preferences.CodeGrouping codeGrouping) {
|
||||
_adapter.setCodeGroupSize(codeGrouping);
|
||||
}
|
||||
|
@ -404,6 +410,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
_adapter.setShowIcon(showIcon);
|
||||
}
|
||||
|
||||
public void setShowNextCode(boolean showNextCode) {
|
||||
_adapter.setShowNextCode(showNextCode);
|
||||
}
|
||||
|
||||
public void setShowExpirationState(boolean showExpirationState) {
|
||||
_adapter.setShowExpirationState(showExpirationState);
|
||||
}
|
||||
|
||||
public void setHighlightEntry(boolean highlightEntry) {
|
||||
_adapter.setHighlightEntry(highlightEntry);
|
||||
}
|
||||
|
@ -424,61 +438,57 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
_adapter.setErrorCardInfo(info);
|
||||
}
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
addEntry(entry, false);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void addEntry(VaultEntry entry, boolean focusEntry) {
|
||||
int position = _adapter.addEntry(entry);
|
||||
updateEmptyState();
|
||||
public void onEntryAdded(VaultEntry entry) {
|
||||
int position = _adapter.getEntryPosition(entry);
|
||||
if (position < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
LinearLayoutManager layoutManager = (LinearLayoutManager) _recyclerView.getLayoutManager();
|
||||
if (focusEntry && position >= 0) {
|
||||
if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition())
|
||||
|| (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) {
|
||||
boolean smoothScroll = !AnimationsHelper.Scale.TRANSITION.isZero(requireContext());
|
||||
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
|
||||
private void handleScroll() {
|
||||
_recyclerView.removeOnScrollListener(this);
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
tempHighlightEntry(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
if (smoothScroll && newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
handleScroll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (!smoothScroll) {
|
||||
handleScroll();
|
||||
}
|
||||
}
|
||||
};
|
||||
_recyclerView.addOnScrollListener(scrollListener);
|
||||
_recyclerView.setOnTouchListener((v, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
_recyclerView.removeOnScrollListener(scrollListener);
|
||||
_recyclerView.stopScroll();
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
// We can't easily control the speed of the smooth scroll animation, but we
|
||||
// can at least disable it if animations are disabled
|
||||
if (smoothScroll) {
|
||||
_recyclerView.smoothScrollToPosition(position);
|
||||
} else {
|
||||
_recyclerView.scrollToPosition(position);
|
||||
if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition())
|
||||
|| (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) {
|
||||
boolean smoothScroll = !AnimationsHelper.Scale.TRANSITION.isZero(requireContext());
|
||||
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
|
||||
private void handleScroll() {
|
||||
_recyclerView.removeOnScrollListener(this);
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
tempHighlightEntry(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
if (smoothScroll && newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
handleScroll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (!smoothScroll) {
|
||||
handleScroll();
|
||||
}
|
||||
}
|
||||
};
|
||||
_recyclerView.addOnScrollListener(scrollListener);
|
||||
_recyclerView.setOnTouchListener((v, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
_recyclerView.removeOnScrollListener(scrollListener);
|
||||
_recyclerView.stopScroll();
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
// We can't easily control the speed of the smooth scroll animation, but we
|
||||
// can at least disable it if animations are disabled
|
||||
if (smoothScroll) {
|
||||
_recyclerView.smoothScrollToPosition(position);
|
||||
} else {
|
||||
tempHighlightEntry(entry);
|
||||
_recyclerView.scrollToPosition(position);
|
||||
}
|
||||
} else {
|
||||
tempHighlightEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -489,27 +499,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
_adapter.focusEntry(entry, secondsToFocus);
|
||||
}
|
||||
|
||||
public void addEntries(Collection<VaultEntry> entries) {
|
||||
_adapter.addEntries(entries);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void removeEntry(VaultEntry entry) {
|
||||
_adapter.removeEntry(entry);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void removeEntry(UUID uuid) {
|
||||
_adapter.removeEntry(uuid);
|
||||
public void setEntries(Collection<VaultEntry> entries) {
|
||||
_adapter.setEntries(new ArrayList<>(entries));
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void clearEntries() {
|
||||
_adapter.clearEntries();
|
||||
}
|
||||
|
||||
public void replaceEntry(UUID uuid, VaultEntry newEntry) {
|
||||
_adapter.replaceEntry(uuid, newEntry);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
public void runEntriesAnimation() {
|
||||
|
@ -519,116 +516,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
_recyclerView.scheduleLayoutAnimation();
|
||||
}
|
||||
|
||||
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) {
|
||||
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
|
||||
chip.setText(group.getName());
|
||||
chip.setCheckable(true);
|
||||
chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID()));
|
||||
chip.setCheckedIconVisible(true);
|
||||
chip.setOnCheckedChangeListener((group1, checkedId) -> {
|
||||
Set<UUID> groupFilter = getGroupFilter(chipGroup);
|
||||
setGroupFilter(groupFilter, true);
|
||||
});
|
||||
chip.setTag(group);
|
||||
chipGroup.addView(chip);
|
||||
}
|
||||
|
||||
private void initializeGroupChip() {
|
||||
View view = getLayoutInflater().inflate(R.layout.dialog_select_groups, null);
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(requireContext());
|
||||
NestedScrollView scrollView = view.findViewById(R.id.scrollView);
|
||||
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) scrollView.getLayoutParams();
|
||||
layoutParams.matchConstraintMaxHeight = getResources().getConfiguration().screenHeightDp;
|
||||
|
||||
dialog.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
dialog.getBehavior().setSkipCollapsed(false);
|
||||
dialog.setContentView(view);
|
||||
|
||||
ChipGroup chipGroup = view.findViewById(R.id.groupChipGroup);
|
||||
Button clearButton = view.findViewById(R.id.btnClear);
|
||||
Button saveButton = view.findViewById(R.id.btnSave);
|
||||
clearButton.setOnClickListener(v -> {
|
||||
chipGroup.clearCheck();
|
||||
Set<UUID> groupFilter = Collections.emptySet();
|
||||
if (_listener != null) {
|
||||
_listener.onSaveGroupFilter(groupFilter);
|
||||
}
|
||||
setGroupFilter(groupFilter, true);
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
saveButton.setOnClickListener(v -> {
|
||||
Set<UUID> groupFilter = getGroupFilter(chipGroup);
|
||||
if (_listener != null) {
|
||||
_listener.onSaveGroupFilter(groupFilter);
|
||||
}
|
||||
setGroupFilter(groupFilter, true);
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
_groupChip.setOnClickListener(v -> {
|
||||
chipGroup.removeAllViews();
|
||||
|
||||
for (VaultGroup group : _groups) {
|
||||
addChipTo(chipGroup, new VaultGroupModel(group));
|
||||
}
|
||||
addChipTo(chipGroup, new VaultGroupModel(getString(R.string.no_group)));
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
private static Set<UUID> getGroupFilter(ChipGroup chipGroup) {
|
||||
return chipGroup.getCheckedChipIds().stream()
|
||||
.map(i -> {
|
||||
Chip chip = chipGroup.findViewById(i);
|
||||
VaultGroupModel group = (VaultGroupModel) chip.getTag();
|
||||
return group.getUUID();
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private void updateGroupChip() {
|
||||
if (_groupFilter.isEmpty()) {
|
||||
_groupChip.setText(R.string.groups);
|
||||
} else {
|
||||
_groupChip.setText(String.format("%s (%d)", getString(R.string.groups), _groupFilter.size()));
|
||||
}
|
||||
}
|
||||
|
||||
private void setShowProgress(boolean showProgress) {
|
||||
_showProgress = showProgress;
|
||||
updateDividerDecoration();
|
||||
}
|
||||
|
||||
public void setGroups(Collection<VaultGroup> groups) {
|
||||
_groups = groups;
|
||||
_adapter.setGroups(groups);
|
||||
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
updateDividerDecoration();
|
||||
|
||||
if (_prefGroupFilter != null) {
|
||||
Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter);
|
||||
_prefGroupFilter = null;
|
||||
if (!groupFilter.isEmpty()) {
|
||||
setGroupFilter(groupFilter, false);
|
||||
}
|
||||
} else if (_groupFilter != null) {
|
||||
Set<UUID> groupFilter = cleanGroupFilter(_groupFilter);
|
||||
if (!_groupFilter.equals(groupFilter)) {
|
||||
setGroupFilter(groupFilter, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) {
|
||||
Set<UUID> groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet());
|
||||
|
||||
return groupFilter.stream()
|
||||
.filter(g -> g == null || groupUuids.contains(g))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private void updateDividerDecoration() {
|
||||
if (_itemDecoration != null) {
|
||||
_recyclerView.removeItemDecoration(_itemDecoration);
|
||||
|
@ -705,15 +597,15 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
}
|
||||
|
||||
int entryIndex = _adapter.translateEntryPosToIndex(adapterPosition);
|
||||
// The first entry should have a top margin, but only if the group chip is not shown and the error card is not shown
|
||||
if (entryIndex == 0 && (_groups == null || _groups.isEmpty()) && !_adapter.isErrorCardShown()) {
|
||||
// The first entry should have a top margin, but only if the error card is not shown
|
||||
if (entryIndex == 0 && !_adapter.isErrorCardShown()) {
|
||||
outRect.top = _offset;
|
||||
}
|
||||
|
||||
// Only non-favorite entries have a bottom margin, except for the final favorite entry
|
||||
int totalFavorites = _adapter.getShownFavoritesCount();
|
||||
if (totalFavorites == 0
|
||||
|| (entryIndex < _adapter.getShownEntriesCount() && !_adapter.getEntryAtPos(adapterPosition).isFavorite())
|
||||
|| (entryIndex < _adapter.getShownEntriesCount() && !_adapter.getEntryAtPosition(adapterPosition).isFavorite())
|
||||
|| totalFavorites == entryIndex + 1) {
|
||||
outRect.bottom = _offset;
|
||||
}
|
||||
|
@ -806,7 +698,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
VaultEntry entry = _adapter.getEntryAtPos(position);
|
||||
VaultEntry entry = _adapter.getEntryAtPosition(position);
|
||||
if (!entry.hasIcon()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
|
|
@ -7,12 +7,14 @@ import android.view.ViewGroup;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
|
||||
import com.beemdevelopment.aegis.util.CollectionUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> implements ItemTouchHelperAdapter {
|
||||
private GroupAdapter.Listener _listener;
|
||||
private ArrayList<VaultGroup> _groups;
|
||||
|
||||
|
@ -32,6 +34,10 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
|||
}
|
||||
}
|
||||
|
||||
public ArrayList<VaultGroup> getGroups() {
|
||||
return _groups;
|
||||
}
|
||||
|
||||
public void replaceGroup(UUID uuid, VaultGroup newGroup) {
|
||||
VaultGroup oldGroup = getGroupByUUID(uuid);
|
||||
int position = _groups.indexOf(oldGroup);
|
||||
|
@ -64,6 +70,18 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMove(int firstPosition, int secondPosition) {
|
||||
CollectionUtils.move(_groups, firstPosition, secondPosition);
|
||||
notifyItemMoved(firstPosition, secondPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDismiss(int position) { }
|
||||
|
||||
@Override
|
||||
public void onItemDrop(int position) { }
|
||||
|
||||
private VaultGroup getGroupByUUID(UUID uuid) {
|
||||
for (VaultGroup group : _groups) {
|
||||
if (group.getUUID().equals(uuid)) {
|
||||
|
|
|
@ -35,8 +35,10 @@ public class IconPackAdapter extends RecyclerView.Adapter<IconPackHolder> {
|
|||
|
||||
public void removeIconPack(IconPack pack) {
|
||||
int position = _iconPacks.indexOf(pack);
|
||||
_iconPacks.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
if (position >= 0) {
|
||||
_iconPacks.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
|
@ -2,14 +2,11 @@ package com.beemdevelopment.aegis.ui.views;
|
|||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
|
||||
|
@ -30,7 +27,6 @@ public class TotpProgressBar extends ProgressBar {
|
|||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public TotpProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ public class Vault {
|
|||
private static final int VERSION = 3;
|
||||
private final UUIDMap<VaultEntry> _entries = new UUIDMap<>();
|
||||
private final UUIDMap<VaultGroup> _groups = new UUIDMap<>();
|
||||
private boolean _iconsOptimized = true;
|
||||
|
||||
// Whether we've migrated the group list to the new format while parsing the vault
|
||||
private boolean _isGroupsMigrationFresh = false;
|
||||
|
@ -42,6 +43,7 @@ public class Vault {
|
|||
obj.put("version", VERSION);
|
||||
obj.put("entries", entriesArray);
|
||||
obj.put("groups", groupsArray);
|
||||
obj.put("icons_optimized", _iconsOptimized);
|
||||
|
||||
return obj;
|
||||
} catch (JSONException e) {
|
||||
|
@ -86,6 +88,10 @@ public class Vault {
|
|||
|
||||
entries.add(entry);
|
||||
}
|
||||
|
||||
if (!obj.optBoolean("icons_optimized")) {
|
||||
vault.setIconsOptimized(false);
|
||||
}
|
||||
} catch (VaultEntryException | JSONException e) {
|
||||
throw new VaultException(e);
|
||||
}
|
||||
|
@ -101,6 +107,14 @@ public class Vault {
|
|||
return _isGroupsMigrationFresh;
|
||||
}
|
||||
|
||||
public void setIconsOptimized(boolean optimized) {
|
||||
_iconsOptimized = optimized;
|
||||
}
|
||||
|
||||
public boolean areIconsOptimized() {
|
||||
return _iconsOptimized;
|
||||
}
|
||||
|
||||
public boolean migrateOldGroup(VaultEntry entry) {
|
||||
if (entry.getOldGroup() != null) {
|
||||
Optional<VaultGroup> optGroup = getGroups().getValues()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.UriPermission;
|
||||
import android.net.Uri;
|
||||
|
@ -9,6 +10,7 @@ import android.util.Log;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.database.AuditLogRepository;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
@ -38,6 +40,7 @@ public class VaultBackupManager {
|
|||
new StrictDateFormat("yyyyMMdd-HHmmss", Locale.ENGLISH);
|
||||
|
||||
public static final String FILENAME_PREFIX = "aegis-backup";
|
||||
public static final String FILENAME_SINGLE = String.format("%s.json", FILENAME_PREFIX);
|
||||
|
||||
private final Context _context;
|
||||
private final Preferences _prefs;
|
||||
|
@ -51,10 +54,10 @@ public class VaultBackupManager {
|
|||
_auditLogRepository = auditLogRepository;
|
||||
}
|
||||
|
||||
public void scheduleBackup(File tempFile, Uri dirUri, int versionsToKeep) {
|
||||
public void scheduleBackup(File tempFile, BackupsVersioningStrategy strategy, Uri uri, int versionsToKeep) {
|
||||
_executor.execute(() -> {
|
||||
try {
|
||||
createBackup(tempFile, dirUri, versionsToKeep);
|
||||
createBackup(tempFile, strategy, uri, versionsToKeep);
|
||||
_auditLogRepository.addBackupCreatedEvent();
|
||||
_prefs.setBuiltInBackupResult(new Preferences.BackupResult(null));
|
||||
} catch (VaultRepositoryException | VaultBackupPermissionException e) {
|
||||
|
@ -64,6 +67,46 @@ public class VaultBackupManager {
|
|||
});
|
||||
}
|
||||
|
||||
private void createBackup(File tempFile, BackupsVersioningStrategy strategy, Uri uri, int versionsToKeep)
|
||||
throws VaultRepositoryException, VaultBackupPermissionException {
|
||||
if (uri == null) {
|
||||
throw new VaultRepositoryException("getBackupsLocation returned null");
|
||||
}
|
||||
if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
|
||||
createBackup(tempFile, uri);
|
||||
} else if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
|
||||
createBackup(tempFile, uri, versionsToKeep);
|
||||
} else {
|
||||
throw new VaultRepositoryException("Invalid backups versioning strategy");
|
||||
}
|
||||
}
|
||||
|
||||
private void createBackup(File tempFile, Uri fileUri)
|
||||
throws VaultRepositoryException, VaultBackupPermissionException {
|
||||
Log.i(TAG, String.format("Creating backup at %s", fileUri));
|
||||
try {
|
||||
if (!hasPermissionsAt(fileUri)) {
|
||||
throw new VaultBackupPermissionException("No persisted URI permissions");
|
||||
}
|
||||
ContentResolver resolver = _context.getContentResolver();
|
||||
try (FileInputStream inStream = new FileInputStream(tempFile);
|
||||
OutputStream outStream = resolver.openOutputStream(fileUri, "wt")
|
||||
) {
|
||||
if (outStream == null) {
|
||||
throw new IOException("openOutputStream returned null");
|
||||
}
|
||||
IOUtils.copy(inStream, outStream);
|
||||
} catch (IOException exception) {
|
||||
throw new VaultRepositoryException(exception);
|
||||
}
|
||||
} catch (VaultRepositoryException | VaultBackupPermissionException exception) {
|
||||
Log.e(TAG, String.format("Unable to create backup: %s", exception));
|
||||
throw exception;
|
||||
} finally {
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private void createBackup(File tempFile, Uri dirUri, int versionsToKeep)
|
||||
throws VaultRepositoryException, VaultBackupPermissionException {
|
||||
FileInfo fileInfo = new FileInfo(FILENAME_PREFIX);
|
||||
|
|
|
@ -103,8 +103,14 @@ public class VaultEntry extends UUIDMap.Value {
|
|||
entry.setOldGroup(JsonUtils.optString(obj, "group"));
|
||||
}
|
||||
|
||||
VaultEntryIcon icon = VaultEntryIcon.fromJson(obj);
|
||||
entry.setIcon(icon);
|
||||
// Silently ignore any errors that occur when trying to parse the icon of an
|
||||
// entry. This allows us to introduce new icon types in the future (e.g. WebP)
|
||||
// without breaking compatibility with older versions of Aegis.
|
||||
try {
|
||||
VaultEntryIcon icon = VaultEntryIcon.fromJson(obj);
|
||||
entry.setIcon(icon);
|
||||
} catch (VaultEntryIconException ignored) {
|
||||
}
|
||||
|
||||
return entry;
|
||||
} catch (OtpInfoException | JSONException e) {
|
||||
|
@ -233,6 +239,10 @@ public class VaultEntry extends UUIDMap.Value {
|
|||
&& getGroups().equals(entry.getGroups());
|
||||
}
|
||||
|
||||
public boolean hasSameNameAndIssuer(VaultEntry entry) {
|
||||
return getName().equals(entry.getName()) && getIssuer().equals(entry.getIssuer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this entry has its values set to the defaults.
|
||||
*/
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
|
@ -7,8 +10,6 @@ import com.beemdevelopment.aegis.icons.IconType;
|
|||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
@ -23,21 +24,23 @@ public class VaultEntryIcon implements Serializable {
|
|||
private final byte[] _hash;
|
||||
private final IconType _type;
|
||||
|
||||
public VaultEntryIcon(byte @NonNull [] bytes, @NonNull IconType type) {
|
||||
public static final int MAX_DIMENS = 512;
|
||||
|
||||
public VaultEntryIcon(@NonNull byte[] bytes, @NonNull IconType type) {
|
||||
this(bytes, type, generateHash(bytes, type));
|
||||
}
|
||||
|
||||
VaultEntryIcon(byte @NonNull [] bytes, @NonNull IconType type, byte @NonNull [] hash) {
|
||||
VaultEntryIcon(@NonNull byte[] bytes, @NonNull IconType type, @NonNull byte[] hash) {
|
||||
_bytes = bytes;
|
||||
_hash = hash;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
public byte @NonNull [] getBytes() {
|
||||
public @NonNull byte[] getBytes() {
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
public byte @NonNull [] getHash() {
|
||||
public @NonNull byte[] getHash() {
|
||||
return _hash;
|
||||
}
|
||||
|
||||
|
@ -70,7 +73,7 @@ public class VaultEntryIcon implements Serializable {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
static VaultEntryIcon fromJson(@NonNull JSONObject obj) throws VaultEntryException {
|
||||
static VaultEntryIcon fromJson(@NonNull JSONObject obj) throws VaultEntryIconException {
|
||||
try {
|
||||
Object icon = obj.get("icon");
|
||||
if (icon == JSONObject.NULL) {
|
||||
|
@ -80,7 +83,7 @@ public class VaultEntryIcon implements Serializable {
|
|||
String mime = JsonUtils.optString(obj, "icon_mime");
|
||||
IconType iconType = mime == null ? IconType.JPEG : IconType.fromMimeType(mime);
|
||||
if (iconType == IconType.INVALID) {
|
||||
throw new VaultEntryException(String.format("Bad icon MIME type: %s", mime));
|
||||
throw new VaultEntryIconException(String.format("Bad icon MIME type: %s", mime));
|
||||
}
|
||||
|
||||
byte[] iconBytes = Base64.decode((String) icon);
|
||||
|
@ -92,11 +95,11 @@ public class VaultEntryIcon implements Serializable {
|
|||
|
||||
return new VaultEntryIcon(iconBytes, iconType);
|
||||
} catch (JSONException | EncodingException e) {
|
||||
throw new VaultEntryException(e);
|
||||
throw new VaultEntryIconException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte @NonNull [] generateHash(byte @NonNull [] bytes, @NonNull IconType type) {
|
||||
private static @NonNull byte[] generateHash(@NonNull byte[] bytes, @NonNull IconType type) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
md.update(type.toMimeType().getBytes(StandardCharsets.UTF_8));
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
public class VaultEntryIconException extends Exception {
|
||||
public VaultEntryIconException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public VaultEntryIconException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -5,12 +5,14 @@ import android.app.backup.BackupManager;
|
|||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
|
@ -172,8 +174,11 @@ public class VaultManager {
|
|||
try (OutputStream outStream = new FileOutputStream(tempFile)) {
|
||||
_repo.export(outStream);
|
||||
}
|
||||
BackupsVersioningStrategy strategy = _prefs.getBackupVersioningStrategy();
|
||||
Uri uri = _prefs.getBackupsLocation();
|
||||
int versionsToKeep = _prefs.getBackupsVersionCount();
|
||||
|
||||
_backups.scheduleBackup(tempFile, _prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
|
||||
_backups.scheduleBackup(tempFile, strategy, uri, versionsToKeep);
|
||||
} catch (IOException e) {
|
||||
throw new VaultRepositoryException(e);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
|||
import androidx.core.util.AtomicFile;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.google.zxing.WriterException;
|
||||
|
||||
|
@ -249,6 +250,13 @@ public class VaultRepository {
|
|||
return _vault.getEntries().replace(entry);
|
||||
}
|
||||
|
||||
public VaultEntry editEntry(VaultEntry entry, EntryEditor editor) {
|
||||
VaultEntry newEntry = Cloner.clone(entry);
|
||||
editor.edit(newEntry);
|
||||
replaceEntry(newEntry);
|
||||
return newEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves entry1 to the position of entry2.
|
||||
*/
|
||||
|
@ -291,8 +299,11 @@ public class VaultRepository {
|
|||
removeGroup(group);
|
||||
}
|
||||
|
||||
public void renameGroup(VaultGroup renamedGroup) {
|
||||
_vault.getGroups().replace(renamedGroup);
|
||||
public void replaceGroups(Collection<VaultGroup> groups) {
|
||||
_vault.getGroups().wipe();
|
||||
for (VaultGroup group : groups) {
|
||||
_vault.getGroups().add(group);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeGroup(VaultGroup group) {
|
||||
|
@ -322,6 +333,14 @@ public class VaultRepository {
|
|||
return _vault.isGroupsMigrationFresh();
|
||||
}
|
||||
|
||||
public boolean areIconsOptimized() {
|
||||
return _vault.areIconsOptimized();
|
||||
}
|
||||
|
||||
public void setIconsOptimized(boolean optimized) {
|
||||
_vault.setIconsOptimized(optimized);
|
||||
}
|
||||
|
||||
public VaultFileCredentials getCredentials() {
|
||||
return _creds == null ? null : _creds.clone();
|
||||
}
|
||||
|
@ -341,4 +360,8 @@ public class VaultRepository {
|
|||
|
||||
return getCredentials().getSlots().findBackupPasswordSlots().size() > 0;
|
||||
}
|
||||
|
||||
public interface EntryEditor {
|
||||
void edit(VaultEntry entry);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,6 @@ message MigrationPayload {
|
|||
repeated OtpParameters otp_parameters = 1;
|
||||
int32 version = 2;
|
||||
int32 batch_size = 3;
|
||||
int32 batch_index = 4;
|
||||
optional int32 batch_index = 4;
|
||||
int32 batch_id = 5;
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorSecondary" android:state_enabled="true"/>
|
||||
<item android:alpha="0.38" android:color="?attr/colorSecondary"/>
|
||||
</selector>
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2017 The Android Open Source Project
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:alpha="@dimen/mtrl_low_ripple_pressed_alpha" android:color="?attr/colorSecondary" android:state_pressed="true"/>
|
||||
<item android:alpha="@dimen/mtrl_low_ripple_focused_alpha" android:color="?attr/colorSecondary" android:state_focused="true" android:state_hovered="true"/>
|
||||
<item android:alpha="@dimen/mtrl_low_ripple_focused_alpha" android:color="?attr/colorSecondary" android:state_focused="true"/>
|
||||
<item android:alpha="@dimen/mtrl_low_ripple_hovered_alpha" android:color="?attr/colorSecondary" android:state_hovered="true"/>
|
||||
<item android:alpha="@dimen/mtrl_low_ripple_default_alpha" android:color="?attr/colorSecondary"/>
|
||||
|
||||
</selector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M400,720L400,640L560,640L560,720L400,720ZM240,520L240,440L720,440L720,520L240,520ZM120,320L120,240L840,240L840,320L120,320Z" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_tag_24.xml
Normal file
9
app/src/main/res/drawable/ic_tag_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="m240,800 l40,-160L120,640l20,-80h160l40,-160L180,400l20,-80h160l40,-160h80l-40,160h160l40,-160h80l-40,160h160l-20,80L660,400l-40,160h160l-20,80L600,640l-40,160h-80l40,-160L360,640l-40,160h-80ZM380,560h160l40,-160L420,400l-40,160Z"
|
||||
android:fillColor="#e3e3e3"/>
|
||||
</vector>
|
|
@ -1,9 +1,14 @@
|
|||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<scale android:scaleWidth="100%">
|
||||
<shape>
|
||||
<solid android:color="?attr/colorPrimaryAlternative"/>
|
||||
<solid android:color="?attr/colorProgressbar"/>
|
||||
<corners
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="2dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="2dp" />
|
||||
</shape>
|
||||
</clip>
|
||||
</scale>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?attr/colorSurfaceContainerLow"/>
|
||||
<corners android:radius="4dp"/>
|
||||
<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp"/>
|
||||
</shape>
|
|
@ -7,6 +7,7 @@
|
|||
android:fitsSystemWindows="true"
|
||||
tools:context="com.beemdevelopment.aegis.ui.AboutActivity">
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
@ -16,6 +17,7 @@
|
|||
android:layout_height="?attr/actionBarSize" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
<ScrollView
|
||||
android:id="@+id/about_scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
tools:context="com.beemdevelopment.aegis.ui.AssignIconsActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="com.beemdevelopment.aegis.ui.AuthActivity">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
android:fitsSystemWindows="true"
|
||||
tools:context="com.beemdevelopment.aegis.ui.EditEntryActivity">
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
@ -98,7 +99,7 @@
|
|||
android:id="@+id/text_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"/>
|
||||
android:inputType="textCapSentences"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
|
@ -117,7 +118,7 @@
|
|||
android:maxLines="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"/>
|
||||
android:inputType="textCapSentences"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -171,7 +172,7 @@
|
|||
android:id="@+id/text_note"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text|textMultiLine"/>
|
||||
android:inputType="text|textMultiLine|textCapSentences"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
|
@ -201,7 +202,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/secret"
|
||||
android:inputType="textPassword"/>
|
||||
android:inputType="textPassword|textMultiLine"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
tools:context="com.beemdevelopment.aegis.ui.GroupManagerActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
tools:context="com.beemdevelopment.aegis.ui.ImportEntriesActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
@ -22,7 +23,6 @@
|
|||
android:id="@+id/list_entries"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="60dp"
|
||||
android:clipToPadding="false"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -15,30 +14,48 @@
|
|||
android:fitsSystemWindows="true"
|
||||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@+id/rvKeyProfiles">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/groupChipGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:selectionRequired="true"/>
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/groupChipGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<fragment
|
||||
android:name="com.beemdevelopment.aegis.ui.views.EntryListView"
|
||||
android:id="@+id/key_profiles"
|
||||
android:layout_height="fill_parent"
|
||||
android:name="com.beemdevelopment.aegis.ui.views.EntryListView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="fill_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -48,6 +65,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
android:src="@drawable/ic_outline_add_24" />
|
||||
android:src="@drawable/ic_outline_add_24" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="true">
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
tools:context="com.beemdevelopment.aegis.ui.ScannerActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_layout"
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
tools:context="com.beemdevelopment.aegis.ui.TransferEntriesActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_layout"
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
@ -22,62 +22,56 @@
|
|||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layoutShareEntry"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivQrCode"
|
||||
android:layout_width="250dp"
|
||||
android:layout_height="250dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTransfer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/transfer_entry"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="5dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/tvDescription"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="?attr/actionBarSize"
|
||||
android:paddingHorizontal="30dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDescription"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="10dp"
|
||||
android:text="@string/transfer_entry_description"
|
||||
app:layout_constraintBottom_toTopOf="@+id/ivQrCode"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivQrCode"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.3"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Aegis.ImageView.Rounded" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvIssuer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
tools:text="Issuer"
|
||||
android:layout_marginTop="20dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ivQrCode" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/ivQrCode"
|
||||
tools:text="Issuer" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAccountName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
tools:text="Accountname"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvIssuer" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvIssuer"
|
||||
tools:text="Accountname" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCopyClipboard"
|
||||
|
@ -86,18 +80,19 @@
|
|||
android:text="@string/copy_uri"
|
||||
android:textAllCaps="false"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toTopOf="@id/tvEntriesCount"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.497"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvAccountName"
|
||||
app:layout_constraintBottom_toTopOf="@id/tvEntriesCount" />
|
||||
app:layout_constraintVertical_bias="0.134" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnNext"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:text="@string/next"
|
||||
android:textAllCaps="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -105,11 +100,11 @@
|
|||
|
||||
<Button
|
||||
android:id="@+id/btnPrevious"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:text="@string/previous"
|
||||
android:textAllCaps="false"
|
||||
android:visibility="invisible"
|
||||
|
@ -123,9 +118,18 @@
|
|||
android:layout_marginBottom="20dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/btnNext"
|
||||
app:layout_constraintHorizontal_bias="0.506"
|
||||
app:layout_constraintStart_toStartOf="@+id/btnPrevious" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/transfer_entry_brightness"
|
||||
app:layout_constraintBottom_toTopOf="@+id/tvEntriesCount"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btnCopyClipboard" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/description"
|
||||
android:layout_alignStart="@+id/profile_code">
|
||||
android:layout_alignStart="@+id/profile_codes_layout">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -109,23 +109,47 @@
|
|||
android:textSize="16sp"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="012 345"
|
||||
android:id="@+id/profile_code"
|
||||
android:layoutDirection="ltr"
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:id="@+id/profile_codes_layout"
|
||||
android:layout_below="@id/description"
|
||||
android:includeFontPadding="false"
|
||||
android:fallbackLineSpacing="false"
|
||||
android:textSize="34sp"
|
||||
android:textColor="?attr/colorCode"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="0dp"
|
||||
android:textStyle="normal|bold"/>
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="012 345"
|
||||
android:id="@+id/profile_code"
|
||||
android:layoutDirection="ltr"
|
||||
android:textSize="34sp"
|
||||
android:layout_below="@id/description"
|
||||
android:textColor="?attr/colorCode"
|
||||
android:includeFontPadding="false"
|
||||
android:fallbackLineSpacing="false"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="0dp"
|
||||
android:textStyle="normal|bold"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/next_profile_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="0dp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingStart="2dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:textColor="?attr/colorOnSurfaceDim"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="normal|bold"
|
||||
android:includeFontPadding="false"
|
||||
android:fallbackLineSpacing="false"
|
||||
tools:text="412 643"/>
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
|
|
@ -85,8 +85,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/description"
|
||||
android:layout_alignStart="@+id/profile_code">
|
||||
|
||||
android:layout_alignStart="@+id/profile_codes_layout">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -95,6 +94,7 @@
|
|||
android:text="@string/issuer"
|
||||
android:textStyle="bold"
|
||||
android:includeFontPadding="false"
|
||||
android:fallbackLineSpacing="false"
|
||||
android:textSize="13sp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"/>
|
||||
|
@ -112,23 +112,50 @@
|
|||
</RelativeLayout>
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="012 345"
|
||||
android:id="@+id/profile_code"
|
||||
android:layoutDirection="ltr"
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:id="@+id/profile_codes_layout"
|
||||
android:layout_below="@id/description"
|
||||
android:includeFontPadding="false"
|
||||
android:fallbackLineSpacing="false"
|
||||
android:textSize="26sp"
|
||||
android:textColor="?attr/colorCode"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="0dp"
|
||||
android:textStyle="normal|bold"/>
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="012 345"
|
||||
android:id="@+id/profile_code"
|
||||
android:layoutDirection="ltr"
|
||||
android:layout_below="@id/description"
|
||||
android:includeFontPadding="false"
|
||||
android:fallbackLineSpacing="false"
|
||||
android:textSize="26sp"
|
||||
android:textColor="?attr/colorCode"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="0dp"
|
||||
android:textStyle="normal|bold"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/next_profile_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:letterSpacing="-0.01"
|
||||
android:layout_marginTop="0dp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingStart="2dp"
|
||||
android:textColor="?attr/colorOnSurfaceDim"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="normal|bold"
|
||||
android:fallbackLineSpacing="false"
|
||||
android:includeFontPadding="false"
|
||||
tools:text="412 643"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/description"
|
||||
android:layout_alignStart="@+id/profile_code">
|
||||
android:layout_alignStart="@+id/profile_codes_layout">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -110,23 +110,48 @@
|
|||
tools:text=" - AccountName" />
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="012 345"
|
||||
android:id="@+id/profile_code"
|
||||
android:layoutDirection="ltr"
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:id="@+id/profile_codes_layout"
|
||||
android:layout_below="@id/description"
|
||||
android:includeFontPadding="false"
|
||||
android:fallbackLineSpacing="false"
|
||||
android:textSize="26sp"
|
||||
android:textColor="?attr/colorCode"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="0dp"
|
||||
android:textStyle="normal|bold"/>
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="012 345"
|
||||
android:id="@+id/profile_code"
|
||||
android:layoutDirection="ltr"
|
||||
android:layout_below="@id/description"
|
||||
android:includeFontPadding="false"
|
||||
android:fallbackLineSpacing="false"
|
||||
android:textSize="26sp"
|
||||
android:textColor="?attr/colorCode"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="0dp"
|
||||
android:textStyle="normal|bold"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/next_profile_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:letterSpacing="-0.01"
|
||||
android:layout_marginTop="0dp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingStart="2dp"
|
||||
android:textColor="?attr/colorOnSurfaceDim"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="normal|bold"
|
||||
android:fallbackLineSpacing="false"
|
||||
android:includeFontPadding="false"
|
||||
tools:text="412 643"/>
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
|
|
@ -104,22 +104,47 @@
|
|||
tools:text=" - AccountName" />
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="012 345"
|
||||
android:id="@+id/profile_code"
|
||||
android:layoutDirection="ltr"
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:id="@+id/profile_codes_layout"
|
||||
android:layout_below="@id/description"
|
||||
android:includeFontPadding="false"
|
||||
android:fallbackLineSpacing="false"
|
||||
android:textSize="26sp"
|
||||
android:textColor="?attr/colorCode"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="10dp"
|
||||
android:textStyle="normal|bold"/>
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="012 345"
|
||||
android:id="@+id/profile_code"
|
||||
android:layoutDirection="ltr"
|
||||
android:layout_below="@id/description"
|
||||
android:includeFontPadding="false"
|
||||
android:fallbackLineSpacing="false"
|
||||
android:textSize="26sp"
|
||||
android:textColor="?attr/colorCode"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="10dp"
|
||||
android:textStyle="normal|bold"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/next_profile_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:letterSpacing="-0.01"
|
||||
android:layout_marginTop="0dp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingStart="2dp"
|
||||
android:textColor="?attr/colorOnSurfaceDim"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="normal|bold"
|
||||
android:includeFontPadding="false"
|
||||
android:fallbackLineSpacing="false"
|
||||
tools:text="412 643"/>
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/radio_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/keep_x_versions_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pref_backups_versioning_strategy_keep_x_versions" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/single_backup_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pref_backups_versioning_strategy_single_backup" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/warning_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="@string/pref_backups_versioning_strategy_single_backup_warning"
|
||||
android:textColor="?attr/colorError" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/risk_accept"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/understand_risk_accept" />
|
||||
|
||||
</LinearLayout>
|
151
app/src/main/res/layout/dialog_duplicate_entry.xml
Normal file
151
app/src/main/res/layout/dialog_duplicate_entry.xml
Normal file
|
@ -0,0 +1,151 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="16dp"
|
||||
android:text="@string/dialog_duplicate_entry_title"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/duplicate_warning_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dialog_duplicate_entry_message"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="20dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/overwrite_entry"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="15dp"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/ic_outline_brush_24"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dialog_duplicate_entry_overwrite_title"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dialog_duplicate_entry_overwrite_subtitle"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/create_new_entry"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="15dp"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/ic_tag_24"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dialog_duplicate_entry_suffix_title"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/duplicate_suffix_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dialog_duplicate_entry_suffix_subtitle"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/cancel_save"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="15dp"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/ic_outline_close_24"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dialog_duplicate_entry_cancel_title"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dialog_duplicate_entry_cancel_subtitle"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
52
app/src/main/res/layout/dialog_select_group.xml
Normal file
52
app/src/main/res/layout/dialog_select_group.xml
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingTop="10dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="25dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:text="@string/assign_groups_dialog_summary" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/group_selection_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="25dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:hint="@string/assign_groups_dialog_dropdown"
|
||||
style="?attr/dropdownStyle">
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/group_selection_dropdown"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/text_group_name_layout"
|
||||
android:hint="@string/group_name_hint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="25dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:visibility="gone">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/text_group_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textCapSentences"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue