Compare commits
218
Commits
v1.2-beta4
...
v1.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c0068bea7 | ||
|
|
6dcde38667 | ||
|
|
ec1913c77a | ||
|
|
3e40cc9c8f | ||
|
|
40d1e22079 | ||
|
|
38bbb60414 | ||
|
|
98d39ee399 | ||
|
|
737b9da292 | ||
|
|
dea13f56f5 | ||
|
|
1f0c51b45b | ||
|
|
466e296988 | ||
|
|
fb17f48013 | ||
|
|
f203eab3ef | ||
|
|
1bd9cf3a3f | ||
|
|
a795015487 | ||
|
|
0a6c89cd9d | ||
|
|
951241e95d | ||
|
|
ddbe9ccfb7 | ||
|
|
4482a21a54 | ||
|
|
15e28abecd | ||
|
|
1748e2221f | ||
|
|
b1b4c021cf | ||
|
|
a27e3c6364 | ||
|
|
8f7b152af4 | ||
|
|
39f18b79cb | ||
|
|
9a81cccbe2 | ||
|
|
2c6be39f88 | ||
|
|
a6299ffb5f | ||
|
|
6e5a80a000 | ||
|
|
f012c7ce22 | ||
|
|
805a9001e1 | ||
|
|
884a1d07c4 | ||
|
|
e9f6371885 | ||
|
|
1f2e45fd12 | ||
|
|
6a5323b12b | ||
|
|
735c086726 | ||
|
|
d94be2420c | ||
|
|
0b6da5a93c | ||
|
|
d24e1471a7 | ||
|
|
7ca072a29c | ||
|
|
d5ac447332 | ||
|
|
0067c79dfa | ||
|
|
4e198d2556 | ||
|
|
f15a0018ef | ||
|
|
d121139676 | ||
|
|
98b246ad20 | ||
|
|
bce0d2c044 | ||
|
|
bf91917745 | ||
|
|
a91339de73 | ||
|
|
5d9de7ac32 | ||
|
|
5f2529ea33 | ||
|
|
61364b5542 | ||
|
|
69196ddbcc | ||
|
|
b21b6dc750 | ||
|
|
e14a61dbf6 | ||
|
|
e8f94968c9 | ||
|
|
8b7e5d265b | ||
|
|
22a70ac626 | ||
|
|
dd83b74251 | ||
|
|
5a9da45a8e | ||
|
|
cac13b082e | ||
|
|
86c3c401ae | ||
|
|
33ff7da7af | ||
|
|
0af48f8bfa | ||
|
|
01885e1fc6 | ||
|
|
52d2713482 | ||
|
|
d875cb6baa | ||
|
|
0cdb882991 | ||
|
|
31d382d6ad | ||
|
|
141f491ca7 | ||
|
|
c0bbe50996 | ||
|
|
875c752f78 | ||
|
|
1d00a2d8d6 | ||
|
|
14407412bf | ||
|
|
bb2716f640 | ||
|
|
0f6b0dddbe | ||
|
|
d43e5e04c7 | ||
|
|
4c86e154c1 | ||
|
|
a45c834b9c | ||
|
|
b746ca0c6e | ||
|
|
7d38bc9b71 | ||
|
|
9557b60c93 | ||
|
|
227b5fd87c | ||
|
|
7d063104dd | ||
|
|
c03d00695d | ||
|
|
171c774b21 | ||
|
|
97944aac88 | ||
|
|
ca21e0bdd1 | ||
|
|
83c1914ddc | ||
|
|
d660fbc6d1 | ||
|
|
35b5ce9d17 | ||
|
|
762fb9bccd | ||
|
|
c3d38c18a3 | ||
|
|
7a3ad11902 | ||
|
|
28fcd68b72 | ||
|
|
c2ed4b4849 | ||
|
|
9bbc2886c2 | ||
|
|
5886462d2c | ||
|
|
98e8a0d97e | ||
|
|
0368a992de | ||
|
|
233eeb6aa1 | ||
|
|
e5d162be9b | ||
|
|
718d575a59 | ||
|
|
59e8c23a01 | ||
|
|
847009d733 | ||
|
|
ce3d674da8 | ||
|
|
9e4313e138 | ||
|
|
02e01723b2 | ||
|
|
874f5e9058 | ||
|
|
7518408554 | ||
|
|
6d68625ee2 | ||
|
|
0e78fd9652 | ||
|
|
9d44d6abb2 | ||
|
|
42c308bc8f | ||
|
|
20fb531f91 | ||
|
|
b8b3da4f85 | ||
|
|
7e403c2af6 | ||
|
|
be0529a0c8 | ||
|
|
9c5f136f5b | ||
|
|
8c34ce8f38 | ||
|
|
3cdf31f940 | ||
|
|
7059f3be74 | ||
|
|
0c0e8c0d66 | ||
|
|
06fd77b5f0 | ||
|
|
9121922ef2 | ||
|
|
2347a322b5 | ||
|
|
fcd3623eef | ||
|
|
121ba06f5a | ||
|
|
6611aa851c | ||
|
|
c0b8b2c7ee | ||
|
|
34888767ed | ||
|
|
61f2e6e020 | ||
|
|
ba3e6203ec | ||
|
|
906d1b453a | ||
|
|
a9ccf412e7 | ||
|
|
21b588dc8b | ||
|
|
90e5003418 | ||
|
|
f8cf87aae9 | ||
|
|
a77b308896 | ||
|
|
a25dbc5cdd | ||
|
|
2aa88a2921 | ||
|
|
7fedb5ab83 | ||
|
|
0eb9a25687 | ||
|
|
330e88c270 | ||
|
|
407d059076 | ||
|
|
de3d0d8515 | ||
|
|
57ef52d6ff | ||
|
|
189bd0d749 | ||
|
|
4ef8d9e826 | ||
|
|
7bb8d28b3b | ||
|
|
94ebc08097 | ||
|
|
a3e14c57e4 | ||
|
|
b55bdf8f04 | ||
|
|
3e0a97dbe0 | ||
|
|
9b13a56002 | ||
|
|
32c145b02d | ||
|
|
25b359a136 | ||
|
|
0eb338a0bf | ||
|
|
b59a29cff4 | ||
|
|
c252e77c5b | ||
|
|
44ca712186 | ||
|
|
93cc945a2c | ||
|
|
68d6c39dd4 | ||
|
|
3a5a185940 | ||
|
|
3d351ece05 | ||
|
|
c65ed16790 | ||
|
|
7c147befcb | ||
|
|
c73c29d2ec | ||
|
|
df0c6b5fa8 | ||
|
|
2ae7ec33f1 | ||
|
|
65beeffa93 | ||
|
|
b5fa6192b3 | ||
|
|
721a3d2d27 | ||
|
|
bf9173301c | ||
|
|
e2cf6a40cb | ||
|
|
db681273e6 | ||
|
|
291fd5427b | ||
|
|
362f1da11f | ||
|
|
cc33c63501 | ||
|
|
6e54497492 | ||
|
|
9fde453ead | ||
|
|
7cd48f1c09 | ||
|
|
4c3d277fce | ||
|
|
0111626d80 | ||
|
|
0b83546db7 | ||
|
|
08ab8237e7 | ||
|
|
39ecfba3e4 | ||
|
|
ae5502b650 | ||
|
|
821d0b5147 | ||
|
|
b4ed1f35ce | ||
|
|
a468342187 | ||
|
|
4f6035dc49 | ||
|
|
506adcf81f | ||
|
|
49c8c2d57d | ||
|
|
2ab698c2d3 | ||
|
|
770f5f9724 | ||
|
|
992dcd651b | ||
|
|
98a38b03e4 | ||
|
|
626995ec91 | ||
|
|
00b0c8deaf | ||
|
|
8e5b51c42f | ||
|
|
b81ec0f073 | ||
|
|
d49d42254b | ||
|
|
95d5b4afa1 | ||
|
|
cca8c9bdf7 | ||
|
|
4e00191039 | ||
|
|
dd09595866 | ||
|
|
b27bfc3fa9 | ||
|
|
c6a9716d16 | ||
|
|
acd7a54ee9 | ||
|
|
e7d240549e | ||
|
|
70fedc53b9 | ||
|
|
cc163f952f | ||
|
|
63f57667ba | ||
|
|
34718a7b89 | ||
|
|
6ac91e6cf3 | ||
|
|
a39d6ab346 | ||
|
|
91d89670d2 |
@@ -0,0 +1,10 @@
|
||||
name: Build
|
||||
on: [pull_request, push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build the app
|
||||
run: ./gradlew build
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
language: android
|
||||
env:
|
||||
global:
|
||||
- ANDROID_HOME=/usr/local/android-sdk
|
||||
android:
|
||||
components:
|
||||
- android-29
|
||||
- build-tools-29.0.2
|
||||
before_install:
|
||||
- mkdir -p $ANDROID_HOME/licenses
|
||||
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > $ANDROID_HOME/licenses/android-sdk-license
|
||||
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> $ANDROID_HOME/licenses/android-sdk-license
|
||||
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> $ANDROID_HOME/licenses/android-sdk-license
|
||||
script:
|
||||
- ./gradlew build test
|
||||
+29
-13
@@ -1,25 +1,40 @@
|
||||
# Contributing
|
||||
|
||||
There are a couple of ways to contribute to Aegis. This document contains some
|
||||
general guidelines for each type of contribution.
|
||||
Looking to contribute to Aegis? That's great! There are a couple of ways to help
|
||||
out. This document contains some general guidelines for each type of
|
||||
contribution.
|
||||
|
||||
## Translations
|
||||
|
||||
We use [Crowdin](https://crowdin.com/project/aegis-authenticator) to crowdsource
|
||||
translations of Aegis for lots of different languages. __Pull requests that
|
||||
add/update a translation are not longer accepted.__ Crowdin is our single source
|
||||
of truth for translations, to keep things easy to maintain.
|
||||
|
||||
The top 30 languages are available for translation. It's possible that yours is
|
||||
not in that list. If that's the case, feel free to send us an email and we'll
|
||||
add it.
|
||||
|
||||
## Pull requests
|
||||
|
||||
If you're planning on adding a new feature or making other large changes, please
|
||||
discuss it with us first through a proposal on GitHub. Discussing your idea with
|
||||
us first ensures that everyone is on the same page before you start working on
|
||||
your change. We don't like rejecting pull requests.
|
||||
discuss it with us first through [a
|
||||
proposal](https://github.com/beemdevelopment/Aegis/issues/new?labels=proposal&template=feature.md)
|
||||
on GitHub. Discussing your idea with us first ensures that everyone is on the
|
||||
same page before you start working on your change. We don't like rejecting pull
|
||||
requests.
|
||||
|
||||
## Bug reports
|
||||
|
||||
We use GitHub's issue tracker to track bugs. Bug reports must follow [the
|
||||
We use GitHub's issue tracker to track bugs. To make bug reports easier to
|
||||
follow up on for us, they must follow [the
|
||||
template](.github/ISSUE_TEMPLATE/bug.md). If a bug report does not follow the
|
||||
template and does not contain enough information, it will be closed without a
|
||||
response. Duplicate bug reports receive the same treatment.
|
||||
template and does not contain enough information, it will be closed. Duplicate
|
||||
bug reports receive the same treatment.
|
||||
|
||||
Please consider trying to find the root cause yourself first and perhaps even
|
||||
send us a patch that fixes the issue! We're happy to help you if you get stuck
|
||||
along the way.
|
||||
Please consider trying to find the root cause yourself first and include your
|
||||
analysis of the issue in your report. Perhaps even send us a patch that fixes
|
||||
it! We're happy to help you if you get stuck along the way.
|
||||
|
||||
### Capturing a log with ADB
|
||||
|
||||
@@ -81,7 +96,7 @@ __On your PC__:
|
||||
|
||||
```
|
||||
adb logcat -c
|
||||
adb logcat -f debug.log
|
||||
adb logcat > debug.log
|
||||
```
|
||||
|
||||
The logcat command captures the full system log by default, which may expose
|
||||
@@ -91,8 +106,9 @@ __On your PC__:
|
||||
one below:
|
||||
|
||||
```sh
|
||||
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) -f debug.log
|
||||
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) > debug.log
|
||||
```
|
||||
|
||||
_If you are using a debug APK, replace ``com.beemdevelopment.aegis`` with
|
||||
``com.beemdevelopment.aegis.debug``._
|
||||
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
# Aegis
|
||||
<img align="left" width="80" height="80" src="metadata/en-US/images/icon.png"
|
||||
alt="App icon">
|
||||
|
||||
Aegis is a free, secure and open source 2FA app for Android.
|
||||
# Aegis Authenticator [](https://travis-ci.org/github/beemdevelopment/aegis) [](https://crowdin.com/project/aegis-authenticator) [](https://www.buymeacoffee.com/beemdevelopment) [](https://matrix.to/#/#aegis:matrix.org)
|
||||
|
||||
Aegis' security design and vault format is described in detail in [this
|
||||
document](docs/vault.md).
|
||||
__Aegis Authenticator__ is a free, secure and open source 2FA app for Android.
|
||||
It aims to provide a secure authenticator for your online services, while also
|
||||
including some features missing in existing authenticator apps, like proper
|
||||
encryption and backups. Aegis supports HOTP and TOTP, making it compatible with
|
||||
thousands of services.
|
||||
|
||||
# Features
|
||||
The security design of the app and the vault format is described in detail in
|
||||
[this document](docs/vault.md).
|
||||
|
||||
## Features
|
||||
|
||||
- Free and open source
|
||||
- Secure
|
||||
- Encryption (AES-256)
|
||||
- The vault is encrypted (AES-256-GCM), and can be unlocked with:
|
||||
- Password (scrypt)
|
||||
- Biometrics (Android Keystore)
|
||||
- Screen capture prevention
|
||||
- Tap to reveal ability
|
||||
- Multiple ways to add new entries
|
||||
- Scan QR code
|
||||
- Enter details manually
|
||||
- Import from files
|
||||
- andOTP
|
||||
- FreeOTP
|
||||
- Aegis
|
||||
- Import from apps (requires root):
|
||||
- Google Authenticator
|
||||
- Steam
|
||||
- Supported algorithms:
|
||||
- HOTP ([RFC 4226](https://tools.ietf.org/html/rfc4226))
|
||||
- TOTP ([RFC 6238](https://tools.ietf.org/html/rfc6238))
|
||||
- Steam ([RFC 6238](https://tools.ietf.org/html/rfc6238) with custom
|
||||
encoding)
|
||||
- Tap to reveal
|
||||
- Compatible with Google Authenticator
|
||||
- Supports industry standard algorithms:
|
||||
[HOTP](https://tools.ietf.org/html/rfc4226) and
|
||||
[TOTP](https://tools.ietf.org/html/rfc6238)
|
||||
- Lots of ways to add new entries
|
||||
- Scan a QR code or an image of one
|
||||
- Enter details manually
|
||||
- Import from other authenticator apps: Authenticator Plus, Authy, andOTP,
|
||||
FreeOTP, FreeOTP+, Google Authenticator, Microsoft Authenticator, Plain
|
||||
text, Steam, TOTP Authenticator and WinAuth (root access is required for
|
||||
some of these)
|
||||
- Organization
|
||||
- Custom or default icons
|
||||
- Drag and drop
|
||||
- Custom groups
|
||||
- Alphabetic/custom sorting
|
||||
- Custom or automatically generated icons
|
||||
- Group entries together
|
||||
- Advanced entry editing
|
||||
- Material design with multiple themes:
|
||||
- Light theme
|
||||
- Dark theme
|
||||
- Amoled / true dark theme
|
||||
- Search by name/issuer
|
||||
- Material design with multiple themes: Light, Dark, AMOLED
|
||||
- Export (plaintext or encrypted)
|
||||
- Automatic backups of the vault to a location of your choosing
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -106,24 +107,21 @@ Certificate fingerprints:
|
||||
|
||||
- [aegis-icons](https://github.com/krisu5/aegis-icons)
|
||||
|
||||
While we're still working on better icon support in Aegis, krisu5 has started
|
||||
a third-party repository for icons that you may want to check out.
|
||||
While we're still [working on better icon
|
||||
support](https://github.com/beemdevelopment/Aegis/issues/509) in Aegis, krisu5
|
||||
has started a third-party repository for icons that you may want to check out.
|
||||
|
||||
[<img width=500 alt="Aegis-icons preview"
|
||||
src="https://raw.githubusercontent.com/krisu5/aegis-icons/master/showcase.png">](https://github.com/krisu5/aegis-icons)
|
||||
|
||||
## Libraries
|
||||
## Contributing
|
||||
|
||||
- [TextDrawable](https://github.com/amulyakhare/TextDrawable) by Amulya Khare
|
||||
- [FloatingActionButton](https://github.com/Clans/FloatingActionButton) by
|
||||
Dmytro Tarianyk
|
||||
- [AppIntro](https://github.com/AppIntro/AppIntro) by Paolo Rotolo
|
||||
- [Krop](https://github.com/avito-tech/krop) by Avito Technology
|
||||
- [SpongyCastle](https://github.com/rtyley/spongycastle) by Roberto Tyley
|
||||
- [CircleImageView](https://github.com/hdodenhof/CircleImageView) by Henning
|
||||
Dodenhof
|
||||
- [barcodescanner](https://github.com/dm77/barcodescanner) by Dushyanth
|
||||
- [libsu](https://github.com/topjohnwu/libsu) by John Wu
|
||||
Looking to contribute to Aegis? That's great! There are a couple of ways to help
|
||||
out. Translations, bug reports and pull requests are all greatly appreciated.
|
||||
Please refer to our [contributing guidelines](CONTRIBUTING.md) to get started.
|
||||
|
||||
Swing by our Matrix room to interact with other contributors:
|
||||
[#aegis:matrix.org](https://matrix.to/#/#aegis:matrix.org).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+81
-33
@@ -13,18 +13,25 @@ def getCmdOutput = { cmd ->
|
||||
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
|
||||
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
|
||||
|
||||
def packageName = "com.beemdevelopment.aegis"
|
||||
def fileProviderAuthority = "${packageName}.fileprovider"
|
||||
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.beemdevelopment.aegis"
|
||||
applicationId "${packageName}"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 33
|
||||
versionName "1.2-beta4"
|
||||
targetSdkVersion 30
|
||||
versionCode 45
|
||||
versionName "1.4.2"
|
||||
multiDexEnabled true
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
|
||||
|
||||
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
@@ -33,6 +40,8 @@ android {
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
@@ -46,7 +55,12 @@ android {
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
manifestPlaceholders = [title: "AegisDev", iconName: "ic_launcher_debug"]
|
||||
manifestPlaceholders = [
|
||||
title: "AegisDev",
|
||||
iconName: "ic_launcher_debug",
|
||||
fileProviderAuthority: "${fileProviderAuthorityDebug}"
|
||||
]
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthorityDebug}\"")
|
||||
resValue "bool", "pref_secure_screen_default", "false"
|
||||
postprocessing {
|
||||
removeUnusedCode true
|
||||
@@ -57,7 +71,12 @@ android {
|
||||
}
|
||||
}
|
||||
release {
|
||||
manifestPlaceholders = [title: "Aegis", iconName: "ic_launcher"]
|
||||
manifestPlaceholders = [
|
||||
title: "Aegis",
|
||||
iconName: "ic_launcher",
|
||||
fileProviderAuthority: "${fileProviderAuthority}"
|
||||
]
|
||||
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthority}\"")
|
||||
resValue "bool", "pref_secure_screen_default", "true"
|
||||
postprocessing {
|
||||
removeUnusedCode true
|
||||
@@ -72,6 +91,7 @@ android {
|
||||
compileOptions {
|
||||
targetCompatibility 1.8
|
||||
sourceCompatibility 1.8
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,37 +111,65 @@ protobuf {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def libsuVersion = '2.5.1'
|
||||
def cameraxVersion = '1.0.0-rc01'
|
||||
def glideVersion = '4.11.0'
|
||||
def guavaVersion = '30.1'
|
||||
def junitVersion = '5.7.0'
|
||||
def libsuVersion = '3.0.2'
|
||||
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.8.0'
|
||||
implementation 'net.lingala.zip4j:zip4j:2.5.2'
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
implementation "androidx.camera:camera-camera2:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha20"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.2.0"
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.guava:guava:28.2-android'
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.getbase:floatingactionbutton:1.10.1'
|
||||
implementation 'com.github.apl-devs:appintro:6.0.0'
|
||||
implementation 'com.github.avito-tech:krop:0.44'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
|
||||
implementation 'de.hdodenhof:circleimageview:3.0.1'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
implementation 'com.github.bumptech.glide:annotations:4.10.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.10.0'
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
||||
implementation("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
|
||||
implementation 'com.github.avito-tech:krop:0.51'
|
||||
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
|
||||
implementation "com.github.bumptech.glide:glide:${glideVersion}"
|
||||
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
|
||||
transitive = false
|
||||
}
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
|
||||
testImplementation 'com.google.guava:guava:28.2-jre"'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
implementation "com.google.guava:guava:${guavaVersion}-android"
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.14.0'
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
|
||||
implementation 'com.nulab-inc:zxcvbn:1.3.1'
|
||||
implementation 'de.hdodenhof:circleimageview:3.1.0'
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
||||
// NOTE: this is kept at an old version on purpose (something in newer versions breaks the Authenticator Plus importer)
|
||||
implementation 'net.lingala.zip4j:zip4j:2.6.0'
|
||||
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.68'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
|
||||
androidTestImplementation 'junit:junit:4.13.1'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.3.0'
|
||||
|
||||
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -16,6 +16,8 @@
|
||||
# public *;
|
||||
#}
|
||||
|
||||
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
|
||||
|
||||
-keep class com.beemdevelopment.aegis.importers.** { *; }
|
||||
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.SCryptParameters;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public abstract class AegisTest {
|
||||
public static final String VAULT_PASSWORD = "test";
|
||||
|
||||
protected AegisApplication getApp() {
|
||||
return (AegisApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
|
||||
}
|
||||
|
||||
protected VaultManager getVault() {
|
||||
return getApp().getVaultManager();
|
||||
}
|
||||
|
||||
protected VaultManager initVault() {
|
||||
PasswordSlot slot = new PasswordSlot();
|
||||
byte[] salt = CryptoUtils.generateSalt();
|
||||
SCryptParameters scryptParams = new SCryptParameters(
|
||||
CryptoUtils.CRYPTO_SCRYPT_N,
|
||||
CryptoUtils.CRYPTO_SCRYPT_r,
|
||||
CryptoUtils.CRYPTO_SCRYPT_p,
|
||||
salt
|
||||
);
|
||||
|
||||
VaultFileCredentials creds = new VaultFileCredentials();
|
||||
try {
|
||||
SecretKey key = slot.deriveKey(VAULT_PASSWORD.toCharArray(), scryptParams);
|
||||
slot.setKey(creds.getKey(), CryptoUtils.createEncryptCipher(key));
|
||||
} catch (NoSuchAlgorithmException
|
||||
| InvalidKeyException
|
||||
| InvalidAlgorithmParameterException
|
||||
| NoSuchPaddingException
|
||||
| SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
creds.getSlots().add(slot);
|
||||
|
||||
VaultManager vault = getApp().initVaultManager(new Vault(), creds);
|
||||
try {
|
||||
vault.save(false);
|
||||
} catch (VaultManagerException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
getApp().getPreferences().setIntroDone(true);
|
||||
return vault;
|
||||
}
|
||||
|
||||
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
|
||||
byte[] secret = CryptoUtils.generateRandomBytes(20);
|
||||
|
||||
OtpInfo info;
|
||||
try {
|
||||
info = type.getConstructor(byte[].class).newInstance(secret);
|
||||
} catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return new VaultEntry(info, name, issuer);
|
||||
}
|
||||
|
||||
// source: https://stackoverflow.com/a/30338665
|
||||
protected static ViewAction clickChildViewWithId(final int id) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Click on a child view with specified id.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
View v = view.findViewById(id);
|
||||
v.performClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.runner.AndroidJUnitRunner;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
public class AegisTestRunner extends AndroidJUnitRunner {
|
||||
@Override
|
||||
public void callApplicationOnCreate(Application app) {
|
||||
Context context = app.getApplicationContext();
|
||||
|
||||
// clear internal storage so that there is no vault file
|
||||
IOUtils.clearDirectory(context.getFilesDir(), false);
|
||||
|
||||
// clear preferences so that the intro is started from MainActivity
|
||||
ApplicationProvider.getApplicationContext().getFilesDir();
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.clear()
|
||||
.apply();
|
||||
|
||||
super.callApplicationOnCreate(app);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class IntentTest extends AegisTest {
|
||||
@Before
|
||||
public void before() {
|
||||
initVault();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doDeepLinkIntent() {
|
||||
VaultEntry entry = generateEntry(TotpInfo.class, "Bob", "Google");
|
||||
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
|
||||
launch(info.getUri());
|
||||
|
||||
onView(withId(R.id.action_save)).perform(click());
|
||||
|
||||
VaultEntry createdEntry = (VaultEntry) getVault().getEntries().toArray()[0];
|
||||
assertTrue(createdEntry.equivalates(entry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doDeepLinkIntent_Empty() {
|
||||
launch(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doDeepLinkIntent_Bad() {
|
||||
launch(Uri.parse("otpauth://bad"));
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void launch(Uri uri) {
|
||||
Intent intent = new Intent(getApp(), MainActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(uri);
|
||||
|
||||
// we need to use the deprecated ActivityTestRule class because of https://github.com/android/android-test/issues/143
|
||||
ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);
|
||||
rule.launchActivity(intent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.IntroActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.replaceText;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class IntroTest extends AegisTest {
|
||||
@Rule
|
||||
public final ActivityScenarioRule<IntroActivity> activityRule = new ActivityScenarioRule<>(IntroActivity.class);
|
||||
|
||||
@Test
|
||||
public void doIntro_None() {
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
ViewInteraction prev = onView(withId(R.id.btnPrevious));
|
||||
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_none)).perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
|
||||
VaultManager vault = getVault();
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(getVault().getCredentials());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doIntro_Password() {
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
ViewInteraction prev = onView(withId(R.id.btnPrevious));
|
||||
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_password)).perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD + "1"), closeSoftKeyboard());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password_confirm)).perform(replaceText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
prev.perform(click());
|
||||
prev.perform(click());
|
||||
prev.check(matches(not(isDisplayed())));
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
next.perform(click());
|
||||
|
||||
VaultManager vault = getVault();
|
||||
SlotList slots = getVault().getCredentials().getSlots();
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(slots.has(PasswordSlot.class));
|
||||
assertFalse(slots.has(BiometricSlot.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.espresso.AmbiguousViewMatcherException;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onData;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
|
||||
import static androidx.test.espresso.action.ViewActions.clearText;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.action.ViewActions.pressBack;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static junit.framework.TestCase.assertNull;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.hamcrest.Matchers.anything;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class OverallTest extends AegisTest {
|
||||
private static final String _groupName = "Test";
|
||||
|
||||
@Rule
|
||||
public final ActivityScenarioRule<MainActivity> activityRule = new ActivityScenarioRule<>(MainActivity.class);
|
||||
|
||||
@Test
|
||||
public void doOverallTest() {
|
||||
ViewInteraction next = onView(withId(R.id.btnNext));
|
||||
next.perform(click());
|
||||
onView(withId(R.id.rb_password)).perform(click());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
next.perform(click());
|
||||
onView(withId(R.id.btnNext)).perform(click());
|
||||
|
||||
VaultManager vault = getVault();
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
|
||||
|
||||
List<VaultEntry> entries = Arrays.asList(
|
||||
generateEntry(TotpInfo.class, "Frank", "Google"),
|
||||
generateEntry(HotpInfo.class, "John", "GitHub"),
|
||||
generateEntry(TotpInfo.class, "Alice", "Office 365"),
|
||||
generateEntry(SteamInfo.class, "Gaben", "Steam")
|
||||
);
|
||||
for (VaultEntry entry : entries) {
|
||||
addEntry(entry);
|
||||
}
|
||||
|
||||
List<VaultEntry> realEntries = new ArrayList<>(vault.getEntries());
|
||||
for (int i = 0; i < realEntries.size(); i++) {
|
||||
assertTrue(realEntries.get(i).equivalates(entries.get(i)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, clickChildViewWithId(R.id.buttonRefresh)));
|
||||
}
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
|
||||
onView(withId(R.id.action_copy)).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
||||
onView(withId(R.id.action_edit)).perform(click());
|
||||
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
|
||||
onView(withId(R.id.spinner_group)).perform(click());
|
||||
onData(anything()).atPosition(1).perform(click());
|
||||
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
onView(isRoot()).perform(pressBack());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
changeSort(R.string.sort_alphabetically_name);
|
||||
changeSort(R.string.sort_alphabetically_name_reverse);
|
||||
changeSort(R.string.sort_alphabetically);
|
||||
changeSort(R.string.sort_alphabetically_reverse);
|
||||
changeSort(R.string.sort_custom);
|
||||
|
||||
changeFilter(_groupName);
|
||||
changeFilter(R.string.filter_ungrouped);
|
||||
changeFilter(R.string.all);
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
|
||||
onView(withId(R.id.action_share_qr)).perform(click());
|
||||
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
|
||||
|
||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
|
||||
onView(withId(R.id.action_delete)).perform(click());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.lock)).perform(click());
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.button_decrypt)).perform(click());
|
||||
vault = getVault();
|
||||
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.action_settings)).perform(click());
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
assertFalse(vault.isEncryptionEnabled());
|
||||
assertNull(vault.getCredentials());
|
||||
|
||||
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
|
||||
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
|
||||
onView(withId(android.R.id.button1)).perform(click());
|
||||
|
||||
assertTrue(vault.isEncryptionEnabled());
|
||||
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
|
||||
}
|
||||
|
||||
private void changeSort(@IdRes int resId) {
|
||||
onView(withId(R.id.action_sort)).perform(click());
|
||||
onView(withText(resId)).perform(click());
|
||||
}
|
||||
|
||||
private void changeFilter(String text) {
|
||||
openContextualActionModeOverflowMenu();
|
||||
onView(withText(R.string.filter)).perform(click());
|
||||
onView(withText(text)).perform(click());
|
||||
}
|
||||
|
||||
private void changeFilter(@IdRes int resId) {
|
||||
changeFilter(ApplicationProvider.getApplicationContext().getString(resId));
|
||||
}
|
||||
|
||||
private void addEntry(VaultEntry entry) {
|
||||
onView(withId(R.id.fab_expand_menu_button)).perform(click());
|
||||
onView(withId(R.id.fab_enter)).perform(click());
|
||||
|
||||
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
|
||||
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
|
||||
|
||||
if (entry.getInfo().getClass() != TotpInfo.class) {
|
||||
int i = entry.getInfo() instanceof HotpInfo ? 1 : 2;
|
||||
try {
|
||||
onView(withId(R.id.spinner_type)).perform(click());
|
||||
onData(anything()).atPosition(i).perform(click());
|
||||
} catch (AmbiguousViewMatcherException e) {
|
||||
// for some reason, clicking twice is sometimes necessary, otherwise the test fails on the next line
|
||||
onView(withId(R.id.spinner_type)).perform(click());
|
||||
onData(anything()).atPosition(i).perform(click());
|
||||
}
|
||||
if (entry.getInfo() instanceof HotpInfo) {
|
||||
onView(withId(R.id.text_counter)).perform(typeText("0"), closeSoftKeyboard());
|
||||
}
|
||||
if (entry.getInfo() instanceof SteamInfo) {
|
||||
onView(withId(R.id.text_digits)).perform(clearText(), typeText("5"), closeSoftKeyboard());
|
||||
}
|
||||
}
|
||||
|
||||
String secret = Base32.encode(entry.getInfo().getSecret());
|
||||
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
|
||||
|
||||
onView(withId(R.id.action_save)).perform(click());
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,12 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".AegisApplication"
|
||||
@@ -18,16 +22,17 @@
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:theme">
|
||||
<activity android:name=".ui.TransferEntriesActivity"
|
||||
android:label="@string/title_activity_transfer" />
|
||||
<activity
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/title_activity_about" />
|
||||
<activity
|
||||
android:name=".ui.SelectEntriesActivity"
|
||||
android:label="Select entries" />
|
||||
android:label="@string/title_activity_select_entries" />
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:label="${title}"
|
||||
android:launchMode="singleTask">
|
||||
android:label="${title}">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -45,24 +50,61 @@
|
||||
<activity
|
||||
android:name=".ui.ScannerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:label="Scan a QR code"
|
||||
android:label="@string/title_activity_scan_qr"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ui.EditEntryActivity"
|
||||
android:label="Edit entry" />
|
||||
android:label="@string/title_activity_edit_entry" />
|
||||
<activity
|
||||
android:name=".ui.IntroActivity"
|
||||
android:theme="@style/Theme.Intro" />
|
||||
<activity android:name=".ui.AuthActivity" />
|
||||
<activity android:name=".ui.PreferencesActivity" />
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".ui.AuthActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
<activity
|
||||
android:name=".ui.PreferencesActivity"
|
||||
android:label="@string/title_activity_preferences" />
|
||||
<activity
|
||||
android:name=".ui.SlotManagerActivity"
|
||||
android:label="Manage key slots" />
|
||||
android:label="@string/title_activity_manage_slots" />
|
||||
<activity
|
||||
android:name=".ui.GroupManagerActivity"
|
||||
android:label="Manage groups" />
|
||||
android:label="@string/title_activity_manage_groups" />
|
||||
<activity
|
||||
android:name=".ui.PanicResponderActivity"
|
||||
android:launchMode="singleInstance"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.ExitActivity" />
|
||||
|
||||
<service android:name=".services.NotificationService" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${fileProviderAuthority}"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths">
|
||||
</meta-data>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
<queries>
|
||||
<package android:name="com.authy.authy" />
|
||||
<package android:name="org.fedorahosted.freeotp" />
|
||||
<package android:name="org.liberty.android.freeotpplus" />
|
||||
<package android:name="com.google.android.apps.authenticator2" />
|
||||
<package android:name="com.azure.authenticator" />
|
||||
<package android:name="com.valvesoftware.android.steam.community" />
|
||||
<package android:name="com.authenticator.authservice2" />
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -31,6 +31,74 @@
|
||||
</head>
|
||||
<body>
|
||||
<div></div>
|
||||
<h3>Version 1.4.2</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>The app would crash if DocumentsUI is not present on the device</li>
|
||||
<li>The app would close when selecting an icon if auto lock on minimize was enabled</li>
|
||||
<li>Importing from Authy was flaky for entries that have an icon</li>
|
||||
<li>The dark theme was not properly applied to the QR code scanner view</li>
|
||||
<li>The app would crash on plain text export on some devices</li>
|
||||
<li>Importing from Authenticator Plus stopped working</li>
|
||||
</ul>
|
||||
<h3>Version 1.4.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Scanning QR codes stopped working on certain devices (primarily OnePlus)</li>
|
||||
</ul>
|
||||
<h3>Version 1.4</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Optionally delete the vault if a panic trigger is received from Ripple</li>
|
||||
<li>More customizable auto-lock</li>
|
||||
<li>More flexible export options
|
||||
<ul>
|
||||
<li>Share mechanism</li>
|
||||
<li>Offer to encrypt even if this feature is disabled in the app</li>
|
||||
<li>Export to a Google Authenticator URI file</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Perform exports/backups on a background thread (automatic backups now work with Nextcloud)</li>
|
||||
<li>Color improvements to the dark theme (slightly darker)</li>
|
||||
<li>Offer more locations to select an image/icon from</li>
|
||||
<li>Display some helpful information when importing from a different app</li>
|
||||
<li>Minimum tap to reveal timeout changed to 1 second</li>
|
||||
<li>After an entry is added, scroll to it and highlight it</li>
|
||||
<li>Updated translations, and new translations for: Basque, Chinese Traditional, Hindi, Indonesian, Japanese, Persian, Romanian, Slovak</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Scanning large images for QR codes would fail</li>
|
||||
<li>The FAB would remain hidden under certain circumstances</li>
|
||||
<li>The app would crash if an entry was added to the vault twice due to an IO error</li>
|
||||
<li>The app would crash if the device was rotated while a progress dialog was shown</li>
|
||||
<li>The PIN keyboard would show even if a new non-digit password was set</li>
|
||||
<li>The password reminder popup would be occluded by the autofill popup</li>
|
||||
<li>Importing from other apps on Android 11 was broken due to some permission issues</li>
|
||||
</ul>
|
||||
<h3>Version 1.3</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Completely rewritten intro/onboarding</li>
|
||||
<li>Option to show a PIN keyboard when unlocking Aegis</li>
|
||||
<li>A password strength meter when setting up encryption (based on zxcvbn)</li>
|
||||
<li>RTL support</li>
|
||||
<li>Arabic and Portuguese translations</li>
|
||||
<li>Updates to existing translations</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Better lifecycle handling of the biometric authentication prompt</li>
|
||||
<li>The filename of exported vaults had a double .json extension</li>
|
||||
<li>The navigation bar color was incorrect on devices pre API 27</li>
|
||||
<li>QR code scanner performance and stability improvements</li>
|
||||
<li>Various other small usability and stability improvements</li>
|
||||
</ul>
|
||||
<h3>Version 1.2.1</h3>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Fix a rare issue where the intro could end up in a bad state</li>
|
||||
</ul>
|
||||
<h3>Version 1.2</h3>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
@@ -41,6 +109,7 @@
|
||||
<li>Add support for importing a plain text Google Authenticator URI file</li>
|
||||
<li>Add support for importing from the new Google Authenticator export QR codes</li>
|
||||
<li>Add support for otpauth://steam URI's</li>
|
||||
<li>Add an option to copy tokens on tap (and disable it by default)</li>
|
||||
<li>Improve method to notify users on copy</li>
|
||||
<li>Add support for backups</li>
|
||||
<li>Improve multiselect flow</li>
|
||||
@@ -49,17 +118,23 @@
|
||||
<li>Use most frequent period to show progress</li>
|
||||
<li>Append a timestamp to the filename of exported vaults</li>
|
||||
<li>Add Hungarian translation</li>
|
||||
<li>Add Turkish translation</li>
|
||||
<li>Display a warning if automatic time sync is not enabled</li>
|
||||
<li>Minor card entry layout overhaul</li>
|
||||
<li>Ability to transfer tokens with qr codes</li>
|
||||
<li>Lockscreen overhaul</li>
|
||||
</ul>
|
||||
<h4>Fixes</h4>
|
||||
<ul>
|
||||
<li>Improve overall exception handling and error feedback to the user</li>
|
||||
<li>Improve icon editing flow</li>
|
||||
<li>Protect writes of the vault file against corruption with AtomicFile</li>
|
||||
<li>Make the parsing logic of the QR code URI more robust</li>
|
||||
<li>Importing from Authy now asks for password if needed</li>
|
||||
<li>Update Russian localization</li>
|
||||
<li>Increase password reminder period to 30 days</li>
|
||||
<li>Fix importing andOTP backups with more than 10000 PBKDF iterations</li>
|
||||
<li>Respect the global animator duration scale setting</li>
|
||||
</ul>
|
||||
<p>Various other minor improvements</p>
|
||||
<h3>Version 1.1.4</h3>
|
||||
|
||||
@@ -12,10 +12,16 @@ import android.content.pm.ShortcutManager;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleEventObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
@@ -23,6 +29,7 @@ import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -33,10 +40,16 @@ public class AegisApplication extends Application {
|
||||
private VaultManager _manager;
|
||||
private Preferences _prefs;
|
||||
private List<LockListener> _lockListeners;
|
||||
private boolean _blockAutoLock;
|
||||
|
||||
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
|
||||
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
|
||||
|
||||
static {
|
||||
// to access other app's internal storage directory, run libsu commands inside the global mount namespace
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
@@ -53,6 +66,12 @@ public class AegisApplication extends Application {
|
||||
intentFilter.addAction(CODE_LOCK_VAULT_ACTION);
|
||||
registerReceiver(receiver, intentFilter);
|
||||
|
||||
// lock the app if the user moves the application to the background
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
|
||||
|
||||
// clear the cache directory on startup, to make sure no temporary vault export files remain
|
||||
IOUtils.clearDirectory(getCacheDir(), false);
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
initAppShortcuts();
|
||||
}
|
||||
@@ -111,8 +130,8 @@ public class AegisApplication extends Application {
|
||||
return _prefs;
|
||||
}
|
||||
|
||||
public boolean isAutoLockEnabled() {
|
||||
return _prefs.isAutoLockEnabled() && !isVaultLocked() && _manager.isEncryptionEnabled() ;
|
||||
public boolean isAutoLockEnabled(int autoLockType) {
|
||||
return _prefs.isAutoLockTypeEnabled(autoLockType) && !isVaultLocked() && _manager.isEncryptionEnabled();
|
||||
}
|
||||
|
||||
public void registerLockListener(LockListener listener) {
|
||||
@@ -123,10 +142,25 @@ public class AegisApplication extends Application {
|
||||
_lockListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void lock() {
|
||||
/**
|
||||
* Sets whether to block automatic lock on minimization. This should only be called
|
||||
* by activities before invoking an intent that shows a DocumentsUI, because that
|
||||
* action leads AppLifecycleObserver to believe that the app has been minimized.
|
||||
*/
|
||||
public void setBlockAutoLock(boolean block) {
|
||||
_blockAutoLock = block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the vault and the app.
|
||||
* @param userInitiated whether or not the user initiated the lock in MainActivity.
|
||||
*/
|
||||
public void lock(boolean userInitiated) {
|
||||
_manager.destroy();
|
||||
_manager = null;
|
||||
|
||||
for (LockListener listener : _lockListeners) {
|
||||
listener.onLocked();
|
||||
listener.onLocked(userInitiated);
|
||||
}
|
||||
|
||||
stopService(new Intent(AegisApplication.this, NotificationService.class));
|
||||
@@ -141,6 +175,7 @@ public class AegisApplication extends Application {
|
||||
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.putExtra("action", "scan");
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
|
||||
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
|
||||
@@ -167,16 +202,31 @@ public class AegisApplication extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
public class ScreenOffReceiver extends BroadcastReceiver {
|
||||
private class AppLifecycleObserver implements LifecycleEventObserver {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
|
||||
if (event == Lifecycle.Event.ON_STOP
|
||||
&& isAutoLockEnabled(Preferences.AUTO_LOCK_ON_MINIMIZE)
|
||||
&& !_blockAutoLock) {
|
||||
lock(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ScreenOffReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (isAutoLockEnabled()) {
|
||||
lock();
|
||||
if (isAutoLockEnabled(Preferences.AUTO_LOCK_ON_DEVICE_LOCK)) {
|
||||
lock(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface LockListener {
|
||||
void onLocked();
|
||||
/**
|
||||
* When called, the app/vault has been locked and the listener should perform its cleanup operations.
|
||||
* @param userInitiated whether or not the user initiated the lock in MainActivity.
|
||||
*/
|
||||
void onLocked(boolean userInitiated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
public enum CancelAction {
|
||||
KILL,
|
||||
CLOSE
|
||||
}
|
||||
|
||||
@@ -12,6 +12,17 @@ import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Preferences {
|
||||
public static final int AUTO_LOCK_OFF = 1 << 0;
|
||||
public static final int AUTO_LOCK_ON_BACK_BUTTON = 1 << 1;
|
||||
public static final int AUTO_LOCK_ON_MINIMIZE = 1 << 2;
|
||||
public static final int AUTO_LOCK_ON_DEVICE_LOCK = 1 << 3;
|
||||
|
||||
public static final int[] AUTO_LOCK_SETTINGS = {
|
||||
AUTO_LOCK_ON_BACK_BUTTON,
|
||||
AUTO_LOCK_ON_MINIMIZE,
|
||||
AUTO_LOCK_ON_DEVICE_LOCK
|
||||
};
|
||||
|
||||
private SharedPreferences _prefs;
|
||||
|
||||
public Preferences(Context context) {
|
||||
@@ -34,6 +45,10 @@ public class Preferences {
|
||||
return _prefs.getBoolean("pref_highlight_entry", false);
|
||||
}
|
||||
|
||||
public boolean isPanicTriggerEnabled() {
|
||||
return _prefs.getBoolean("pref_panic_trigger", false);
|
||||
}
|
||||
|
||||
public boolean isSecureScreenEnabled() {
|
||||
// screen security should be enabled by default, but not for debug builds
|
||||
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
|
||||
@@ -73,8 +88,25 @@ public class Preferences {
|
||||
return _prefs.getBoolean("pref_intro", false);
|
||||
}
|
||||
|
||||
private int getAutoLockMask() {
|
||||
final int def = AUTO_LOCK_ON_BACK_BUTTON | AUTO_LOCK_ON_DEVICE_LOCK;
|
||||
if (!_prefs.contains("pref_auto_lock_mask")) {
|
||||
return _prefs.getBoolean("pref_auto_lock", true) ? def : AUTO_LOCK_OFF;
|
||||
}
|
||||
|
||||
return _prefs.getInt("pref_auto_lock_mask", def);
|
||||
}
|
||||
|
||||
public boolean isAutoLockEnabled() {
|
||||
return _prefs.getBoolean("pref_auto_lock", true);
|
||||
return getAutoLockMask() != AUTO_LOCK_OFF;
|
||||
}
|
||||
|
||||
public boolean isAutoLockTypeEnabled(int autoLockType) {
|
||||
return (getAutoLockMask() & autoLockType) == autoLockType;
|
||||
}
|
||||
|
||||
public void setAutoLockMask(int autoLock) {
|
||||
_prefs.edit().putInt("pref_auto_lock_mask", autoLock).apply();
|
||||
}
|
||||
|
||||
public void setIntroDone(boolean done) {
|
||||
@@ -128,7 +160,12 @@ public class Preferences {
|
||||
}
|
||||
}
|
||||
|
||||
return new Locale(lang);
|
||||
String[] parts = lang.split("_");
|
||||
if (parts.length == 1) {
|
||||
return new Locale(parts[0]);
|
||||
}
|
||||
|
||||
return new Locale(parts[0], parts[1]);
|
||||
}
|
||||
|
||||
public boolean isBackupsEnabled() {
|
||||
@@ -160,6 +197,18 @@ public class Preferences {
|
||||
_prefs.edit().putInt("pref_backups_versions", versions).apply();
|
||||
}
|
||||
|
||||
public void setBackupsError(Exception e) {
|
||||
_prefs.edit().putString("pref_backups_error", e == null ? null : e.toString()).apply();
|
||||
}
|
||||
|
||||
public String getBackupsError() {
|
||||
return _prefs.getString("pref_backups_error", null);
|
||||
}
|
||||
|
||||
public boolean isPinKeyboardEnabled() {
|
||||
return _prefs.getBoolean("pref_pin_keyboard", false);
|
||||
}
|
||||
|
||||
public boolean isTimeSyncWarningEnabled() {
|
||||
return _prefs.getBoolean("pref_warn_time_sync", true);
|
||||
}
|
||||
@@ -167,4 +216,8 @@ public class Preferences {
|
||||
public void setIsTimeSyncWarningEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
|
||||
}
|
||||
|
||||
public boolean isCopyOnTapEnabled() {
|
||||
return _prefs.getBoolean("pref_copy_on_tap", false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ThemeMap {
|
||||
private ThemeMap() {
|
||||
|
||||
}
|
||||
|
||||
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.AppTheme,
|
||||
Theme.DARK, R.style.AppTheme_Dark,
|
||||
Theme.AMOLED, R.style.AppTheme_TrueBlack
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> NO_ACTION_BAR = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.AppTheme_Light_NoActionBar,
|
||||
Theme.DARK, R.style.AppTheme_Dark_NoActionBar,
|
||||
Theme.AMOLED, R.style.AppTheme_TrueBlack_NoActionBar
|
||||
);
|
||||
|
||||
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
|
||||
Theme.LIGHT, R.style.AppTheme_Fullscreen,
|
||||
Theme.DARK, R.style.AppTheme_Fullscreen_Dark,
|
||||
Theme.AMOLED, R.style.AppTheme_Fullscreen_TrueBlack
|
||||
);
|
||||
}
|
||||
@@ -63,9 +63,9 @@ public class KeyStoreHandle {
|
||||
return generator.generateKey();
|
||||
} catch (ProviderException e) {
|
||||
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
|
||||
// so if this was caused by a KeyStoreException, throw a KeyStoreHandleException instead
|
||||
// so if this was caused by an android.security.KeyStoreException, throw a KeyStoreHandleException instead
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof KeyStoreException) {
|
||||
if (cause != null && cause.getClass().getName().equals("android.security.KeyStoreException")) {
|
||||
throw new KeyStoreHandleException(cause);
|
||||
}
|
||||
throw e;
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.beemdevelopment.aegis.crypto.pins;
|
||||
|
||||
import info.guardianproject.trustedintents.ApkSignaturePin;
|
||||
|
||||
public final class GuardianProjectFDroidRSA2048 extends ApkSignaturePin {
|
||||
|
||||
public GuardianProjectFDroidRSA2048() {
|
||||
fingerprints = new String[]{
|
||||
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
|
||||
};
|
||||
certificates = new byte[][]{
|
||||
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.beemdevelopment.aegis.crypto.pins;
|
||||
|
||||
import info.guardianproject.trustedintents.ApkSignaturePin;
|
||||
|
||||
public final class GuardianProjectFDroidRSA4096 extends ApkSignaturePin {
|
||||
|
||||
public GuardianProjectFDroidRSA4096() {
|
||||
fingerprints = new String[]{
|
||||
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
|
||||
};
|
||||
certificates = new byte[][]{
|
||||
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,19 @@ package com.beemdevelopment.aegis.encoding;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
public class Base32 {
|
||||
private static final BaseEncoding _encoding = BaseEncoding.base32().omitPadding();
|
||||
|
||||
private Base32() {
|
||||
|
||||
}
|
||||
|
||||
public static byte[] decode(String s) throws EncodingException {
|
||||
try {
|
||||
return _encoding.decode(s.toUpperCase());
|
||||
return BaseEncoding.base32().decode(s.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new EncodingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
return _encoding.encode(data);
|
||||
return BaseEncoding.base32().omitPadding().encode(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
public class BitmapHelper {
|
||||
private BitmapHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales the given Bitmap to the given maximum width/height, while keeping the aspect ratio intact.
|
||||
*/
|
||||
public static Bitmap resize(Bitmap bitmap, int maxWidth, int maxHeight) {
|
||||
if (maxHeight <= 0 || maxWidth <= 0) {
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
float maxRatio = (float) maxWidth / maxHeight;
|
||||
float ratio = (float) bitmap.getWidth() / bitmap.getHeight();
|
||||
|
||||
int width = maxWidth;
|
||||
int height = maxHeight;
|
||||
if (maxRatio > 1) {
|
||||
width = (int) ((float) maxHeight * ratio);
|
||||
} else {
|
||||
height = (int) ((float) maxWidth / ratio);
|
||||
}
|
||||
|
||||
return Bitmap.createScaledBitmap(bitmap, width, height, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
|
||||
import androidx.activity.ComponentActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* ContextHelper contains some disgusting hacks to obtain the Activity/Lifecycle from a Context.
|
||||
*/
|
||||
public class ContextHelper {
|
||||
private ContextHelper() {
|
||||
|
||||
}
|
||||
|
||||
// source: https://github.com/androidx/androidx/blob/e32e1da51a0c7448c74861c667fa76738a415a89/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteButton.java#L425-L435
|
||||
@Nullable
|
||||
public static ComponentActivity getActivity(@NonNull Context context) {
|
||||
while (context instanceof ContextWrapper) {
|
||||
if (context instanceof ComponentActivity) {
|
||||
return (ComponentActivity) context;
|
||||
}
|
||||
|
||||
context = ((ContextWrapper) context).getBaseContext();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Lifecycle getLifecycle(@NonNull Context context) {
|
||||
ComponentActivity activity = getActivity(context);
|
||||
return activity == null ? null : activity.getLifecycle();
|
||||
}
|
||||
}
|
||||
@@ -9,22 +9,10 @@ public class EditTextHelper {
|
||||
private EditTextHelper() {
|
||||
}
|
||||
|
||||
public static void clearEditText(EditText text) {
|
||||
text.getText().clear();
|
||||
}
|
||||
|
||||
public static char[] getEditTextChars(EditText text) {
|
||||
return getEditTextChars(text, false);
|
||||
}
|
||||
|
||||
public static char[] getEditTextChars(EditText text, boolean removeSpaces) {
|
||||
String editTextString = text.getText().toString();
|
||||
if (removeSpaces) {
|
||||
editTextString = editTextString.replaceAll("\\s","");
|
||||
}
|
||||
|
||||
char[] chars = new char[editTextString.length()];
|
||||
editTextString.getChars(0, editTextString.length(), chars, 0);
|
||||
Editable editable = text.getText();
|
||||
char[] chars = new char[editable.length()];
|
||||
editable.getChars(0, editable.length(), chars, 0);
|
||||
return chars;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,26 @@ public class FabScrollHelper {
|
||||
|
||||
public void onScroll(int dx, int dy) {
|
||||
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
|
||||
setVisible(false);
|
||||
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
|
||||
setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
if (visible) {
|
||||
_fabMenu.setVisibility(View.VISIBLE);
|
||||
_fabMenu.animate()
|
||||
.translationY(0)
|
||||
.setInterpolator(new DecelerateInterpolator(2))
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
_isAnimating = false;
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
_isAnimating = true;
|
||||
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
|
||||
int fabBottomMargin = lp.bottomMargin;
|
||||
@@ -32,18 +52,7 @@ public class FabScrollHelper {
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
|
||||
_fabMenu.setVisibility(View.VISIBLE);
|
||||
_fabMenu.animate()
|
||||
.translationY(0)
|
||||
.setInterpolator(new DecelerateInterpolator(2))
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
_isAnimating = false;
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
public class MetricsHelper {
|
||||
private MetricsHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static int convertDpToPixels(Context context, float dp) {
|
||||
return (int) (dp * (context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class PasswordStrengthHelper {
|
||||
// Material design color palette
|
||||
private static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
|
||||
|
||||
public static String getString(int score, Context context) {
|
||||
if (score < 0 || score > 4) {
|
||||
throw new IllegalArgumentException("Not a valid zxcvbn score");
|
||||
}
|
||||
|
||||
String[] strings = context.getResources().getStringArray(R.array.password_strength);
|
||||
return strings[score];
|
||||
}
|
||||
|
||||
public static String getColor(int score) {
|
||||
if (score < 0 || score > 4) {
|
||||
throw new IllegalArgumentException("Not a valid zxcvbn score");
|
||||
}
|
||||
|
||||
return COLORS[score];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.ImageAnalysis;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.ChecksumException;
|
||||
import com.google.zxing.FormatException;
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.google.zxing.PlanarYUVLuminanceSource;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.google.zxing.qrcode.QRCodeReader;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static android.graphics.ImageFormat.YUV_420_888;
|
||||
import static android.graphics.ImageFormat.YUV_422_888;
|
||||
import static android.graphics.ImageFormat.YUV_444_888;
|
||||
|
||||
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
|
||||
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
|
||||
|
||||
private final QrCodeAnalyzer.Listener _listener;
|
||||
|
||||
public QrCodeAnalyzer(QrCodeAnalyzer.Listener listener) {
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void analyze(@NonNull ImageProxy image) {
|
||||
int format = image.getFormat();
|
||||
if (format != YUV_420_888 && format != YUV_422_888 && format != YUV_444_888) {
|
||||
Log.e(TAG, String.format("Expected YUV format, got %d instead", format));
|
||||
image.close();
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer buf = image.getPlanes()[0].getBuffer();
|
||||
byte[] data = new byte[buf.remaining()];
|
||||
buf.get(data);
|
||||
buf.rewind();
|
||||
|
||||
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
|
||||
data, image.getWidth(), image.getHeight(), 0, 0, image.getWidth(), image.getHeight(), false
|
||||
);
|
||||
|
||||
QRCodeReader reader = new QRCodeReader();
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
try {
|
||||
Result result = reader.decode(bitmap);
|
||||
if (_listener != null) {
|
||||
_listener.onQrCodeDetected(result);
|
||||
}
|
||||
} catch (ChecksumException | FormatException | NotFoundException ignored) {
|
||||
|
||||
} finally {
|
||||
image.close();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onQrCodeDetected(Result result);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -43,7 +43,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
EntryAdapter adapter = (EntryAdapter)recyclerView.getAdapter();
|
||||
if (adapter.getEntryAt(position) != _selectedEntry)
|
||||
if (adapter.getEntryAt(position) != _selectedEntry || !isLongPressDragEnabled())
|
||||
{
|
||||
dragFlags = 0;
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.beemdevelopment.aegis.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import me.dm7.barcodescanner.core.ViewFinderView;
|
||||
|
||||
public class SquareFinderView extends ViewFinderView {
|
||||
|
||||
public SquareFinderView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public SquareFinderView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setSquareViewFinder(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,20 @@ import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class AegisImporter extends DatabaseImporter {
|
||||
|
||||
@@ -23,19 +26,14 @@ public class AegisImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] bytes = reader.readAll();
|
||||
byte[] bytes = IOUtils.readAll(stream);
|
||||
VaultFile file = VaultFile.fromBytes(bytes);
|
||||
if (file.isEncrypted()) {
|
||||
return new EncryptedState(file);
|
||||
|
||||
@@ -3,26 +3,32 @@ package com.beemdevelopment.aegis.importers;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
import com.beemdevelopment.aegis.crypto.CryptResult;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
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;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
@@ -49,27 +55,20 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
private static final int SALT_SIZE = 12;
|
||||
private static final int KEY_SIZE = 256; // bits
|
||||
|
||||
private static final int MAX_ITERATIONS = 10000;
|
||||
|
||||
public AndOtpImporter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
byte[] bytes;
|
||||
try {
|
||||
bytes = reader.readAll();
|
||||
bytes = IOUtils.readAll(stream);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
@@ -96,38 +95,12 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
_data = data;
|
||||
}
|
||||
|
||||
private DecryptedState decrypt(char[] password, boolean oldFormat) throws DatabaseImporterException {
|
||||
private DecryptedState decryptData(SecretKey key, int offset) throws DatabaseImporterException {
|
||||
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
|
||||
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
|
||||
CryptParameters params = new CryptParameters(nonce, tag);
|
||||
|
||||
try {
|
||||
SecretKey key;
|
||||
int offset = 0;
|
||||
|
||||
if (oldFormat) {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this exists solely to support the old andOTP backup format
|
||||
// it is not a secure way to derive a key from a password
|
||||
MessageDigest hash = MessageDigest.getInstance("SHA-256");
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
key = new SecretKeySpec(keyBytes, "AES");
|
||||
} else {
|
||||
offset = INT_SIZE + SALT_SIZE;
|
||||
|
||||
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
|
||||
int iterations = ByteBuffer.wrap(iterBytes).getInt();
|
||||
if (iterations < 1 || iterations > MAX_ITERATIONS) {
|
||||
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
|
||||
}
|
||||
|
||||
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(password, salt, iterations, KEY_SIZE);
|
||||
key = factory.generateSecret(spec);
|
||||
}
|
||||
|
||||
// extract nonce and tag
|
||||
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
|
||||
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
|
||||
CryptParameters params = new CryptParameters(nonce, tag);
|
||||
|
||||
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
|
||||
int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
|
||||
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
|
||||
@@ -137,13 +110,51 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
} catch (NoSuchAlgorithmException
|
||||
| InvalidAlgorithmParameterException
|
||||
| InvalidKeyException
|
||||
| InvalidKeySpecException
|
||||
| NoSuchPaddingException
|
||||
| IllegalBlockSizeException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
|
||||
if (oldFormat) {
|
||||
// WARNING: DON'T DO THIS IN YOUR OWN CODE
|
||||
// this exists solely to support the old andOTP backup format
|
||||
// it is not a secure way to derive a key from a password
|
||||
MessageDigest hash;
|
||||
try {
|
||||
hash = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
|
||||
SecretKey key = new SecretKeySpec(keyBytes, "AES");
|
||||
DecryptedState state = decryptData(key, 0);
|
||||
listener.onStateDecrypted(state);
|
||||
} else {
|
||||
int offset = INT_SIZE + SALT_SIZE;
|
||||
|
||||
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
|
||||
int iterations = ByteBuffer.wrap(iterBytes).getInt();
|
||||
if (iterations < 1) {
|
||||
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
|
||||
}
|
||||
|
||||
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
|
||||
AndOtpKeyDerivationTask.Params params = new AndOtpKeyDerivationTask.Params(password, salt, iterations);
|
||||
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key1 -> {
|
||||
try {
|
||||
DecryptedState state = decryptData(key1, offset);
|
||||
listener.onStateDecrypted(state);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
});
|
||||
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
|
||||
task.execute(lifecycle, params);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decrypt(Context context, DecryptListener listener) {
|
||||
String[] choices = new String[]{
|
||||
@@ -158,8 +169,7 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
Dialogs.showPasswordInputDialog(context, password -> {
|
||||
try {
|
||||
DecryptedState state = decrypt(password, i != 0);
|
||||
listener.onStateDecrypted(state);
|
||||
decrypt(context, password, i != 0, listener);
|
||||
} catch (DatabaseImporterException e) {
|
||||
listener.onError(e);
|
||||
}
|
||||
@@ -221,12 +231,17 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
String name;
|
||||
String issuer = "";
|
||||
|
||||
String[] parts = obj.getString("label").split(" - ");
|
||||
if (parts.length > 1) {
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
if (obj.has("issuer")) {
|
||||
name = obj.getString("label");
|
||||
issuer = obj.getString("issuer");
|
||||
} else {
|
||||
name = parts[0];
|
||||
String[] parts = obj.getString("label").split(" - ");
|
||||
if (parts.length > 1) {
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
} else {
|
||||
name = parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
return new VaultEntry(info, name, issuer);
|
||||
@@ -235,4 +250,64 @@ public class AndOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpKeyDerivationTask.Params, SecretKey> {
|
||||
private Callback _cb;
|
||||
|
||||
public AndOtpKeyDerivationTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.unlocking_vault));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SecretKey doInBackground(AndOtpKeyDerivationTask.Params... args) {
|
||||
setPriority();
|
||||
|
||||
AndOtpKeyDerivationTask.Params params = args[0];
|
||||
SecretKey key;
|
||||
try {
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
|
||||
key = factory.generateSecret(spec);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(SecretKey key) {
|
||||
super.onPostExecute(key);
|
||||
_cb.onTaskFinished(key);
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
private char[] _password;
|
||||
private byte[] _salt;
|
||||
private int _iterations;
|
||||
|
||||
public Params(char[] password, byte[] salt, int iterations) {
|
||||
_iterations = iterations;
|
||||
_password = password;
|
||||
_salt = salt;
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return _password;
|
||||
}
|
||||
|
||||
public int getIterations() {
|
||||
return _iterations;
|
||||
}
|
||||
|
||||
public byte[] getSalt() {
|
||||
return _salt;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(SecretKey key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-10
@@ -3,6 +3,8 @@ package com.beemdevelopment.aegis.importers;
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import net.lingala.zip4j.io.inputstream.ZipInputStream;
|
||||
import net.lingala.zip4j.model.LocalFileHeader;
|
||||
@@ -11,6 +13,7 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class AuthenticatorPlusImporter extends DatabaseImporter {
|
||||
private static final String FILENAME = "Accounts.txt";
|
||||
@@ -20,19 +23,14 @@ public class AuthenticatorPlusImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
return new EncryptedState(reader.readAll());
|
||||
return new EncryptedState(IOUtils.readAll(stream));
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
@@ -55,9 +53,8 @@ public class AuthenticatorPlusImporter extends DatabaseImporter {
|
||||
while ((header = zipStream.getNextEntry()) != null) {
|
||||
File file = new File(header.getFileName());
|
||||
if (file.getName().equals(FILENAME)) {
|
||||
FileReader reader = new FileReader(zipStream);
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(context);
|
||||
GoogleAuthUriImporter.State state = importer.read(reader);
|
||||
DatabaseImporter.State state = importer.read(zipStream);
|
||||
listener.onStateDecrypted(state);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.JsonUtils;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -21,6 +26,7 @@ import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
@@ -38,8 +44,10 @@ import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
|
||||
public class AuthyImporter extends DatabaseImporter {
|
||||
private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml";
|
||||
private static final String _subPath = "shared_prefs";
|
||||
private static final String _pkgName = "com.authy.authy";
|
||||
private static final String _authFilename = "com.authy.storage.tokens.authenticator";
|
||||
private static final String _authyFilename = "com.authy.storage.tokens.authy";
|
||||
|
||||
private static final int ITERATIONS = 1000;
|
||||
private static final int KEY_SIZE = 256;
|
||||
@@ -53,42 +61,91 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
public State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile path = getAppPath();
|
||||
|
||||
JSONArray array;
|
||||
JSONArray authyArray;
|
||||
try {
|
||||
array = readFile(new SuFile(path, String.format("%s.xml", _authFilename)), String.format("%s.key", _authFilename));
|
||||
authyArray = readFile(new SuFile(path, String.format("%s.xml", _authyFilename)), String.format("%s.key", _authyFilename));
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < authyArray.length(); i++) {
|
||||
array.put(authyArray.getJSONObject(i));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return read(array);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(reader.getStream(), null);
|
||||
parser.setInput(stream, null);
|
||||
parser.nextTag();
|
||||
|
||||
JSONArray array = new JSONArray();
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (entry.Name.equals("com.authy.storage.tokens.authenticator.key")) {
|
||||
if (entry.Name.equals(String.format("%s.key", _authFilename))
|
||||
|| entry.Name.equals(String.format("%s.key", _authyFilename))) {
|
||||
array = new JSONArray(entry.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
if (!array.getJSONObject(i).has("decryptedSecret")) {
|
||||
return new EncryptedState(array);
|
||||
}
|
||||
}
|
||||
|
||||
return new DecryptedState(array);
|
||||
return read(array);
|
||||
} catch (XmlPullParserException | JSONException | IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private State read(JSONArray array) throws DatabaseImporterException {
|
||||
try {
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject obj = array.getJSONObject(i);
|
||||
if (!obj.has("decryptedSecret") && !obj.has("secretSeed")) {
|
||||
return new EncryptedState(array);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return new DecryptedState(array);
|
||||
}
|
||||
|
||||
private JSONArray readFile(SuFile file, String key) throws IOException, XmlPullParserException {
|
||||
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(inStream, null);
|
||||
parser.nextTag();
|
||||
|
||||
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
|
||||
if (entry.Name.equals(key)) {
|
||||
return new JSONArray(entry.Value);
|
||||
}
|
||||
}
|
||||
} catch (JSONException ignored) {
|
||||
|
||||
}
|
||||
|
||||
return new JSONArray();
|
||||
}
|
||||
|
||||
public static class EncryptedState extends DatabaseImporter.State {
|
||||
private JSONArray _array;
|
||||
|
||||
@@ -103,7 +160,7 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
try {
|
||||
for (int i = 0; i < _array.length(); i++) {
|
||||
JSONObject obj = _array.getJSONObject(i);
|
||||
String secretString = obj.optString("encryptedSecret", null);
|
||||
String secretString = JsonUtils.optString(obj, "encryptedSecret");
|
||||
if (secretString == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -173,40 +230,50 @@ public class AuthyImporter extends DatabaseImporter {
|
||||
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
|
||||
authyEntryInfo.OriginalName = entry.optString("originalName", null);
|
||||
authyEntryInfo.OriginalIssuer = entry.optString("originalIssuer", null);
|
||||
authyEntryInfo.AccountType = entry.getString("accountType");
|
||||
authyEntryInfo.OriginalName = JsonUtils.optString(entry, "originalName");
|
||||
authyEntryInfo.OriginalIssuer = JsonUtils.optString(entry, "originalIssuer");
|
||||
authyEntryInfo.AccountType = JsonUtils.optString(entry, "accountType");
|
||||
authyEntryInfo.Name = entry.optString("name");
|
||||
|
||||
sanitizeEntryInfo(authyEntryInfo);
|
||||
boolean isAuthy = entry.has("secretSeed");
|
||||
sanitizeEntryInfo(authyEntryInfo, isAuthy);
|
||||
|
||||
byte[] secret;
|
||||
if (isAuthy) {
|
||||
secret = Hex.decode(entry.getString("secretSeed"));
|
||||
} else {
|
||||
secret = Base32.decode(entry.getString("decryptedSecret"));
|
||||
}
|
||||
|
||||
int digits = entry.getInt("digits");
|
||||
byte[] secret = Base32.decode(entry.getString("decryptedSecret"));
|
||||
|
||||
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
|
||||
|
||||
OtpInfo info = new TotpInfo(secret, "SHA1", digits, isAuthy ? 10 : 30);
|
||||
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
|
||||
} catch (OtpInfoException | JSONException | EncodingException e) {
|
||||
throw new DatabaseImporterEntryException(e, entry.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void sanitizeEntryInfo(AuthyEntryInfo info) {
|
||||
String separator = "";
|
||||
private static void sanitizeEntryInfo(AuthyEntryInfo info, boolean isAuthy) {
|
||||
if (!isAuthy) {
|
||||
String separator = "";
|
||||
|
||||
if (info.OriginalIssuer != null) {
|
||||
info.Issuer = info.OriginalIssuer;
|
||||
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
|
||||
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
|
||||
separator = ":";
|
||||
} else if (info.Name.contains(" - ")) {
|
||||
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
|
||||
separator = " - ";
|
||||
if (info.OriginalIssuer != null) {
|
||||
info.Issuer = info.OriginalIssuer;
|
||||
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
|
||||
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
|
||||
separator = ":";
|
||||
} else if (info.Name.contains(" - ")) {
|
||||
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
|
||||
separator = " - ";
|
||||
} else {
|
||||
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
|
||||
}
|
||||
|
||||
info.Name = info.Name.replace(info.Issuer + separator, "");
|
||||
} else {
|
||||
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
|
||||
info.Issuer = info.Name;
|
||||
info.Name = "";
|
||||
}
|
||||
|
||||
info.Name = info.Name.replace(info.Issuer + separator, "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,50 +3,42 @@ package com.beemdevelopment.aegis.importers;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public abstract class DatabaseImporter {
|
||||
private Context _context;
|
||||
|
||||
private static Map<String, Class<? extends DatabaseImporter>> _importers;
|
||||
private static Map<String, Class<? extends DatabaseImporter>> _appImporters;
|
||||
private static List<Definition> _importers;
|
||||
|
||||
static {
|
||||
// note: keep these lists sorted alphabetically
|
||||
_importers = new LinkedHashMap<>();
|
||||
_importers.put("Aegis", AegisImporter.class);
|
||||
_importers.put("Authenticator Plus", AuthenticatorPlusImporter.class);
|
||||
_importers.put("Authy", AuthyImporter.class);
|
||||
_importers.put("andOTP", AndOtpImporter.class);
|
||||
_importers.put("FreeOTP", FreeOtpImporter.class);
|
||||
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
|
||||
_importers.put("Google Authenticator", GoogleAuthImporter.class);
|
||||
_importers.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
|
||||
_importers.put("Plain text", GoogleAuthUriImporter.class);
|
||||
_importers.put("Steam", SteamImporter.class);
|
||||
_importers.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
|
||||
_importers.put("WinAuth", WinAuthImporter.class);
|
||||
|
||||
_appImporters = new LinkedHashMap<>();
|
||||
_appImporters.put("Authy", AuthyImporter.class);
|
||||
_appImporters.put("FreeOTP", FreeOtpImporter.class);
|
||||
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
|
||||
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
|
||||
_appImporters.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
|
||||
_appImporters.put("Steam", SteamImporter.class);
|
||||
_appImporters.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
|
||||
_importers = new ArrayList<>();
|
||||
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
|
||||
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
|
||||
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
|
||||
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
|
||||
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
|
||||
_importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
|
||||
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
|
||||
_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("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
|
||||
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
|
||||
}
|
||||
|
||||
public DatabaseImporter(Context context) {
|
||||
@@ -57,20 +49,27 @@ public abstract class DatabaseImporter {
|
||||
return _context;
|
||||
}
|
||||
|
||||
public SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
|
||||
return getAppPath(getAppPkgName(), getAppSubPath());
|
||||
}
|
||||
protected abstract SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
|
||||
|
||||
protected SuFile getAppPath(String pkgName, String subPath) throws PackageManager.NameNotFoundException {
|
||||
PackageManager man = getContext().getPackageManager();
|
||||
return new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
|
||||
}
|
||||
|
||||
protected abstract String getAppPkgName();
|
||||
protected abstract State read(InputStream stream, boolean isInternal) throws DatabaseImporterException;
|
||||
|
||||
protected abstract String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
|
||||
public State read(InputStream stream) throws DatabaseImporterException {
|
||||
return read(stream, false);
|
||||
}
|
||||
|
||||
public abstract State read(FileReader reader) throws DatabaseImporterException;
|
||||
public State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile file = getAppPath();
|
||||
try (SuFileInputStream stream = new SuFileInputStream(file)) {
|
||||
return read(stream, true);
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static DatabaseImporter create(Context context, Class<? extends DatabaseImporter> type) {
|
||||
try {
|
||||
@@ -81,12 +80,42 @@ public abstract class DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Class<? extends DatabaseImporter>> getImporters() {
|
||||
return Collections.unmodifiableMap(_importers);
|
||||
public static List<Definition> getImporters(boolean isDirect) {
|
||||
if (isDirect) {
|
||||
return Collections.unmodifiableList(_importers.stream().filter(Definition::supportsDirect).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(_importers);
|
||||
}
|
||||
|
||||
public static Map<String, Class<? extends DatabaseImporter>> getAppImporters() {
|
||||
return Collections.unmodifiableMap(_appImporters);
|
||||
public static class Definition {
|
||||
private final String _name;
|
||||
private final Class<? extends DatabaseImporter> _type;
|
||||
private final @StringRes int _help;
|
||||
private final boolean _supportsDirect;
|
||||
|
||||
public Definition(String name, Class<? extends DatabaseImporter> type, @StringRes int help, boolean supportsDirect) {
|
||||
_name = name;
|
||||
_type = type;
|
||||
_help = help;
|
||||
_supportsDirect = supportsDirect;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
public Class<? extends DatabaseImporter> getType() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
public @StringRes int getHelp() {
|
||||
return _help;
|
||||
}
|
||||
|
||||
public boolean supportsDirect() {
|
||||
return _supportsDirect;
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class State {
|
||||
@@ -138,36 +167,6 @@ public abstract class DatabaseImporter {
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileReader {
|
||||
private InputStream _stream;
|
||||
private boolean _internal;
|
||||
|
||||
public FileReader(InputStream stream) {
|
||||
this(stream, false);
|
||||
}
|
||||
|
||||
public FileReader(InputStream stream, boolean internal) {
|
||||
_stream = stream;
|
||||
_internal = internal;
|
||||
}
|
||||
|
||||
public byte[] readAll() throws IOException {
|
||||
return IOUtils.readAll(_stream);
|
||||
}
|
||||
|
||||
public InputStream getStream() {
|
||||
return _stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this reader reads the internal state of an app.
|
||||
* @return true if reading from internal file, false if reading from external file
|
||||
*/
|
||||
public boolean isInternal() {
|
||||
return _internal;
|
||||
}
|
||||
}
|
||||
|
||||
public interface DecryptListener {
|
||||
void onStateDecrypted(State state);
|
||||
void onError(Exception e);
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
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.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -18,6 +20,7 @@ import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -30,21 +33,16 @@ public class FreeOtpImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(reader.getStream(), null);
|
||||
parser.setInput(stream, null);
|
||||
parser.nextTag();
|
||||
|
||||
List<JSONObject> entries = new ArrayList<>();
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -20,24 +25,19 @@ public class FreeOtpPlusImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
State state;
|
||||
|
||||
if (reader.isInternal()) {
|
||||
state = new FreeOtpImporter(getContext()).read(reader);
|
||||
if (isInternal) {
|
||||
state = new FreeOtpImporter(getContext()).read(stream);
|
||||
} else {
|
||||
try {
|
||||
String json = new String(reader.readAll(), StandardCharsets.UTF_8);
|
||||
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
|
||||
JSONObject obj = new JSONObject(json);
|
||||
JSONArray array = obj.getJSONArray("tokens");
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class GoogleAuthImporter extends DatabaseImporter {
|
||||
@@ -25,19 +28,22 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, reader.getStream(), "accounts");
|
||||
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile path = getAppPath();
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, path, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
@@ -67,7 +73,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
|
||||
|
||||
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
|
||||
try {
|
||||
byte[] secret = Base32.decode(entry.getSecret());
|
||||
byte[] secret = GoogleAuthInfo.parseSecret(entry.getSecret());
|
||||
|
||||
OtpInfo info;
|
||||
switch (entry.getType()) {
|
||||
|
||||
@@ -5,9 +5,11 @@ import android.content.Context;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -17,20 +19,15 @@ public class GoogleAuthUriImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return null;
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GoogleAuthUriImporter.State read(DatabaseImporter.FileReader reader) throws DatabaseImporterException {
|
||||
public GoogleAuthUriImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
ArrayList<String> lines = new ArrayList<>();
|
||||
|
||||
try (InputStreamReader streamReader = new InputStreamReader(reader.getStream());
|
||||
try (InputStreamReader streamReader = new InputStreamReader(stream);
|
||||
BufferedReader bufferedReader = new BufferedReader(streamReader)) {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
@@ -10,7 +11,9 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class MicrosoftAuthImporter extends DatabaseImporter {
|
||||
@@ -25,19 +28,22 @@ public class MicrosoftAuthImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, reader.getStream(), "accounts");
|
||||
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
|
||||
SuFile path = getAppPath();
|
||||
SqlImporterHelper helper = new SqlImporterHelper(getContext());
|
||||
List<Entry> entries = helper.read(Entry.class, path, "accounts");
|
||||
return new State(entries);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.google.common.io.Files;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -24,19 +27,75 @@ public class SqlImporterHelper {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
|
||||
File file;
|
||||
public <T extends Entry> List<T> read(Class<T> type, SuFile path, String table) throws DatabaseImporterException {
|
||||
File dir = Files.createTempDir();
|
||||
File mainFile = new File(dir, path.getName());
|
||||
|
||||
List<File> fileCopies = new ArrayList<>();
|
||||
for (SuFile file : SqlImporterHelper.findDatabaseFiles(path)) {
|
||||
// create temporary copies of the database files so that SQLiteDatabase can open them
|
||||
File fileCopy = null;
|
||||
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
|
||||
fileCopy = new File(dir, file.getName());
|
||||
try (FileOutputStream out = new FileOutputStream(fileCopy)) {
|
||||
IOUtils.copy(inStream, out);
|
||||
}
|
||||
fileCopies.add(fileCopy);
|
||||
} catch (IOException e) {
|
||||
if (fileCopy != null) {
|
||||
fileCopy.delete();
|
||||
}
|
||||
|
||||
for (File fileCopy2 : fileCopies) {
|
||||
fileCopy2.delete();
|
||||
}
|
||||
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return read(type, mainFile, table);
|
||||
} finally {
|
||||
for (File fileCopy : fileCopies) {
|
||||
fileCopy.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SuFile[] findDatabaseFiles(SuFile path) throws DatabaseImporterException {
|
||||
SuFile[] files = path.getParentFile().listFiles((d, name) -> name.startsWith(path.getName()));
|
||||
if (files == null || files.length == 0) {
|
||||
throw new DatabaseImporterException(String.format("File does not exist: %s", path.getAbsolutePath()));
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
|
||||
File file = null;
|
||||
try {
|
||||
// create a temporary copy of the database so that SQLiteDatabase can open it
|
||||
file = File.createTempFile("db-import-", "", _context.getCacheDir());
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
ShellUtils.pump(inStream, out);
|
||||
IOUtils.copy(inStream, out);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (file != null) {
|
||||
file.delete();
|
||||
}
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
return read(type, file, table);
|
||||
} finally {
|
||||
// always delete the temporary file
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Entry> List<T> read(Class<T> type, File file, String table) throws DatabaseImporterException {
|
||||
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY)) {
|
||||
try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) {
|
||||
List<T> entries = new ArrayList<>();
|
||||
@@ -55,9 +114,6 @@ public class SqlImporterHelper {
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} finally {
|
||||
// always delete the temporary file
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
@@ -14,6 +15,7 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class SteamImporter extends DatabaseImporter {
|
||||
@@ -25,27 +27,22 @@ public class SteamImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
|
||||
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
|
||||
// NOTE: this assumes that a global root shell has already been obtained by the caller
|
||||
SuFile path = getAppPath(getAppPkgName(), _subDir);
|
||||
SuFile path = getAppPath(_pkgName, _subDir);
|
||||
SuFile[] files = path.listFiles((d, name) -> name.startsWith("Steamguard-"));
|
||||
if (files == null || files.length == 0) {
|
||||
throw new DatabaseImporterException(String.format("Empty directory: %s", path.getAbsolutePath()));
|
||||
}
|
||||
|
||||
// TODO: handle multiple files (can this even occur?)
|
||||
return new SuFile(_subDir, files[0].getName()).getPath();
|
||||
return files[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
byte[] bytes = reader.readAll();
|
||||
byte[] bytes = IOUtils.readAll(stream);
|
||||
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
||||
return new State(obj);
|
||||
} catch (IOException | JSONException e) {
|
||||
|
||||
+10
-11
@@ -1,6 +1,7 @@
|
||||
package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@@ -14,8 +15,10 @@ import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.util.PreferenceParser;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@@ -24,6 +27,7 @@ import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
@@ -59,22 +63,17 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return _pkgName;
|
||||
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
|
||||
return getAppPath(_pkgName, _subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return _subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
try {
|
||||
if (reader.isInternal()) {
|
||||
if (isInternal) {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(reader.getStream(), null);
|
||||
parser.setInput(stream, null);
|
||||
parser.nextTag();
|
||||
|
||||
String data = null;
|
||||
@@ -91,7 +90,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
|
||||
List<JSONObject> entries = parse(data);
|
||||
return new DecryptedState(entries);
|
||||
} else {
|
||||
byte[] base64 = reader.readAll();
|
||||
byte[] base64 = IOUtils.readAll(stream);
|
||||
byte[] cipherText = Base64.decode(base64);
|
||||
return new EncryptedState(cipherText);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.beemdevelopment.aegis.importers;
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class WinAuthImporter extends DatabaseImporter {
|
||||
public WinAuthImporter(Context context) {
|
||||
@@ -10,32 +13,27 @@ public class WinAuthImporter extends DatabaseImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppPkgName() {
|
||||
return null;
|
||||
protected SuFile getAppPath() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAppSubPath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WinAuthImporter.State read(FileReader reader) throws DatabaseImporterException {
|
||||
public WinAuthImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
|
||||
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getContext());
|
||||
GoogleAuthUriImporter.State state = importer.read(reader);
|
||||
DatabaseImporter.State state = importer.read(stream);
|
||||
return new State(state);
|
||||
}
|
||||
|
||||
public static class State extends DatabaseImporter.State {
|
||||
private GoogleAuthUriImporter.State _state;
|
||||
private DatabaseImporter.State _state;
|
||||
|
||||
private State(GoogleAuthUriImporter.State state) {
|
||||
private State(DatabaseImporter.State state) {
|
||||
super(false);
|
||||
_state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result convert() {
|
||||
public Result convert() throws DatabaseImporterException {
|
||||
Result result = _state.convert();
|
||||
|
||||
for (VaultEntry entry : result.getEntries()) {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.beemdevelopment.aegis.ui.glide;
|
||||
package com.beemdevelopment.aegis.licenses;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.beemdevelopment.aegis.licenses;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import de.psdev.licensesdialog.licenses.License;
|
||||
|
||||
public class ProtobufLicense extends License {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Protocol Buffers License";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readSummaryTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.protobuf_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readFullTextFromResources(Context context) {
|
||||
return getContent(context, R.raw.protobuf_license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return "https://raw.githubusercontent.com/protocolbuffers/protobuf/master/LICENSE";
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,11 @@ import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GoogleAuthInfo {
|
||||
public class GoogleAuthInfo implements Serializable {
|
||||
public static final String SCHEME = "otpauth";
|
||||
public static final String SCHEME_EXPORT = "otpauth-migration";
|
||||
|
||||
@@ -49,7 +50,7 @@ public class GoogleAuthInfo {
|
||||
|
||||
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
|
||||
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
|
||||
builder.appendQueryParameter("secret", new String(Base32.encode(_info.getSecret())));
|
||||
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
|
||||
|
||||
if (_issuer != null && !_issuer.equals("")) {
|
||||
builder.path(String.format("%s:%s", _issuer, _accountName));
|
||||
@@ -64,7 +65,7 @@ public class GoogleAuthInfo {
|
||||
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException(String.format("Bad URI format: %s", s));
|
||||
throw new GoogleAuthInfoException(uri, String.format("Bad URI format: %s", s));
|
||||
}
|
||||
return GoogleAuthInfo.parseUri(uri);
|
||||
}
|
||||
@@ -72,29 +73,27 @@ public class GoogleAuthInfo {
|
||||
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals(SCHEME)) {
|
||||
throw new GoogleAuthInfoException("Unsupported protocol");
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported protocol: %s", scheme));
|
||||
}
|
||||
|
||||
// 'secret' is a required parameter
|
||||
String encodedSecret = uri.getQueryParameter("secret");
|
||||
if (encodedSecret == null) {
|
||||
throw new GoogleAuthInfoException("Parameter 'secret' is not present");
|
||||
throw new GoogleAuthInfoException(uri, "Parameter 'secret' is not present");
|
||||
}
|
||||
|
||||
// decode secret
|
||||
byte[] secret;
|
||||
try {
|
||||
secret = Base32.decode(encodedSecret);
|
||||
secret = parseSecret(encodedSecret);
|
||||
} catch (EncodingException e) {
|
||||
throw new GoogleAuthInfoException("Bad secret", e);
|
||||
throw new GoogleAuthInfoException(uri, "Bad secret", e);
|
||||
}
|
||||
|
||||
// check the otp type
|
||||
OtpInfo info;
|
||||
try {
|
||||
String type = uri.getHost();
|
||||
if (type == null) {
|
||||
throw new GoogleAuthInfoException(String.format("Host not present in URI: %s", uri.toString()));
|
||||
throw new GoogleAuthInfoException(uri, String.format("Host not present in URI: %s", uri.toString()));
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
@@ -118,16 +117,16 @@ public class GoogleAuthInfo {
|
||||
HotpInfo hotpInfo = new HotpInfo(secret);
|
||||
String counter = uri.getQueryParameter("counter");
|
||||
if (counter == null) {
|
||||
throw new GoogleAuthInfoException("Parameter 'counter' is not present");
|
||||
throw new GoogleAuthInfoException(uri, "Parameter 'counter' is not present");
|
||||
}
|
||||
hotpInfo.setCounter(Long.parseLong(counter));
|
||||
info = hotpInfo;
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(String.format("Unsupported OTP type: %s", type));
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported OTP type: %s", type));
|
||||
}
|
||||
} catch (OtpInfoException | NumberFormatException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
// provider info used to disambiguate accounts
|
||||
@@ -167,16 +166,24 @@ public class GoogleAuthInfo {
|
||||
info.setDigits(Integer.parseInt(digits));
|
||||
}
|
||||
} catch (OtpInfoException | NumberFormatException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
return new GoogleAuthInfo(info, accountName, issuer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given base 32 secret, while being tolerant of whitespace and dashes.
|
||||
*/
|
||||
public static byte[] parseSecret(String s) throws EncodingException {
|
||||
s = s.trim().replace("-", "").replace(" ", "");
|
||||
return Base32.decode(s);
|
||||
}
|
||||
|
||||
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException("Bad URI format");
|
||||
throw new GoogleAuthInfoException(uri, "Bad URI format");
|
||||
}
|
||||
return GoogleAuthInfo.parseExportUri(uri);
|
||||
}
|
||||
@@ -184,17 +191,17 @@ public class GoogleAuthInfo {
|
||||
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
|
||||
throw new GoogleAuthInfoException("Unsupported protocol");
|
||||
throw new GoogleAuthInfoException(uri, "Unsupported protocol");
|
||||
}
|
||||
|
||||
String host = uri.getHost();
|
||||
if (host == null || !host.equals("offline")) {
|
||||
throw new GoogleAuthInfoException("Unsupported host");
|
||||
throw new GoogleAuthInfoException(uri, "Unsupported host");
|
||||
}
|
||||
|
||||
String data = uri.getQueryParameter("data");
|
||||
if (data == null) {
|
||||
throw new GoogleAuthInfoException("Parameter 'data' is not set");
|
||||
throw new GoogleAuthInfoException(uri, "Parameter 'data' is not set");
|
||||
}
|
||||
|
||||
GoogleAuthProtos.MigrationPayload payload;
|
||||
@@ -202,26 +209,59 @@ public class GoogleAuthInfo {
|
||||
byte[] bytes = Base64.decode(data);
|
||||
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
|
||||
} catch (EncodingException | InvalidProtocolBufferException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
List<GoogleAuthInfo> infos = new ArrayList<>();
|
||||
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
|
||||
OtpInfo otp;
|
||||
try {
|
||||
byte[] secret = params.getSecret().toByteArray();
|
||||
switch (params.getType()) {
|
||||
case OTP_HOTP:
|
||||
otp = new HotpInfo(secret, params.getCounter());
|
||||
int digits;
|
||||
switch (params.getDigits()) {
|
||||
case DIGIT_COUNT_UNSPECIFIED:
|
||||
// intentional fallthrough
|
||||
case DIGIT_COUNT_SIX:
|
||||
digits = 6;
|
||||
break;
|
||||
case OTP_TOTP:
|
||||
otp = new TotpInfo(secret);
|
||||
case DIGIT_COUNT_EIGHT:
|
||||
digits = 8;
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(String.format("Unsupported algorithm: %d", params.getType().ordinal()));
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported digits: %d", params.getDigits().ordinal()));
|
||||
}
|
||||
|
||||
String algo;
|
||||
switch (params.getAlgorithm()) {
|
||||
case ALGORITHM_UNSPECIFIED:
|
||||
// intentional fallthrough
|
||||
case ALGORITHM_SHA1:
|
||||
algo = "SHA1";
|
||||
break;
|
||||
case ALGORITHM_SHA256:
|
||||
algo = "SHA256";
|
||||
break;
|
||||
case ALGORITHM_SHA512:
|
||||
algo = "SHA512";
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported hash algorithm: %d", params.getAlgorithm().ordinal()));
|
||||
}
|
||||
|
||||
byte[] secret = params.getSecret().toByteArray();
|
||||
switch (params.getType()) {
|
||||
case OTP_TYPE_UNSPECIFIED:
|
||||
// intentional fallthrough
|
||||
case OTP_TYPE_TOTP:
|
||||
otp = new TotpInfo(secret, algo, digits, 30);
|
||||
break;
|
||||
case OTP_TYPE_HOTP:
|
||||
otp = new HotpInfo(secret, algo, digits, params.getCounter());
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported algorithm: %d", params.getType().ordinal()));
|
||||
}
|
||||
} catch (OtpInfoException e){
|
||||
throw new GoogleAuthInfoException(e);
|
||||
throw new GoogleAuthInfoException(uri, e);
|
||||
}
|
||||
|
||||
String name = params.getName();
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
package com.beemdevelopment.aegis.otp;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
public class GoogleAuthInfoException extends Exception {
|
||||
public GoogleAuthInfoException(Throwable cause) {
|
||||
private final Uri _uri;
|
||||
|
||||
public GoogleAuthInfoException(Uri uri, Throwable cause) {
|
||||
super(cause);
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
public GoogleAuthInfoException(String message) {
|
||||
public GoogleAuthInfoException(Uri uri, String message) {
|
||||
super(message);
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
public GoogleAuthInfoException(String message, Throwable cause) {
|
||||
public GoogleAuthInfoException(Uri uri, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether the scheme of the URI is phonefactor://.
|
||||
*/
|
||||
public boolean isPhoneFactor() {
|
||||
return _uri != null && _uri.getScheme() != null && _uri.getScheme().equals("phonefactor");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -19,6 +33,7 @@ public class GoogleAuthInfoException extends Exception {
|
||||
if (cause == null) {
|
||||
return super.getMessage();
|
||||
}
|
||||
|
||||
return String.format("%s (%s)", super.getMessage(), cause.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,12 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.view.LayoutInflaterCompat;
|
||||
|
||||
@@ -20,7 +19,8 @@ import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.ui.glide.GlideLicense;
|
||||
import com.beemdevelopment.aegis.licenses.GlideLicense;
|
||||
import com.beemdevelopment.aegis.licenses.ProtobufLicense;
|
||||
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
|
||||
|
||||
import de.psdev.licensesdialog.LicenseResolver;
|
||||
@@ -43,6 +43,11 @@ public class AboutActivity extends AegisActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
View btnLicenses = findViewById(R.id.btn_licenses);
|
||||
btnLicenses.setOnClickListener(v -> showLicenseDialog());
|
||||
|
||||
@@ -74,7 +79,7 @@ public class AboutActivity extends AegisActivity {
|
||||
|
||||
View btnChangelog = findViewById(R.id.btn_changelog);
|
||||
btnChangelog.setOnClickListener(v -> {
|
||||
ChangelogDialog.create().setTheme(getCurrentTheme()).show(getSupportFragmentManager(), "CHANGELOG_DIALOG");
|
||||
ChangelogDialog.create().setTheme(getConfiguredTheme()).show(getSupportFragmentManager(), "CHANGELOG_DIALOG");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -112,7 +117,7 @@ public class AboutActivity extends AegisActivity {
|
||||
|
||||
private void showLicenseDialog() {
|
||||
String stylesheet = getString(R.string.custom_notices_format_style);
|
||||
int backgroundColorResource = getCurrentTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
int backgroundColorResource = getConfiguredTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
|
||||
String backgroundColor = getThemeColorAsHex(backgroundColorResource);
|
||||
String textColor = getThemeColorAsHex(R.attr.primaryText);
|
||||
String licenseColor = getThemeColorAsHex(R.attr.cardBackgroundFocused);
|
||||
@@ -121,6 +126,8 @@ public class AboutActivity extends AegisActivity {
|
||||
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor, linkColor);
|
||||
|
||||
LicenseResolver.registerLicense(new GlideLicense());
|
||||
LicenseResolver.registerLicense(new ProtobufLicense());
|
||||
|
||||
new LicensesDialog.Builder(this)
|
||||
.setNotices(R.raw.notices)
|
||||
.setTitle(R.string.licenses)
|
||||
@@ -133,4 +140,17 @@ public class AboutActivity extends AegisActivity {
|
||||
private String getThemeColorAsHex(@AttrRes int attributeId) {
|
||||
return String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(attributeId, getTheme())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class AegisActivity extends AppCompatActivity implements AegisApplication.LockListener {
|
||||
private boolean _resumed;
|
||||
private AegisApplication _app;
|
||||
private Theme _currentTheme;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -30,15 +30,16 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
|
||||
// set the theme and locale before creating the activity
|
||||
Preferences prefs = getPreferences();
|
||||
setPreferredTheme(prefs.getCurrentTheme());
|
||||
onSetTheme();
|
||||
setLocale(prefs.getLocale());
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// if the app was killed, relaunch MainActivity and close everything else
|
||||
if (savedInstanceState != null && isOrphan()) {
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,9 +48,6 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
// apply a dirty hack to make progress bars work even if animations are disabled
|
||||
setGlobalAnimationDurationScale();
|
||||
|
||||
// register a callback to listen for lock events
|
||||
_app.registerLockListener(this);
|
||||
}
|
||||
@@ -60,25 +58,36 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
_resumed = true;
|
||||
_app.setBlockAutoLock(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
_resumed = false;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onLocked() {
|
||||
if (isOrphan()) {
|
||||
setResult(RESULT_CANCELED, null);
|
||||
finish();
|
||||
public void startActivityForResult(Intent intent, int requestCode, Bundle bundle) {
|
||||
if (isAutoLockBypassedForAction(intent.getAction())) {
|
||||
_app.setBlockAutoLock(true);
|
||||
}
|
||||
|
||||
try {
|
||||
super.startActivityForResult(intent, requestCode, bundle);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
if (isDocsAction(intent.getAction())) {
|
||||
Dialogs.showErrorDialog(this, R.string.documentsui_error, e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocked(boolean userInitiated) {
|
||||
setResult(RESULT_CANCELED, null);
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
protected AegisApplication getApp() {
|
||||
@@ -89,9 +98,26 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
return _app.getPreferences();
|
||||
}
|
||||
|
||||
protected void setPreferredTheme(Theme theme) {
|
||||
/**
|
||||
* Called when the activity is expected to set its theme.
|
||||
*/
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme of the activity. The actual style that is set is picked from the
|
||||
* given map, based on the theme configured by the user.
|
||||
*/
|
||||
protected void setTheme(Map<Theme, Integer> themeMap) {
|
||||
int theme = themeMap.get(getConfiguredTheme());
|
||||
setTheme(theme);
|
||||
}
|
||||
|
||||
protected Theme getConfiguredTheme() {
|
||||
Theme theme = getPreferences().getCurrentTheme();
|
||||
|
||||
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
|
||||
// set the theme based on the system theme
|
||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
switch (currentNightMode) {
|
||||
case Configuration.UI_MODE_NIGHT_NO:
|
||||
@@ -103,19 +129,7 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
}
|
||||
}
|
||||
|
||||
_currentTheme = theme;
|
||||
|
||||
switch (theme) {
|
||||
case LIGHT:
|
||||
setTheme(R.style.AppTheme);
|
||||
break;
|
||||
case DARK:
|
||||
setTheme(R.style.AppTheme_Dark);
|
||||
break;
|
||||
case AMOLED:
|
||||
setTheme(R.style.AppTheme_TrueBlack);
|
||||
break;
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
protected void setLocale(Locale locale) {
|
||||
@@ -127,9 +141,9 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
this.getResources().updateConfiguration(config, this.getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
protected boolean saveVault() {
|
||||
protected boolean saveVault(boolean backup) {
|
||||
try {
|
||||
getApp().getVaultManager().save();
|
||||
getApp().getVaultManager().save(backup);
|
||||
return true;
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
|
||||
@@ -137,33 +151,24 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this Activity has been resumed. (i.e. onResume was called)
|
||||
*/
|
||||
protected boolean isOpen() {
|
||||
return _resumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this Activity instance has become an orphan. This can happen if
|
||||
* the vault was locked by an external trigger while the Activity was still open.
|
||||
*/
|
||||
private boolean isOrphan() {
|
||||
return !(this instanceof MainActivity) && _app.isVaultLocked();
|
||||
protected boolean isOrphan() {
|
||||
return !(this instanceof MainActivity) && !(this instanceof AuthActivity) && !(this instanceof IntroActivity) && _app.isVaultLocked();
|
||||
}
|
||||
|
||||
private void setGlobalAnimationDurationScale() {
|
||||
float durationScale = Settings.Global.getFloat(getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 0);
|
||||
if (durationScale != 1) {
|
||||
try {
|
||||
ValueAnimator.class.getMethod("setDurationScale", float.class).invoke(null, 1f);
|
||||
} catch (Throwable t) {
|
||||
Toast.makeText(this, R.string.progressbar_error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
private static boolean isDocsAction(@Nullable String action) {
|
||||
return action != null && (action.equals(Intent.ACTION_GET_CONTENT)
|
||||
|| action.equals(Intent.ACTION_CREATE_DOCUMENT)
|
||||
|| action.equals(Intent.ACTION_OPEN_DOCUMENT)
|
||||
|| action.equals(Intent.ACTION_OPEN_DOCUMENT_TREE));
|
||||
}
|
||||
|
||||
protected Theme getCurrentTheme() {
|
||||
return _currentTheme;
|
||||
private static boolean isAutoLockBypassedForAction(@Nullable String action) {
|
||||
return isDocsAction(action) || (action != null && (action.equals(Intent.ACTION_PICK)
|
||||
|| action.equals(Intent.ACTION_SEND)
|
||||
|| action.equals(Intent.ACTION_CHOOSER)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -14,6 +15,7 @@ import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -21,19 +23,19 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.MetricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
@@ -50,13 +52,17 @@ import javax.crypto.SecretKey;
|
||||
public class AuthActivity extends AegisActivity {
|
||||
private EditText _textPassword;
|
||||
|
||||
private CancelAction _cancelAction;
|
||||
private SlotList _slots;
|
||||
|
||||
private SecretKey _bioKey;
|
||||
private BiometricSlot _bioSlot;
|
||||
private BiometricPrompt _bioPrompt;
|
||||
|
||||
private int _failedUnlockAttempts;
|
||||
|
||||
// the first time this activity is resumed after creation, it's possible to inhibit showing the
|
||||
// biometric prompt by setting 'inhibitBioPrompt' to true through the intent
|
||||
private boolean _inhibitBioPrompt;
|
||||
|
||||
private Preferences _prefs;
|
||||
private boolean _stateless;
|
||||
|
||||
@@ -68,7 +74,7 @@ public class AuthActivity extends AegisActivity {
|
||||
_textPassword = findViewById(R.id.text_password);
|
||||
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
|
||||
Button decryptButton = findViewById(R.id.button_decrypt);
|
||||
Button biometricsButton = findViewById(R.id.button_biometrics);
|
||||
TextView biometricsButton = findViewById(R.id.button_biometrics);
|
||||
|
||||
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
@@ -77,8 +83,16 @@ public class AuthActivity extends AegisActivity {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (_prefs.isPinKeyboardEnabled()) {
|
||||
_textPassword.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
|
||||
if (savedInstanceState == null) {
|
||||
_inhibitBioPrompt = intent.getBooleanExtra("inhibitBioPrompt", false);
|
||||
} else {
|
||||
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
|
||||
}
|
||||
_slots = (SlotList) intent.getSerializableExtra("slots");
|
||||
_stateless = _slots != null;
|
||||
if (!_stateless) {
|
||||
@@ -126,6 +140,7 @@ public class AuthActivity extends AegisActivity {
|
||||
// display a help message if a matching invalidated keystore entry was found
|
||||
if (invalidated) {
|
||||
boxBiometricInfo.setVisibility(View.VISIBLE);
|
||||
biometricsButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +151,8 @@ public class AuthActivity extends AegisActivity {
|
||||
char[] password = EditTextHelper.getEditTextChars(_textPassword);
|
||||
List<PasswordSlot> slots = _slots.findAll(PasswordSlot.class);
|
||||
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
|
||||
new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener()).execute(params);
|
||||
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener());
|
||||
task.execute(getLifecycle(), params);
|
||||
});
|
||||
|
||||
biometricsButton.setOnClickListener(v -> {
|
||||
@@ -144,6 +160,17 @@ public class AuthActivity extends AegisActivity {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean("inhibitBioPrompt", _inhibitBioPrompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.NO_ACTION_BAR);
|
||||
}
|
||||
|
||||
private void selectPassword() {
|
||||
_textPassword.selectAll();
|
||||
|
||||
@@ -153,11 +180,10 @@ public class AuthActivity extends AegisActivity {
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
switch (_cancelAction) {
|
||||
case KILL:
|
||||
finishAffinity();
|
||||
case CLOSE:
|
||||
finish();
|
||||
if (_stateless) {
|
||||
super.onBackPressed();
|
||||
} else {
|
||||
finishAffinity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,24 +191,40 @@ public class AuthActivity extends AegisActivity {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (_bioKey != null) {
|
||||
if (_prefs.isPasswordReminderNeeded()) {
|
||||
focusPasswordField();
|
||||
} else {
|
||||
showBiometricPrompt();
|
||||
}
|
||||
} else {
|
||||
boolean remindPassword = _prefs.isPasswordReminderNeeded();
|
||||
if (_bioKey == null || remindPassword) {
|
||||
focusPasswordField();
|
||||
}
|
||||
|
||||
if (_bioKey != null && _bioPrompt == null && !_inhibitBioPrompt && !remindPassword) {
|
||||
_bioPrompt = showBiometricPrompt();
|
||||
}
|
||||
|
||||
_inhibitBioPrompt = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (!isChangingConfigurations() && _bioPrompt != null) {
|
||||
_bioPrompt.cancelAuthentication();
|
||||
_bioPrompt = null;
|
||||
}
|
||||
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
if (_bioPrompt != null && _prefs.isPasswordReminderNeeded()) {
|
||||
if (_bioKey != null && _prefs.isPasswordReminderNeeded()) {
|
||||
showPasswordReminder();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isOrphan() {
|
||||
return _stateless && super.isOrphan();
|
||||
}
|
||||
|
||||
private void focusPasswordField() {
|
||||
_textPassword.requestFocus();
|
||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
@@ -190,44 +232,48 @@ public class AuthActivity extends AegisActivity {
|
||||
|
||||
private void showPasswordReminder() {
|
||||
View popupLayout = getLayoutInflater().inflate(R.layout.popup_password, null);
|
||||
popupLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
||||
|
||||
PopupWindow popup = new PopupWindow(popupLayout, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
popup.setFocusable(false);
|
||||
popup.setOutsideTouchable(true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
|
||||
popup.setElevation(5.0f);
|
||||
}
|
||||
_textPassword.post(() -> popup.showAsDropDown(_textPassword));
|
||||
_textPassword.post(() -> {
|
||||
// calculating the actual height of the popup window does not seem possible
|
||||
// adding 25dp seems to look good enough
|
||||
int yoff = _textPassword.getHeight()
|
||||
+ popupLayout.getMeasuredHeight()
|
||||
+ MetricsHelper.convertDpToPixels(this, 25);
|
||||
popup.showAsDropDown(_textPassword, 0, -yoff);
|
||||
});
|
||||
_textPassword.postDelayed(popup::dismiss, 5000);
|
||||
}
|
||||
|
||||
public void showBiometricPrompt() {
|
||||
public BiometricPrompt showBiometricPrompt() {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(_textPassword.getWindowToken(), 0);
|
||||
|
||||
Cipher cipher;
|
||||
try {
|
||||
cipher = _bioSlot.createDecryptCipher(_bioKey);
|
||||
} catch (SlotException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
BiometricPrompt.CryptoObject cryptoObj = new BiometricPrompt.CryptoObject(cipher);
|
||||
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
|
||||
BiometricPrompt prompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
|
||||
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.authentication))
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.setConfirmationRequired(false)
|
||||
.build();
|
||||
_bioPrompt.authenticate(info, cryptoObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
if (_bioPrompt != null) {
|
||||
_bioPrompt.cancelAuthentication();
|
||||
}
|
||||
prompt.authenticate(info, cryptoObj);
|
||||
return prompt;
|
||||
}
|
||||
|
||||
private void finish(MasterKey key, boolean isSlotRepaired) {
|
||||
@@ -241,10 +287,9 @@ public class AuthActivity extends AegisActivity {
|
||||
} else {
|
||||
try {
|
||||
AegisApplication app = getApp();
|
||||
VaultManager vault = app.initVaultManager(app.loadVaultFile(), creds);
|
||||
app.initVaultManager(app.loadVaultFile(), creds);
|
||||
if (isSlotRepaired) {
|
||||
vault.setCredentials(creds);
|
||||
saveVault();
|
||||
saveVault(true);
|
||||
}
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
@@ -258,6 +303,21 @@ public class AuthActivity extends AegisActivity {
|
||||
finish();
|
||||
}
|
||||
|
||||
private void onInvalidPassword() {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
|
||||
.setTitle(getString(R.string.unlock_vault_error))
|
||||
.setMessage(getString(R.string.unlock_vault_error_description))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
|
||||
.create());
|
||||
|
||||
_failedUnlockAttempts ++;
|
||||
|
||||
if (_failedUnlockAttempts >= 3) {
|
||||
_textPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
private class PasswordDerivationListener implements PasswordSlotDecryptTask.Callback {
|
||||
@Override
|
||||
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
|
||||
@@ -273,12 +333,7 @@ public class AuthActivity extends AegisActivity {
|
||||
|
||||
finish(result.getKey(), result.isSlotRepaired());
|
||||
} else {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
|
||||
.setTitle(getString(R.string.unlock_vault_error))
|
||||
.setMessage(getString(R.string.unlock_vault_error_description))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
|
||||
.create());
|
||||
onInvalidPassword();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,6 +342,8 @@ public class AuthActivity extends AegisActivity {
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
super.onAuthenticationError(errorCode, errString);
|
||||
_bioPrompt = null;
|
||||
|
||||
if (!BiometricsHelper.isCanceled(errorCode)) {
|
||||
Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
@@ -295,6 +352,7 @@ public class AuthActivity extends AegisActivity {
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
super.onAuthenticationSucceeded(result);
|
||||
_bioPrompt = null;
|
||||
|
||||
MasterKey key;
|
||||
BiometricSlot slot = _slots.find(BiometricSlot.class);
|
||||
|
||||
@@ -6,8 +6,8 @@ import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.provider.Settings;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
@@ -15,24 +15,34 @@ import android.text.method.PasswordTransformationMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.ComponentActivity;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.nulabinc.zxcvbn.Strength;
|
||||
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
@@ -66,7 +76,7 @@ public class Dialogs {
|
||||
String title, message;
|
||||
if (totalEntries > 1) {
|
||||
title = activity.getString(R.string.delete_entries);
|
||||
message = String.format(activity.getString(R.string.delete_entries_description), totalEntries);
|
||||
message = activity.getResources().getQuantityString(R.plurals.delete_entries_description, totalEntries, totalEntries);
|
||||
} else {
|
||||
title = activity.getString(R.string.delete_entry);
|
||||
message = activity.getString(R.string.delete_entry_description);
|
||||
@@ -89,10 +99,14 @@ public class Dialogs {
|
||||
.create());
|
||||
}
|
||||
|
||||
public static void showSetPasswordDialog(Activity activity, Dialogs.SlotListener listener) {
|
||||
public static void showSetPasswordDialog(ComponentActivity activity, Dialogs.SlotListener listener) {
|
||||
Zxcvbn zxcvbn = new Zxcvbn();
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
|
||||
EditText textPassword = view.findViewById(R.id.text_password);
|
||||
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
|
||||
ProgressBar barPasswordStrength = view.findViewById(R.id.progressBar);
|
||||
TextView textPasswordStrength = view.findViewById(R.id.text_password_strength);
|
||||
TextInputLayout textPasswordWrapper = view.findViewById(R.id.text_password_wrapper);
|
||||
CheckBox switchToggleVisibility = view.findViewById(R.id.check_toggle_visibility);
|
||||
|
||||
switchToggleVisibility.setOnCheckedChangeListener((CompoundButton.OnCheckedChangeListener) (buttonView, isChecked) -> {
|
||||
@@ -113,6 +127,7 @@ public class Dialogs {
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
|
||||
final AtomicReference<Button> buttonOK = new AtomicReference<>();
|
||||
dialog.setOnShowListener(d -> {
|
||||
@@ -140,19 +155,30 @@ public class Dialogs {
|
||||
listener.onSlotResult(slot, cipher);
|
||||
dialog.dismiss();
|
||||
});
|
||||
task.execute(new KeyDerivationTask.Params(slot, password));
|
||||
KeyDerivationTask.Params params = new KeyDerivationTask.Params(slot, password);
|
||||
task.execute(activity.getLifecycle(), params);
|
||||
});
|
||||
});
|
||||
|
||||
TextWatcher watcher = new TextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence c, int start, int before, int count) {
|
||||
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
|
||||
buttonOK.get().setEnabled(equal);
|
||||
|
||||
Strength strength = zxcvbn.measure(textPassword.getText());
|
||||
barPasswordStrength.setProgress(strength.getScore());
|
||||
barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(PasswordStrengthHelper.getColor(strength.getScore()))));
|
||||
textPasswordStrength.setText((textPassword.getText().length() != 0) ? PasswordStrengthHelper.getString(strength.getScore(), activity) : "");
|
||||
textPasswordWrapper.setError(strength.getFeedback().getWarning());
|
||||
strength.wipe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence c, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable c) {
|
||||
}
|
||||
};
|
||||
@@ -162,7 +188,7 @@ public class Dialogs {
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener, boolean isSecret) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
|
||||
EditText input = view.findViewById(R.id.text_input);
|
||||
if (isSecret) {
|
||||
@@ -177,16 +203,22 @@ public class Dialogs {
|
||||
char[] text = EditTextHelper.getEditTextChars(input);
|
||||
listener.onTextInputResult(text);
|
||||
});
|
||||
|
||||
if (dismissListener != null) {
|
||||
builder.setOnDismissListener(dismissListener);
|
||||
}
|
||||
|
||||
if (messageId != 0) {
|
||||
builder.setMessage(messageId);
|
||||
}
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, isSecret);
|
||||
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret);
|
||||
}
|
||||
|
||||
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
|
||||
@@ -198,13 +230,48 @@ public class Dialogs {
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
|
||||
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, true);
|
||||
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, null, true);
|
||||
}
|
||||
|
||||
public static void showPasswordInputDialog(Context context, @StringRes int setPasswordMessageId, @StringRes int messageId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener) {
|
||||
showTextInputDialog(context, setPasswordMessageId, messageId, R.string.password, listener, dismissListener, true);
|
||||
}
|
||||
|
||||
public static void showCheckboxDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int checkboxMessageId, CheckboxInputListener listener) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_checkbox, null);
|
||||
CheckBox checkBox = view.findViewById(R.id.checkbox);
|
||||
checkBox.setText(checkboxMessageId);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
.setTitle(titleId)
|
||||
.setView(view)
|
||||
.setNegativeButton(R.string.no, (dialog1, which) ->
|
||||
listener.onCheckboxInputResult(false))
|
||||
.setPositiveButton(R.string.yes, (dialog1, which) ->
|
||||
listener.onCheckboxInputResult(checkBox.isChecked()));
|
||||
|
||||
if (messageId != 0) {
|
||||
builder.setMessage(messageId);
|
||||
}
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
|
||||
final AtomicReference<Button> buttonOK = new AtomicReference<>();
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setEnabled(false);
|
||||
buttonOK.set(button);
|
||||
});
|
||||
|
||||
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> buttonOK.get().setEnabled(isChecked));
|
||||
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) {
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
|
||||
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
|
||||
numberPicker.setMinValue(3);
|
||||
numberPicker.setMinValue(1);
|
||||
numberPicker.setMaxValue(60);
|
||||
numberPicker.setValue(new Preferences(activity.getApplicationContext()).getTapToRevealTime());
|
||||
numberPicker.setWrapSelectorWheel(true);
|
||||
@@ -320,6 +387,41 @@ public class Dialogs {
|
||||
.create());
|
||||
}
|
||||
|
||||
public static void showImportersDialog(Context context, boolean isDirect, ImporterListener listener) {
|
||||
List<DatabaseImporter.Definition> importers = DatabaseImporter.getImporters(isDirect);
|
||||
String[] names = importers.stream().map(DatabaseImporter.Definition::getName).toArray(String[]::new);
|
||||
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.dialog_importers, null);
|
||||
TextView helpText = view.findViewById(R.id.text_importer_help);
|
||||
setImporterHelpText(helpText, importers.get(0), isDirect);
|
||||
ListView listView = view.findViewById(R.id.list_importers);
|
||||
listView.setAdapter(new ArrayAdapter<>(context, R.layout.card_importer, names));
|
||||
listView.setItemChecked(0, true);
|
||||
listView.setOnItemClickListener((parent, view1, position, id) -> {
|
||||
setImporterHelpText(helpText, importers.get(position), isDirect);
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.choose_application)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
|
||||
listener.onImporterSelectionResult(importers.get(listView.getCheckedItemPosition()));
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
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()));
|
||||
} else {
|
||||
view.setText(definition.getHelp());
|
||||
}
|
||||
}
|
||||
|
||||
public interface CheckboxInputListener {
|
||||
void onCheckboxInputResult(boolean checkbox);
|
||||
}
|
||||
|
||||
public interface NumberInputListener {
|
||||
void onNumberInputResult(int number);
|
||||
}
|
||||
@@ -332,4 +434,8 @@ public class Dialogs {
|
||||
void onSlotResult(Slot slot, Cipher cipher);
|
||||
void onException(Exception e);
|
||||
}
|
||||
|
||||
public interface ImporterListener {
|
||||
void onImporterSelectionResult(DatabaseImporter.Definition definition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
@@ -36,6 +37,7 @@ import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
@@ -67,6 +69,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
private boolean _hasCustomIcon = false;
|
||||
// keep track of icon changes separately as the generated jpeg's are not deterministic
|
||||
private boolean _hasChangedIcon = false;
|
||||
private boolean _isEditingIcon;
|
||||
private CircleImageView _iconView;
|
||||
private ImageView _saveImageButton;
|
||||
|
||||
@@ -245,11 +248,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
});
|
||||
|
||||
_iconView.setOnClickListener(v -> {
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, "Select photo");
|
||||
startActivityForResult(Intent.createChooser(chooserIntent, "Select photo"), PICK_IMAGE_REQUEST);
|
||||
startIconSelectionActivity();
|
||||
});
|
||||
|
||||
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
|
||||
@@ -326,6 +325,11 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (_isEditingIcon) {
|
||||
stopEditingIcon(false);
|
||||
return;
|
||||
}
|
||||
|
||||
AtomicReference<String> msg = new AtomicReference<>();
|
||||
AtomicReference<VaultEntry> entry = new AtomicReference<>();
|
||||
|
||||
@@ -370,6 +374,9 @@ public class EditEntryActivity extends AegisActivity {
|
||||
deleteAndFinish(_origEntry);
|
||||
});
|
||||
break;
|
||||
case R.id.action_edit_icon:
|
||||
startIconSelectionActivity();
|
||||
break;
|
||||
case R.id.action_default_icon:
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
@@ -382,6 +389,56 @@ public class EditEntryActivity extends AegisActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startIconSelectionActivity() {
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
fileIntent.setType("image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
|
||||
startActivityForResult(chooserIntent, PICK_IMAGE_REQUEST);
|
||||
}
|
||||
|
||||
private void startEditingIcon(Uri data) {
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(data)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
_kropView.setBitmap(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
|
||||
}
|
||||
});
|
||||
_iconView.setVisibility(View.GONE);
|
||||
_kropView.setVisibility(View.VISIBLE);
|
||||
|
||||
_saveImageButton.setOnClickListener(v -> {
|
||||
stopEditingIcon(true);
|
||||
});
|
||||
|
||||
_isEditingIcon = true;
|
||||
}
|
||||
|
||||
private void stopEditingIcon(boolean save) {
|
||||
if (save) {
|
||||
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
|
||||
}
|
||||
_iconView.setVisibility(View.VISIBLE);
|
||||
_kropView.setVisibility(View.GONE);
|
||||
_hasCustomIcon = _hasCustomIcon || save;
|
||||
_hasChangedIcon = save;
|
||||
_isEditingIcon = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_edit, menu);
|
||||
@@ -396,7 +453,11 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
private void addAndFinish(VaultEntry entry) {
|
||||
if (_isNew) {
|
||||
// It's possible that the new entry was already added to the vault, but writing the
|
||||
// vault to disk failed, causing the user to tap 'Save' again. Calling addEntry
|
||||
// again would cause a crash in that case, so the isEntryDuplicate check prevents
|
||||
// that.
|
||||
if (_isNew && !_vault.isEntryDuplicate(entry)) {
|
||||
_vault.addEntry(entry);
|
||||
} else {
|
||||
_vault.replaceEntry(entry);
|
||||
@@ -415,7 +476,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
intent.putExtra("entryUUID", entry.getUUID());
|
||||
intent.putExtra("delete", delete);
|
||||
|
||||
if (saveVault()) {
|
||||
if (saveVault(true)) {
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
@@ -424,35 +485,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
|
||||
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(data.getData())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
_kropView.setBitmap(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
|
||||
}
|
||||
});
|
||||
_iconView.setVisibility(View.GONE);
|
||||
_kropView.setVisibility(View.VISIBLE);
|
||||
|
||||
_saveImageButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
|
||||
_iconView.setVisibility(View.VISIBLE);
|
||||
_kropView.setVisibility(View.GONE);
|
||||
_hasCustomIcon = true;
|
||||
_hasChangedIcon = true;
|
||||
}
|
||||
});
|
||||
startEditingIcon(data.getData());
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
@@ -483,7 +516,8 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
byte[] secret;
|
||||
try {
|
||||
secret = Base32.decode(new String(EditTextHelper.getEditTextChars(_textSecret, true)));
|
||||
String secretString = new String(EditTextHelper.getEditTextChars(_textSecret));
|
||||
secret = GoogleAuthInfo.parseSecret(secretString);
|
||||
if (secret.length == 0) {
|
||||
throw new ParseException("Secret cannot be empty");
|
||||
}
|
||||
@@ -556,6 +590,10 @@ public class EditEntryActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
private boolean onSave() {
|
||||
if (_isEditingIcon) {
|
||||
stopEditingIcon(true);
|
||||
}
|
||||
|
||||
VaultEntry entry;
|
||||
try {
|
||||
entry = parseEntry();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class ExitActivity extends Activity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
public static void exitAppAndRemoveFromRecents(Context context) {
|
||||
Intent intent = new Intent(context, ExitActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK |
|
||||
Intent.FLAG_ACTIVITY_NO_ANIMATION |
|
||||
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
|
||||
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.beemdevelopment.aegis.ui;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
|
||||
@@ -11,7 +12,6 @@ import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -19,6 +19,8 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
|
||||
private GroupAdapter _adapter;
|
||||
private TreeSet<String> _groups;
|
||||
private RecyclerView _slotsView;
|
||||
private View _emptyStateView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -29,21 +31,35 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
_groups = new TreeSet<>(Collator.getInstance());
|
||||
_groups.addAll(intent.getStringArrayListExtra("groups"));
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
// set up the recycler view
|
||||
_adapter = new GroupAdapter(this);
|
||||
RecyclerView slotsView = findViewById(R.id.list_slots);
|
||||
_slotsView= findViewById(R.id.list_slots);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
slotsView.setLayoutManager(layoutManager);
|
||||
slotsView.setAdapter(_adapter);
|
||||
slotsView.setNestedScrollingEnabled(false);
|
||||
_slotsView.setLayoutManager(layoutManager);
|
||||
_slotsView.setAdapter(_adapter);
|
||||
_slotsView.setNestedScrollingEnabled(false);
|
||||
|
||||
for (String group : _groups) {
|
||||
_adapter.addGroup(group);
|
||||
}
|
||||
|
||||
_emptyStateView = findViewById(R.id.vEmptyList);
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
if (_adapter.getItemCount() > 0) {
|
||||
_slotsView.setVisibility(View.VISIBLE);
|
||||
_emptyStateView.setVisibility(View.GONE);
|
||||
} else {
|
||||
_slotsView.setVisibility(View.GONE);
|
||||
_emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,6 +70,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
_groups.remove(group);
|
||||
_adapter.removeGroup(group);
|
||||
updateEmptyState();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
|
||||
@@ -1,103 +1,77 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
|
||||
import com.beemdevelopment.aegis.ThemeMap;
|
||||
import com.beemdevelopment.aegis.ui.intro.IntroBaseActivity;
|
||||
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
|
||||
import com.beemdevelopment.aegis.ui.slides.DoneSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
|
||||
import com.beemdevelopment.aegis.ui.slides.WelcomeSlide;
|
||||
import com.beemdevelopment.aegis.vault.Vault;
|
||||
import com.beemdevelopment.aegis.vault.VaultFile;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileException;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultManagerException;
|
||||
import com.github.appintro.AppIntro2;
|
||||
import com.github.appintro.AppIntroFragment;
|
||||
import com.github.appintro.model.SliderPage;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class IntroActivity extends AppIntro2 {
|
||||
private CustomAuthenticatedSlide _authenticatedSlide;
|
||||
private CustomAuthenticationSlide _authenticationSlide;
|
||||
private Fragment _endSlide;
|
||||
|
||||
private AegisApplication _app;
|
||||
private Preferences _prefs;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_INVALID;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_NONE;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_PASS;
|
||||
|
||||
public class IntroActivity extends IntroBaseActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
_app = (AegisApplication) getApplication();
|
||||
// set FLAG_SECURE on the window of every IntroActivity
|
||||
_prefs = new Preferences(this);
|
||||
if (_prefs.isSecureScreenEnabled()) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
setWizardMode(true);
|
||||
setSkipButtonEnabled(false);
|
||||
showStatusBar(true);
|
||||
setSystemBackButtonLocked(true);
|
||||
setBarColor(getResources().getColor(R.color.colorPrimary));
|
||||
|
||||
SliderPage homeSliderPage = new SliderPage();
|
||||
homeSliderPage.setTitle(getString(R.string.welcome));
|
||||
homeSliderPage.setImageDrawable(R.drawable.app_icon);
|
||||
homeSliderPage.setTitleColor(getResources().getColor(R.color.primary_text_dark));
|
||||
homeSliderPage.setDescription(getString(R.string.app_description));
|
||||
homeSliderPage.setDescriptionColor(getResources().getColor(R.color.primary_text_dark));
|
||||
homeSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
|
||||
addSlide(AppIntroFragment.newInstance(homeSliderPage));
|
||||
|
||||
_authenticationSlide = new CustomAuthenticationSlide();
|
||||
_authenticationSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
addSlide(_authenticationSlide);
|
||||
_authenticatedSlide = new CustomAuthenticatedSlide();
|
||||
_authenticatedSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
|
||||
addSlide(_authenticatedSlide);
|
||||
|
||||
SliderPage endSliderPage = new SliderPage();
|
||||
endSliderPage.setTitle(getString(R.string.setup_completed));
|
||||
endSliderPage.setDescription(getString(R.string.setup_completed_description));
|
||||
endSliderPage.setImageDrawable(R.drawable.app_icon);
|
||||
endSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
|
||||
_endSlide = AppIntroFragment.newInstance(endSliderPage);
|
||||
addSlide(_endSlide);
|
||||
addSlide(WelcomeSlide.class);
|
||||
addSlide(SecurityPickerSlide.class);
|
||||
addSlide(SecuritySetupSlide.class);
|
||||
addSlide(DoneSlide.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlideChanged(Fragment oldFragment, Fragment newFragment) {
|
||||
if (oldFragment == _authenticationSlide && newFragment != _endSlide) {
|
||||
// skip to the last slide if no encryption will be used
|
||||
int cryptType = getIntent().getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
// TODO: no magic indices
|
||||
goToNextSlide(false);
|
||||
}
|
||||
}
|
||||
|
||||
setSwipeLock(true);
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.NO_ACTION_BAR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDonePressed(Fragment currentFragment) {
|
||||
super.onDonePressed(currentFragment);
|
||||
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
|
||||
if (oldSlide == SecurityPickerSlide.class
|
||||
&& newSlide == SecuritySetupSlide.class
|
||||
&& getState().getInt("cryptType", CRYPT_TYPE_INVALID) == CRYPT_TYPE_NONE) {
|
||||
skipToSlide(DoneSlide.class);
|
||||
return true;
|
||||
}
|
||||
|
||||
int cryptType = _authenticatedSlide.getCryptType();
|
||||
VaultFileCredentials creds = _authenticatedSlide.getCredentials();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDonePressed() {
|
||||
Bundle state = getState();
|
||||
|
||||
int cryptType = state.getInt("cryptType", CRYPT_TYPE_INVALID);
|
||||
VaultFileCredentials creds = (VaultFileCredentials) state.getSerializable("creds");
|
||||
if (cryptType == CRYPT_TYPE_INVALID
|
||||
|| (cryptType == CRYPT_TYPE_NONE && creds != null)
|
||||
|| (cryptType == CRYPT_TYPE_PASS && (creds == null || !creds.getSlots().has(PasswordSlot.class)))
|
||||
|| (cryptType == CRYPT_TYPE_BIOMETRIC && (creds == null || !creds.getSlots().has(PasswordSlot.class) || !creds.getSlots().has(BiometricSlot.class)))) {
|
||||
throw new RuntimeException(String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d, creds: %s", cryptType, creds));
|
||||
}
|
||||
|
||||
Vault vault = new Vault();
|
||||
VaultFile vaultFile = new VaultFile();
|
||||
try {
|
||||
JSONObject obj = vault.toJson();
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
if (cryptType == CRYPT_TYPE_NONE) {
|
||||
vaultFile.setContent(obj);
|
||||
} else {
|
||||
vaultFile.setContent(obj, creds);
|
||||
@@ -110,20 +84,16 @@ public class IntroActivity extends AppIntro2 {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
_app.initVaultManager(vault, null);
|
||||
if (cryptType == CRYPT_TYPE_NONE) {
|
||||
getApp().initVaultManager(vault, null);
|
||||
} else {
|
||||
_app.initVaultManager(vault, creds);
|
||||
getApp().initVaultManager(vault, creds);
|
||||
}
|
||||
|
||||
// skip the intro from now on
|
||||
_prefs.setIntroDone(true);
|
||||
getPreferences().setIntroDone(true);
|
||||
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
public void goToNextSlide() {
|
||||
super.goToNextSlide(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,16 +16,20 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.helpers.BitmapHelper;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.helpers.PermissionHelper;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
@@ -40,12 +44,12 @@ import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.ChecksumException;
|
||||
import com.google.zxing.FormatException;
|
||||
import com.google.zxing.LuminanceSource;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.google.zxing.RGBLuminanceSource;
|
||||
import com.google.zxing.Reader;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.google.zxing.qrcode.QRCodeReader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -74,6 +78,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
private String _selectedGroup;
|
||||
private boolean _searchSubmitted;
|
||||
|
||||
private boolean _isAuthenticating;
|
||||
private boolean _isDoingIntro;
|
||||
|
||||
private List<VaultEntry> _selectedEntries;
|
||||
private ActionMode _actionMode;
|
||||
|
||||
@@ -81,6 +88,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
private SearchView _searchView;
|
||||
private FloatingActionsMenu _fabMenu;
|
||||
private EntryListView _entryListView;
|
||||
private LinearLayout _btnBackupError;
|
||||
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
@@ -89,13 +97,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
_app = (AegisApplication) getApplication();
|
||||
_vault = _app.getVaultManager();
|
||||
_loaded = false;
|
||||
|
||||
// set up the main view
|
||||
setContentView(R.layout.activity_main);
|
||||
if (savedInstanceState != null) {
|
||||
_isAuthenticating = savedInstanceState.getBoolean("isAuthenticating");
|
||||
_isDoingIntro = savedInstanceState.getBoolean("isDoingIntro");
|
||||
}
|
||||
|
||||
// set up the entry view
|
||||
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
|
||||
@@ -108,6 +119,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.setTapToRevealTime(getPreferences().getTapToRevealTime());
|
||||
_entryListView.setSortCategory(getPreferences().getCurrentSortCategory(), false);
|
||||
_entryListView.setViewMode(getPreferences().getCurrentViewMode());
|
||||
_entryListView.setIsCopyOnTapEnabled(getPreferences().isCopyOnTapEnabled());
|
||||
|
||||
// set up the floating action button
|
||||
_fabMenu = findViewById(R.id.fab);
|
||||
@@ -124,10 +136,22 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
startScanActivity();
|
||||
});
|
||||
|
||||
_btnBackupError = findViewById(R.id.btn_backup_error);
|
||||
_btnBackupError.setOnClickListener(view -> {
|
||||
startPreferencesActivity("pref_backups");
|
||||
});
|
||||
|
||||
_fabScrollHelper = new FabScrollHelper(_fabMenu);
|
||||
_selectedEntries = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean("isAuthenticating", _isAuthenticating);
|
||||
outState.putBoolean("isDoingIntro", _isDoingIntro);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
_entryListView.setListener(null);
|
||||
@@ -151,20 +175,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
return super.dispatchTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
|
||||
doShortcutActions();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
// don't process any activity results if the vault is locked
|
||||
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _app.isVaultLocked()) {
|
||||
return;
|
||||
}
|
||||
_isAuthenticating = false;
|
||||
_isDoingIntro = false;
|
||||
|
||||
if (resultCode != RESULT_OK) {
|
||||
return;
|
||||
@@ -215,24 +229,28 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
private void onPreferencesResult(Intent data) {
|
||||
// refresh the entire entry list if needed
|
||||
if (data.getBooleanExtra("needsRecreate", false)) {
|
||||
recreate();
|
||||
} else if (data.getBooleanExtra("needsRefresh", false)) {
|
||||
boolean showAccountName = getPreferences().isAccountNameVisible();
|
||||
int codeGroupSize = getPreferences().getCodeGroupSize();
|
||||
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
|
||||
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
|
||||
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
|
||||
int tapToRevealTime = getPreferences().getTapToRevealTime();
|
||||
ViewMode viewMode = getPreferences().getCurrentViewMode();
|
||||
_entryListView.setShowAccountName(showAccountName);
|
||||
_entryListView.setCodeGroupSize(codeGroupSize);
|
||||
_entryListView.setSearchAccountName(searchAccountName);
|
||||
_entryListView.setHighlightEntry(highlightEntry);
|
||||
_entryListView.setTapToReveal(tapToReveal);
|
||||
_entryListView.setTapToRevealTime(tapToRevealTime);
|
||||
_entryListView.setViewMode(viewMode);
|
||||
_entryListView.refresh(true);
|
||||
if (_loaded) {
|
||||
if (data.getBooleanExtra("needsRecreate", false)) {
|
||||
recreate();
|
||||
} else if (data.getBooleanExtra("needsRefresh", false)) {
|
||||
boolean showAccountName = getPreferences().isAccountNameVisible();
|
||||
int codeGroupSize = getPreferences().getCodeGroupSize();
|
||||
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
|
||||
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
|
||||
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
|
||||
int tapToRevealTime = getPreferences().getTapToRevealTime();
|
||||
ViewMode viewMode = getPreferences().getCurrentViewMode();
|
||||
boolean copyOnTap = getPreferences().isCopyOnTapEnabled();
|
||||
_entryListView.setShowAccountName(showAccountName);
|
||||
_entryListView.setCodeGroupSize(codeGroupSize);
|
||||
_entryListView.setSearchAccountName(searchAccountName);
|
||||
_entryListView.setHighlightEntry(highlightEntry);
|
||||
_entryListView.setTapToReveal(tapToReveal);
|
||||
_entryListView.setTapToRevealTime(tapToRevealTime);
|
||||
_entryListView.setViewMode(viewMode);
|
||||
_entryListView.setIsCopyOnTapEnabled(copyOnTap);
|
||||
_entryListView.refresh(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,27 +272,33 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
} else {
|
||||
for (VaultEntry entry : entries) {
|
||||
_vault.addEntry(entry);
|
||||
_entryListView.addEntry(entry);
|
||||
if (_loaded) {
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
saveVault();
|
||||
saveVault(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAddEntryResult(Intent data) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.addEntry(entry);
|
||||
if (_loaded) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.addEntry(entry, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditEntryResult(Intent data) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
if (_loaded) {
|
||||
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
|
||||
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
_entryListView.removeEntry(entryUUID);
|
||||
} else {
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
_entryListView.removeEntry(entryUUID);
|
||||
} else {
|
||||
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
|
||||
_entryListView.replaceEntry(entryUUID, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +311,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
|
||||
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
|
||||
bitmap = BitmapHelper.resize(bitmap, 640, 480);
|
||||
}
|
||||
|
||||
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
|
||||
@@ -295,7 +320,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
|
||||
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
Reader reader = new MultiFormatReader();
|
||||
Reader reader = new QRCodeReader();
|
||||
Result result = reader.decode(binaryBitmap);
|
||||
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
|
||||
@@ -385,8 +410,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
fileIntent.setType("image/*");
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
|
||||
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE);
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
|
||||
startActivityForResult(chooserIntent, CODE_SCAN_IMAGE);
|
||||
}
|
||||
|
||||
private void startPreferencesActivity(String preference) {
|
||||
Intent intent = new Intent(this, PreferencesActivity.class);
|
||||
intent.putExtra("pref", preference);
|
||||
startActivityForResult(intent, CODE_PREFERENCES);
|
||||
}
|
||||
|
||||
private void doShortcutActions() {
|
||||
@@ -411,10 +446,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
|
||||
Uri uri = intent.getData();
|
||||
getIntent().setData(null);
|
||||
getIntent().setAction(null);
|
||||
Uri uri = intent.getData();
|
||||
if (Intent.ACTION_VIEW.equals(intent.getAction()) && uri != null) {
|
||||
intent.setData(null);
|
||||
intent.setAction(null);
|
||||
|
||||
GoogleAuthInfo info = null;
|
||||
try {
|
||||
@@ -437,12 +472,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
if (_vault == null) {
|
||||
// start the intro if the vault file doesn't exist
|
||||
if (!VaultManager.fileExists(this)) {
|
||||
if (!_isDoingIntro && !VaultManager.fileExists(this)) {
|
||||
if (getPreferences().isIntroDone()) {
|
||||
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
Intent intro = new Intent(this, IntroActivity.class);
|
||||
startActivityForResult(intro, CODE_DO_INTRO);
|
||||
_isDoingIntro = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -461,7 +497,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
if (_app.isVaultLocked()) {
|
||||
startAuthActivity();
|
||||
startAuthActivity(false);
|
||||
} else if (_loaded) {
|
||||
// update the list of groups in the filter menu
|
||||
if (_menu != null) {
|
||||
@@ -478,6 +514,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
handleDeeplink();
|
||||
updateLockIcon();
|
||||
doShortcutActions();
|
||||
updateBackupErrorBar();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -492,8 +529,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
return;
|
||||
}
|
||||
|
||||
if (_app.isAutoLockEnabled()) {
|
||||
_app.lock();
|
||||
if (_app.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)) {
|
||||
_app.lock(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -506,7 +543,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.removeEntry(oldEntry);
|
||||
}
|
||||
|
||||
saveVault();
|
||||
saveVault(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -556,8 +593,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, PreferencesActivity.class);
|
||||
startActivityForResult(intent, CODE_PREFERENCES);
|
||||
startPreferencesActivity(null);
|
||||
return true;
|
||||
}
|
||||
case R.id.action_about: {
|
||||
@@ -566,7 +602,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
return true;
|
||||
}
|
||||
case R.id.action_lock:
|
||||
_app.lock();
|
||||
_app.lock(true);
|
||||
return true;
|
||||
default:
|
||||
if (item.getGroupId() == R.id.action_filter_group) {
|
||||
@@ -615,16 +651,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
private void loadEntries() {
|
||||
List<VaultEntry> entries = new ArrayList<>(_vault.getEntries());
|
||||
_entryListView.addEntries(entries);
|
||||
_entryListView.runEntriesAnimation();
|
||||
_loaded = true;
|
||||
if (!_loaded) {
|
||||
_entryListView.addEntries(_vault.getEntries());
|
||||
_entryListView.runEntriesAnimation();
|
||||
_loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void startAuthActivity() {
|
||||
Intent intent = new Intent(this, AuthActivity.class);
|
||||
intent.putExtra("cancelAction", CancelAction.KILL);
|
||||
startActivityForResult(intent, CODE_DECRYPT);
|
||||
private void startAuthActivity(boolean inhibitBioPrompt) {
|
||||
if (!_isAuthenticating) {
|
||||
Intent intent = new Intent(this, AuthActivity.class);
|
||||
intent.putExtra("inhibitBioPrompt", inhibitBioPrompt);
|
||||
startActivityForResult(intent, CODE_DECRYPT);
|
||||
_isAuthenticating = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLockIcon() {
|
||||
@@ -635,6 +675,15 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBackupErrorBar() {
|
||||
String error = null;
|
||||
if (_app.getPreferences().isBackupsEnabled()) {
|
||||
error = _app.getPreferences().getBackupsError();
|
||||
}
|
||||
|
||||
_btnBackupError.setVisibility(error == null ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryClick(VaultEntry entry) {
|
||||
if (_actionMode != null) {
|
||||
@@ -646,8 +695,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
copyEntryCode(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -684,12 +731,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
@Override
|
||||
public void onEntryDrop(VaultEntry entry) {
|
||||
saveVault();
|
||||
saveVault(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryChange(VaultEntry entry) {
|
||||
saveVault();
|
||||
saveVault(true);
|
||||
}
|
||||
|
||||
public void onEntryCopy(VaultEntry entry) {
|
||||
copyEntryCode(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -698,7 +749,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocked() {
|
||||
public void onListChange() { _fabScrollHelper.setVisible(true); }
|
||||
|
||||
@Override
|
||||
public void onLocked(boolean userInitiated) {
|
||||
if (_actionMode != null) {
|
||||
_actionMode.finish();
|
||||
}
|
||||
@@ -706,11 +760,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.clearEntries();
|
||||
_loaded = false;
|
||||
|
||||
if (isOpen()) {
|
||||
startAuthActivity();
|
||||
if (userInitiated) {
|
||||
startAuthActivity(true);
|
||||
} else {
|
||||
super.onLocked(userInitiated);
|
||||
}
|
||||
|
||||
super.onLocked();
|
||||
}
|
||||
|
||||
private void copyEntryCode(VaultEntry entry) {
|
||||
@@ -745,6 +799,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_share_qr:
|
||||
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
|
||||
ArrayList<GoogleAuthInfo> authInfos = new ArrayList<>();
|
||||
for (VaultEntry entry : _selectedEntries) {
|
||||
GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
|
||||
authInfos.add(authInfo);
|
||||
}
|
||||
|
||||
intent.putExtra("authInfos", authInfos);
|
||||
startActivity(intent);
|
||||
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
case R.id.action_delete:
|
||||
Dialogs.showDeleteEntriesDialog(MainActivity.this, (d, which) -> {
|
||||
deleteEntries(_selectedEntries);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.crypto.pins.GuardianProjectFDroidRSA2048;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
|
||||
import info.guardianproject.GuardianProjectRSA4096;
|
||||
import info.guardianproject.trustedintents.TrustedIntents;
|
||||
|
||||
public class PanicResponderActivity extends AegisActivity {
|
||||
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Preferences prefs = getPreferences();
|
||||
|
||||
if(!prefs.isPanicTriggerEnabled()) {
|
||||
Toast.makeText(this, R.string.panic_trigger_ignore_toast, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
TrustedIntents trustedIntents = TrustedIntents.get(this);
|
||||
trustedIntents.addTrustedSigner(GuardianProjectRSA4096.class);
|
||||
trustedIntents.addTrustedSigner(GuardianProjectFDroidRSA2048.class);
|
||||
|
||||
Intent intent = trustedIntents.getIntentFromTrustedSender(this);
|
||||
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
||||
getApp().lock(false);
|
||||
VaultManager.deleteFile(this);
|
||||
finishApp();
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private void finishApp() {
|
||||
ExitActivity.exitAppAndRemoveFromRecents(this);
|
||||
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
public class PreferencesActivity extends AegisActivity {
|
||||
private PreferencesFragment _fragment;
|
||||
@@ -9,6 +11,11 @@ public class PreferencesActivity extends AegisActivity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
_fragment = new PreferencesFragment();
|
||||
_fragment.setArguments(getIntent().getExtras());
|
||||
@@ -18,6 +25,18 @@ public class PreferencesActivity extends AegisActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
Intent intent = getIntent();
|
||||
String preference = intent.getStringExtra("pref");
|
||||
if (preference != null) {
|
||||
_fragment.scrollToPreference(preference);
|
||||
intent.removeExtra("pref");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
// pass permission request results to the fragment
|
||||
@@ -40,4 +59,17 @@ public class PreferencesActivity extends AegisActivity {
|
||||
outState.putParcelable("result", _fragment.getResult());
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,20 +7,27 @@ import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.BuildConfig;
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
@@ -29,6 +36,7 @@ import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
|
||||
import com.beemdevelopment.aegis.importers.AegisImporter;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
|
||||
@@ -36,6 +44,8 @@ import com.beemdevelopment.aegis.importers.DatabaseImporterException;
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
import com.beemdevelopment.aegis.ui.models.ImportEntry;
|
||||
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ExportTask;
|
||||
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.beemdevelopment.aegis.vault.VaultBackupManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
@@ -49,22 +59,21 @@ import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotList;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
import static android.text.TextUtils.isDigitsOnly;
|
||||
|
||||
public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
// activity request codes
|
||||
private static final int CODE_IMPORT = 0;
|
||||
@@ -73,10 +82,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
private static final int CODE_GROUPS = 3;
|
||||
private static final int CODE_SELECT_ENTRIES = 4;
|
||||
private static final int CODE_EXPORT = 5;
|
||||
private static final int CODE_EXPORT_ENCRYPT = 6;
|
||||
private static final int CODE_BACKUPS = 7;
|
||||
private static final int CODE_EXPORT_PLAIN = 6;
|
||||
private static final int CODE_EXPORT_GOOGLE_URI = 7;
|
||||
private static final int CODE_BACKUPS = 8;
|
||||
|
||||
private Intent _result;
|
||||
private AegisApplication _app;
|
||||
private Preferences _prefs;
|
||||
private VaultManager _vault;
|
||||
|
||||
@@ -92,6 +103,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
private Preference _slotsPreference;
|
||||
private Preference _groupsPreference;
|
||||
private Preference _passwordReminderPreference;
|
||||
private SwitchPreferenceCompat _pinKeyboardPreference;
|
||||
private SwitchPreferenceCompat _backupsPreference;
|
||||
private Preference _backupsLocationPreference;
|
||||
private Preference _backupsTriggerPreference;
|
||||
@@ -101,9 +113,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
AegisApplication app = (AegisApplication) getActivity().getApplication();
|
||||
_prefs = app.getPreferences();
|
||||
_vault = app.getVaultManager();
|
||||
_app = (AegisApplication) getActivity().getApplication();
|
||||
_prefs = _app.getPreferences();
|
||||
_vault = _app.getVaultManager();
|
||||
|
||||
// set the result intent in advance
|
||||
setResult(new Intent());
|
||||
@@ -174,7 +186,13 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
startImport();
|
||||
Dialogs.showImportersDialog(getContext(), false, definition -> {
|
||||
_importerType = definition.getType();
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, CODE_IMPORT);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -183,7 +201,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
importAppPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
onImportApp();
|
||||
Dialogs.showImportersDialog(getContext(), true, definition -> {
|
||||
DatabaseImporter importer = DatabaseImporter.create(getContext(), definition.getType());
|
||||
importApp(importer);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -228,6 +249,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference copyOnTapPreference = findPreference("pref_copy_on_tap");
|
||||
copyOnTapPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_result.putExtra("needsRefresh", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
|
||||
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
_result.putExtra("needsRefresh", true);
|
||||
@@ -246,7 +273,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
_result.putExtra("needsRecreate", true);
|
||||
Window window = getActivity().getWindow();
|
||||
if ((boolean)newValue) {
|
||||
if ((boolean) newValue) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
@@ -256,14 +283,14 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
});
|
||||
|
||||
Preference tapToRevealTimePreference = findPreference("pref_tap_to_reveal_time");
|
||||
tapToRevealTimePreference.setSummary(app.getPreferences().getTapToRevealTime() + " seconds");
|
||||
tapToRevealTimePreference.setSummary(_app.getPreferences().getTapToRevealTime() + " seconds");
|
||||
tapToRevealTimePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Dialogs.showNumberPickerDialog(getActivity(), new Dialogs.NumberInputListener() {
|
||||
@Override
|
||||
public void onNumberInputResult(int number) {
|
||||
app.getPreferences().setTapToRevealTime(number);
|
||||
_app.getPreferences().setTapToRevealTime(number);
|
||||
tapToRevealTimePreference.setSummary(number + " seconds");
|
||||
_result.putExtra("needsRefresh", true);
|
||||
}
|
||||
@@ -366,6 +393,33 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
});
|
||||
|
||||
_pinKeyboardPreference = findPreference("pref_pin_keyboard");
|
||||
_pinKeyboardPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if (!(boolean) newValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Dialogs.showPasswordInputDialog(getActivity(), R.string.set_password_confirm, R.string.pin_keyboard_description, password -> {
|
||||
if (isDigitsOnly(new String(password))) {
|
||||
List<PasswordSlot> slots = _vault.getCredentials().getSlots().findAll(PasswordSlot.class);
|
||||
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
|
||||
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(getActivity(), new PasswordConfirmationListener());
|
||||
task.execute(getLifecycle(), params);
|
||||
} else {
|
||||
setPinKeyboardPreference(false);
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.pin_keyboard_error)
|
||||
.setMessage(R.string.pin_keyboard_error_description)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create());
|
||||
}
|
||||
}, dialog -> {
|
||||
setPinKeyboardPreference(false);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
_groupsPreference = findPreference("pref_groups");
|
||||
_groupsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
@@ -404,6 +458,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
if (_prefs.isBackupsEnabled()) {
|
||||
try {
|
||||
_vault.backup();
|
||||
Toast.makeText(getActivity(), R.string.backup_successful, Toast.LENGTH_LONG).show();
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.backup_error, e);
|
||||
@@ -413,23 +468,52 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
});
|
||||
|
||||
_backupsVersionsPreference = findPreference("pref_backups_versions");
|
||||
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), _prefs.getBackupsVersionCount()));
|
||||
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, _prefs.getBackupsVersionCount(), _prefs.getBackupsVersionCount()));
|
||||
_backupsVersionsPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> {
|
||||
number = number * 5 + 5;
|
||||
_prefs.setBackupsVersionCount(number);
|
||||
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), number));
|
||||
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, _prefs.getBackupsVersionCount(), _prefs.getBackupsVersionCount()));
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
_autoLockPreference = findPreference("pref_auto_lock");
|
||||
_autoLockPreference.setSummary(getAutoLockSummary());
|
||||
_autoLockPreference.setOnPreferenceClickListener((preference) -> {
|
||||
final int[] items = Preferences.AUTO_LOCK_SETTINGS;
|
||||
final String[] textItems = getResources().getStringArray(R.array.pref_auto_lock_types);
|
||||
final boolean[] checkedItems = new boolean[items.length];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
checkedItems[i] = _prefs.isAutoLockTypeEnabled(items[i]);
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.pref_auto_lock_prompt)
|
||||
.setMultiChoiceItems(textItems, checkedItems, (dialog, index, isChecked) -> checkedItems[index] = isChecked)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int autoLock = Preferences.AUTO_LOCK_OFF;
|
||||
for (int i = 0; i < checkedItems.length; i++) {
|
||||
if (checkedItems[i]) {
|
||||
autoLock |= items[i];
|
||||
}
|
||||
}
|
||||
|
||||
_prefs.setAutoLockMask(autoLock);
|
||||
_autoLockPreference.setSummary(getAutoLockSummary());
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
Dialogs.showSecureDialog(builder.create());
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
_passwordReminderPreference = findPreference("pref_password_reminder");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateEncryptionPreferences();
|
||||
updateBackupPreference();
|
||||
}
|
||||
@@ -458,8 +542,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
break;
|
||||
case CODE_EXPORT:
|
||||
// intentional fallthrough
|
||||
case CODE_EXPORT_ENCRYPT:
|
||||
onExportResult(resultCode, data, requestCode == CODE_EXPORT_ENCRYPT);
|
||||
case CODE_EXPORT_PLAIN:
|
||||
// intentional fallthrough
|
||||
case CODE_EXPORT_GOOGLE_URI:
|
||||
onExportResult(requestCode, resultCode, data);
|
||||
break;
|
||||
case CODE_BACKUPS:
|
||||
onSelectBackupsLocationResult(resultCode, data);
|
||||
@@ -476,42 +562,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
getActivity().setResult(Activity.RESULT_OK, _result);
|
||||
}
|
||||
|
||||
private void startImport() {
|
||||
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getImporters();
|
||||
String[] names = importers.keySet().toArray(new String[0]);
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.choose_application)
|
||||
.setSingleChoiceItems(names, 0, null)
|
||||
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
_importerType = importers.get(names[i]);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, CODE_IMPORT);
|
||||
}
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
private void onImportApp() {
|
||||
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getAppImporters();
|
||||
String[] names = importers.keySet().toArray(new String[0]);
|
||||
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.choose_application)
|
||||
.setSingleChoiceItems(names, 0, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
Class<? extends DatabaseImporter> importerType = Objects.requireNonNull(importers.get(names[i]));
|
||||
DatabaseImporter importer = DatabaseImporter.create(getContext(), importerType);
|
||||
importApp(importer);
|
||||
})
|
||||
.create());
|
||||
}
|
||||
|
||||
private void importApp(DatabaseImporter importer) {
|
||||
// obtain the global root shell and close it immediately after we're done
|
||||
// TODO: find a way to use SuFileInputStream with Shell.newInstance()
|
||||
@@ -521,11 +571,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return;
|
||||
}
|
||||
|
||||
SuFile file = importer.getAppPath();
|
||||
try (SuFileInputStream stream = new SuFileInputStream(file)) {
|
||||
DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream, true);
|
||||
importDatabase(importer, reader);
|
||||
}
|
||||
DatabaseImporter.State state = importer.readFromApp();
|
||||
processImporterState(state);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Toast.makeText(getActivity(), R.string.app_lookup_error, Toast.LENGTH_SHORT).show();
|
||||
@@ -535,9 +582,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
}
|
||||
|
||||
private void importDatabase(DatabaseImporter importer, DatabaseImporter.FileReader reader) {
|
||||
private void processImporterState(DatabaseImporter.State state) {
|
||||
try {
|
||||
DatabaseImporter.State state = importer.read(reader);
|
||||
if (state.isEncrypted()) {
|
||||
// temporary special case for encrypted Aegis vaults
|
||||
if (state instanceof AegisImporter.EncryptedState) {
|
||||
@@ -545,7 +591,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
Intent intent = new Intent(getActivity(), AuthActivity.class);
|
||||
intent.putExtra("slots", ((AegisImporter.EncryptedState) state).getSlots());
|
||||
intent.putExtra("cancelAction", CancelAction.CLOSE);
|
||||
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
|
||||
} else {
|
||||
state.decrypt(getActivity(), new DatabaseImporter.DecryptListener() {
|
||||
@@ -598,11 +643,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
try (InputStream stream = getContext().getContentResolver().openInputStream(uri)) {
|
||||
DatabaseImporter importer = DatabaseImporter.create(getContext(), _importerType);
|
||||
DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream);
|
||||
importDatabase(importer, reader);
|
||||
DatabaseImporter.State state = importer.read(stream);
|
||||
processImporterState(state);
|
||||
} catch (FileNotFoundException e) {
|
||||
Toast.makeText(getActivity(), R.string.file_not_found, Toast.LENGTH_SHORT).show();
|
||||
} catch (IOException e) {
|
||||
} catch (DatabaseImporterException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.reading_file_error, e);
|
||||
}
|
||||
@@ -627,39 +672,111 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
Intent intent = new Intent(getActivity(), SelectEntriesActivity.class);
|
||||
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
|
||||
intent.putExtra("errors", (ArrayList<DatabaseImporterEntryException>) result.getErrors());
|
||||
intent.putExtra("vaultContainsEntries", _vault.getEntries().size() > 0);
|
||||
startActivityForResult(intent, CODE_SELECT_ENTRIES);
|
||||
}
|
||||
|
||||
private void startExport() {
|
||||
// TODO: create a custom layout to show a message AND a checkbox
|
||||
final AtomicReference<Boolean> checked = new AtomicReference<>(true);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_export, null);
|
||||
TextView warningText = view.findViewById(R.id.text_export_warning);
|
||||
CheckBox checkBoxEncrypt = view.findViewById(R.id.checkbox_export_encrypt);
|
||||
CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept);
|
||||
Spinner spinner = view.findViewById(R.id.spinner_export_format);
|
||||
SpinnerHelper.fillSpinner(getContext(), spinner, R.array.export_formats);
|
||||
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
checkBoxEncrypt.setChecked(position == 0);
|
||||
checkBoxEncrypt.setEnabled(position == 0);
|
||||
warningText.setVisibility(checkBoxEncrypt.isChecked() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.pref_export_summary)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
String filename = checked.get() ? VaultManager.FILENAME_EXPORT : VaultManager.FILENAME_EXPORT_PLAIN;
|
||||
filename = new VaultBackupManager.FileInfo(filename).toString();
|
||||
.setView(view)
|
||||
.setNeutralButton(R.string.share, null)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType("application/json")
|
||||
.putExtra(Intent.EXTRA_TITLE, filename);
|
||||
dialog.setOnShowListener(d -> {
|
||||
Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
Button btnNeutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
|
||||
startActivityForResult(intent, checked.get() ? CODE_EXPORT_ENCRYPT : CODE_EXPORT);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
if (_vault.isEncryptionEnabled()) {
|
||||
final String[] items = {"Keep the vault encrypted"};
|
||||
final boolean[] checkedItems = {true};
|
||||
builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int index, boolean isChecked) {
|
||||
checked.set(isChecked);
|
||||
}
|
||||
checkBoxEncrypt.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
warningText.setVisibility(isChecked ? View.GONE : View.VISIBLE);
|
||||
checkBoxAccept.setVisibility(isChecked ? View.GONE : View.VISIBLE);
|
||||
checkBoxAccept.setChecked(false);
|
||||
btnPos.setEnabled(isChecked);
|
||||
btnNeutral.setEnabled(isChecked);
|
||||
});
|
||||
} else {
|
||||
builder.setMessage(R.string.export_warning);
|
||||
}
|
||||
Dialogs.showSecureDialog(builder.create());
|
||||
|
||||
checkBoxAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
btnPos.setEnabled(isChecked);
|
||||
btnNeutral.setEnabled(isChecked);
|
||||
});
|
||||
|
||||
btnPos.setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
|
||||
if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int requestCode = getExportRequestCode(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked());
|
||||
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked());
|
||||
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType(getExportMimeType(requestCode))
|
||||
.putExtra(Intent.EXTRA_TITLE, fileInfo.toString());
|
||||
startActivityForResult(intent, requestCode);
|
||||
});
|
||||
|
||||
btnNeutral.setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
|
||||
if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
File file;
|
||||
try {
|
||||
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked());
|
||||
file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
int requestCode = getExportRequestCode(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked());
|
||||
startExportVault(requestCode, cb -> {
|
||||
try (OutputStream stream = new FileOutputStream(file)) {
|
||||
cb.exportVault(stream);
|
||||
} catch (IOException | VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = FileProvider.getUriForFile(getContext(), BuildConfig.FILE_PROVIDER_AUTHORITY, file);
|
||||
Intent intent = new Intent(Intent.ACTION_SEND)
|
||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setType(getExportMimeType(requestCode))
|
||||
.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
Intent chooser = Intent.createChooser(intent, getString(R.string.pref_export_summary));
|
||||
startActivity(chooser);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private void onSlotManagerResult(int resultCode, Intent data) {
|
||||
@@ -685,6 +802,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
entry.setGroup(null);
|
||||
}
|
||||
}
|
||||
|
||||
saveVault();
|
||||
}
|
||||
|
||||
private void onSelectEntriesResult(int resultCode, Intent data) {
|
||||
@@ -692,6 +811,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean wipeEntries = data.getBooleanExtra("wipeEntries", false);
|
||||
if (wipeEntries) {
|
||||
_vault.wipeEntries();
|
||||
}
|
||||
|
||||
List<ImportEntry> selectedEntries = (ArrayList<ImportEntry>) data.getSerializableExtra("entries");
|
||||
for (ImportEntry selectedEntry : selectedEntries) {
|
||||
VaultEntry savedEntry = _importerEntries.getByUUID(selectedEntry.getUUID());
|
||||
@@ -709,27 +833,109 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return;
|
||||
}
|
||||
|
||||
String toastMessage = getResources().getString(R.string.imported_entries_count, selectedEntries.size());
|
||||
String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size());
|
||||
Toast.makeText(getContext(), toastMessage, Toast.LENGTH_SHORT).show();
|
||||
|
||||
_result.putExtra("needsRecreate", true);
|
||||
}
|
||||
|
||||
private void onExportResult(int resultCode, Intent data, boolean encrypt) {
|
||||
private static int getExportRequestCode(int spinnerPos, boolean encrypt) {
|
||||
if (spinnerPos == 0) {
|
||||
return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN;
|
||||
}
|
||||
|
||||
return CODE_EXPORT_GOOGLE_URI;
|
||||
}
|
||||
|
||||
private static VaultBackupManager.FileInfo getExportFileInfo(int spinnerPos, boolean encrypt) {
|
||||
if (spinnerPos == 0) {
|
||||
String filename = encrypt ? VaultManager.FILENAME_PREFIX_EXPORT : VaultManager.FILENAME_PREFIX_EXPORT_PLAIN;
|
||||
return new VaultBackupManager.FileInfo(filename);
|
||||
}
|
||||
|
||||
return new VaultBackupManager.FileInfo(VaultManager.FILENAME_PREFIX_EXPORT_URI, "txt");
|
||||
}
|
||||
|
||||
private static String getExportMimeType(int requestCode) {
|
||||
return requestCode == CODE_EXPORT_GOOGLE_URI ? "text/plain" : "application/json";
|
||||
}
|
||||
|
||||
private File getExportCacheDir() throws IOException {
|
||||
File dir = new File(getContext().getCacheDir(), "export");
|
||||
if (!dir.exists() && !dir.mkdir()) {
|
||||
throw new IOException(String.format("Unable to create directory %s", dir));
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
private void startExportVault(int requestCode, StartExportCallback cb) {
|
||||
switch (requestCode) {
|
||||
case CODE_EXPORT:
|
||||
if (_vault.isEncryptionEnabled()) {
|
||||
cb.exportVault(stream -> _vault.export(stream));
|
||||
} else {
|
||||
Dialogs.showSetPasswordDialog(getActivity(), new Dialogs.SlotListener() {
|
||||
@Override
|
||||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
VaultFileCredentials creds = new VaultFileCredentials();
|
||||
|
||||
try {
|
||||
slot.setKey(creds.getKey(), cipher);
|
||||
creds.getSlots().add(slot);
|
||||
} catch (SlotException e) {
|
||||
onException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
cb.exportVault(stream -> _vault.export(stream, creds));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case CODE_EXPORT_PLAIN:
|
||||
cb.exportVault((stream) -> _vault.export(stream, null));
|
||||
break;
|
||||
case CODE_EXPORT_GOOGLE_URI:
|
||||
cb.exportVault((stream) -> _vault.exportGoogleUris(stream));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onExportResult(int requestCode, int resultCode, Intent data) {
|
||||
Uri uri = data.getData();
|
||||
if (resultCode != Activity.RESULT_OK || uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (OutputStream stream = getContext().getContentResolver().openOutputStream(uri, "w")) {
|
||||
_vault.export(stream, encrypt);
|
||||
} catch (IOException | VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(getActivity(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
|
||||
startExportVault(requestCode, cb -> {
|
||||
File file;
|
||||
OutputStream outStream = null;
|
||||
try {
|
||||
file = File.createTempFile(VaultManager.FILENAME_PREFIX_EXPORT + "-", ".json", getExportCacheDir());
|
||||
outStream = new FileOutputStream(file);
|
||||
cb.exportVault(outStream);
|
||||
|
||||
new ExportTask(getContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri));
|
||||
} catch (VaultManagerException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
|
||||
} finally {
|
||||
try {
|
||||
if (outStream != null) {
|
||||
outStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onSelectBackupsLocationResult(int resultCode, Intent data) {
|
||||
@@ -743,13 +949,14 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
_prefs.setBackupsLocation(uri);
|
||||
_prefs.setIsBackupsEnabled(true);
|
||||
_prefs.setBackupsError(null);
|
||||
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
|
||||
updateBackupPreference();
|
||||
}
|
||||
|
||||
private boolean saveVault() {
|
||||
try {
|
||||
_vault.save();
|
||||
_vault.save(true);
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.saving_error, e);
|
||||
@@ -766,6 +973,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
_biometricsPreference.setVisible(encrypted);
|
||||
_slotsPreference.setEnabled(encrypted);
|
||||
_autoLockPreference.setVisible(encrypted);
|
||||
_pinKeyboardPreference.setVisible(encrypted);
|
||||
|
||||
if (encrypted) {
|
||||
SlotList slots = _vault.getCredentials().getSlots();
|
||||
@@ -805,10 +1013,35 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||
|
||||
startActivityForResult(intent, CODE_BACKUPS);
|
||||
}
|
||||
|
||||
private void setPinKeyboardPreference(boolean enable) {
|
||||
_pinKeyboardPreference.setChecked(enable);
|
||||
}
|
||||
|
||||
private String getAutoLockSummary() {
|
||||
final int[] settings = Preferences.AUTO_LOCK_SETTINGS;
|
||||
final String[] descriptions = getResources().getStringArray(R.array.pref_auto_lock_types);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < settings.length; i++) {
|
||||
if (_prefs.isAutoLockTypeEnabled(settings[i])) {
|
||||
if (builder.length() != 0) {
|
||||
builder.append(", ");
|
||||
}
|
||||
|
||||
builder.append(descriptions[i].toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.length() == 0) {
|
||||
return getString(R.string.pref_auto_lock_summary_disabled);
|
||||
}
|
||||
|
||||
return getString(R.string.pref_auto_lock_summary, builder.toString());
|
||||
}
|
||||
|
||||
private class SetPasswordListener implements Dialogs.SlotListener {
|
||||
@Override
|
||||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
@@ -821,7 +1054,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
// remove the old master password slot
|
||||
PasswordSlot oldSlot = creds.getSlots().find(PasswordSlot.class);
|
||||
slots.remove(oldSlot);
|
||||
if (oldSlot != null) {
|
||||
slots.remove(oldSlot);
|
||||
}
|
||||
|
||||
// add the new master password slot
|
||||
slots.add(slot);
|
||||
@@ -832,6 +1067,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
_vault.setCredentials(creds);
|
||||
saveVault();
|
||||
|
||||
if (_prefs.isPinKeyboardEnabled()) {
|
||||
setPinKeyboardPreference(false);
|
||||
Toast.makeText(getContext(), R.string.pin_keyboard_disabled, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -892,4 +1132,41 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
|
||||
}
|
||||
}
|
||||
|
||||
private class PasswordConfirmationListener implements PasswordSlotDecryptTask.Callback {
|
||||
@Override
|
||||
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
|
||||
if (result != null) {
|
||||
setPinKeyboardPreference(true);
|
||||
} else {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.pin_keyboard_error)
|
||||
.setMessage(R.string.invalid_password)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create());
|
||||
setPinKeyboardPreference(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ExportResultListener implements ExportTask.Callback {
|
||||
@Override
|
||||
public void onTaskFinished(Exception e) {
|
||||
if (e != null) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
|
||||
} else {
|
||||
Toast.makeText(getContext(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface FinishExportCallback {
|
||||
void exportVault(OutputStream stream) throws IOException, VaultManagerException;
|
||||
}
|
||||
|
||||
private interface StartExportCallback {
|
||||
void exportVault(FinishExportCallback exportCb);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,133 +1,166 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.Camera;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.CameraInfoUnavailableException;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.ImageAnalysis;
|
||||
import androidx.camera.core.Preview;
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||
import androidx.camera.view.PreviewView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.SquareFinderView;
|
||||
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.vault.VaultEntry;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.zxing.Result;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import me.dm7.barcodescanner.core.IViewFinder;
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView;
|
||||
public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Listener {
|
||||
private ProcessCameraProvider _cameraProvider;
|
||||
private ListenableFuture<ProcessCameraProvider> _cameraProviderFuture;
|
||||
|
||||
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
|
||||
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
|
||||
private List<Integer> _lenses;
|
||||
private int _currentLens;
|
||||
|
||||
public class ScannerActivity extends AegisActivity implements ZXingScannerView.ResultHandler {
|
||||
private ZXingScannerView _scannerView;
|
||||
private Menu _menu;
|
||||
private int _facing = CAMERA_FACING_BACK;
|
||||
private PreviewView _previewView;
|
||||
|
||||
private int _batchId = 0;
|
||||
private int _batchIndex = -1;
|
||||
private List<VaultEntry> _entries;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_scanner);
|
||||
|
||||
_entries = new ArrayList<>();
|
||||
_scannerView = new ZXingScannerView(this) {
|
||||
@Override
|
||||
protected IViewFinder createViewFinderView(Context context) {
|
||||
return new SquareFinderView(context);
|
||||
}
|
||||
};
|
||||
_scannerView.setResultHandler(this);
|
||||
_scannerView.setFormats(Collections.singletonList(BarcodeFormat.QR_CODE));
|
||||
_lenses = new ArrayList<>();
|
||||
_previewView = findViewById(R.id.preview_view);
|
||||
|
||||
int camera = getRearCameraId();
|
||||
if (camera == -1) {
|
||||
camera = getFrontCameraId();
|
||||
if (camera == -1) {
|
||||
_cameraProviderFuture = ProcessCameraProvider.getInstance(this);
|
||||
_cameraProviderFuture.addListener(() -> {
|
||||
try {
|
||||
_cameraProvider = _cameraProviderFuture.get();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
// if we're to believe the Android documentation, this should never happen
|
||||
// https://developer.android.com/training/camerax/preview#check-provider
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
addCamera(CameraSelector.LENS_FACING_BACK);
|
||||
addCamera(CameraSelector.LENS_FACING_FRONT);
|
||||
if (_lenses.size() == 0) {
|
||||
Toast.makeText(this, getString(R.string.no_cameras_available), Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
_facing = CAMERA_FACING_FRONT;
|
||||
}
|
||||
_scannerView.startCamera(camera);
|
||||
_currentLens = _lenses.get(0);
|
||||
updateCameraIcon();
|
||||
|
||||
setContentView(_scannerView);
|
||||
bindPreview(_cameraProvider);
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPreferredTheme(Theme theme) {
|
||||
setTheme(R.style.AppTheme_Fullscreen);
|
||||
protected void onSetTheme() {
|
||||
setTheme(ThemeMap.FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
_menu = menu;
|
||||
getMenuInflater().inflate(R.menu.menu_scanner, menu);
|
||||
updateCameraIcon();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_camera:
|
||||
_scannerView.stopCamera();
|
||||
switch (_facing) {
|
||||
case CAMERA_FACING_BACK:
|
||||
_facing = CAMERA_FACING_FRONT;
|
||||
break;
|
||||
case CAMERA_FACING_FRONT:
|
||||
_facing = CAMERA_FACING_BACK;
|
||||
break;
|
||||
}
|
||||
updateCameraIcon();
|
||||
_scannerView.startCamera(getCameraId(_facing));
|
||||
return true;
|
||||
case R.id.action_lock:
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == R.id.action_camera) {
|
||||
_cameraProvider.unbindAll();
|
||||
_currentLens = _currentLens == CameraSelector.LENS_FACING_BACK ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
|
||||
bindPreview(_cameraProvider);
|
||||
updateCameraIcon();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void addCamera(int lens) {
|
||||
try {
|
||||
CameraSelector camera = new CameraSelector.Builder().requireLensFacing(lens).build();
|
||||
if (_cameraProvider.hasCamera(camera)) {
|
||||
_lenses.add(lens);
|
||||
}
|
||||
} catch (CameraInfoUnavailableException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
_scannerView.startCamera(getCameraId(_facing));
|
||||
private void updateCameraIcon() {
|
||||
if (_menu != null) {
|
||||
MenuItem item = _menu.findItem(R.id.action_camera);
|
||||
boolean dual = _lenses.size() > 1;
|
||||
if (dual) {
|
||||
switch (_currentLens) {
|
||||
case CameraSelector.LENS_FACING_BACK:
|
||||
item.setIcon(R.drawable.ic_camera_front_24dp);
|
||||
break;
|
||||
case CameraSelector.LENS_FACING_FRONT:
|
||||
item.setIcon(R.drawable.ic_camera_rear_24dp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
item.setVisible(dual);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
|
||||
Preview preview = new Preview.Builder().build();
|
||||
preview.setSurfaceProvider(_previewView.getSurfaceProvider());
|
||||
|
||||
CameraSelector selector = new CameraSelector.Builder()
|
||||
.requireLensFacing(_currentLens)
|
||||
.build();
|
||||
|
||||
ImageAnalysis analysis = new ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build();
|
||||
analysis.setAnalyzer(ContextCompat.getMainExecutor(this), new QrCodeAnalyzer(this));
|
||||
|
||||
cameraProvider.bindToLifecycle(this, selector, preview, analysis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
_scannerView.stopCamera();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(Result rawResult) {
|
||||
public void onQrCodeDetected(Result result) {
|
||||
try {
|
||||
Uri uri = Uri.parse(rawResult.getText().trim());
|
||||
Uri uri = Uri.parse(result.getText().trim());
|
||||
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
|
||||
handleExportUri(uri);
|
||||
} else {
|
||||
handleUri(uri);
|
||||
}
|
||||
|
||||
_scannerView.resumeCameraPreview(this);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.read_qr_error, e, (dialog, which) -> {
|
||||
_scannerView.resumeCameraPreview(this);
|
||||
});
|
||||
Dialogs.showErrorDialog(this,
|
||||
e.isPhoneFactor() ? R.string.read_qr_error_phonefactor : R.string.read_qr_error,
|
||||
e, ((dialog, which) -> bindPreview(_cameraProvider)));
|
||||
_cameraProvider.unbindAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +192,7 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
||||
finish(_entries);
|
||||
}
|
||||
|
||||
Toast.makeText(this, getString(R.string.google_qr_export_scanned, _batchIndex + 1, export.getBatchSize()), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, getResources().getQuantityString(R.plurals.google_qr_export_scanned, export.getBatchSize(), _batchIndex + 1, export.getBatchSize()), Toast.LENGTH_SHORT).show();
|
||||
} else if (_batchIndex != batchIndex) {
|
||||
Toast.makeText(this, getString(R.string.google_qr_export_unexpected, _batchIndex + 1, batchIndex + 1), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
@@ -171,42 +204,4 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void updateCameraIcon() {
|
||||
if (_menu != null) {
|
||||
MenuItem item = _menu.findItem(R.id.action_camera);
|
||||
boolean dual = getFrontCameraId() != -1 && getRearCameraId() != -1;
|
||||
if (dual) {
|
||||
switch (_facing) {
|
||||
case CAMERA_FACING_BACK:
|
||||
item.setIcon(R.drawable.ic_camera_front_24dp);
|
||||
break;
|
||||
case CAMERA_FACING_FRONT:
|
||||
item.setIcon(R.drawable.ic_camera_rear_24dp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
item.setVisible(dual);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getCameraId(int facing) {
|
||||
Camera.CameraInfo info = new Camera.CameraInfo();
|
||||
for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
|
||||
Camera.getCameraInfo(i, info);
|
||||
if (info.facing == facing) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int getRearCameraId() {
|
||||
return getCameraId(CAMERA_FACING_BACK);
|
||||
}
|
||||
|
||||
private static int getFrontCameraId() {
|
||||
return getCameraId(CAMERA_FACING_FRONT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
@@ -16,6 +10,12 @@ import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
|
||||
@@ -25,10 +25,12 @@ import com.getbase.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class SelectEntriesActivity extends AegisActivity {
|
||||
private ImportEntriesAdapter _adapter;
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
private boolean _vaultContainsEntries;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -57,6 +59,7 @@ public class SelectEntriesActivity extends AegisActivity {
|
||||
Intent intent = getIntent();
|
||||
List<ImportEntry> entries = (ArrayList<ImportEntry>) intent.getSerializableExtra("entries");
|
||||
List<DatabaseImporterEntryException> errors = (ArrayList<DatabaseImporterEntryException>) intent.getSerializableExtra("errors");
|
||||
_vaultContainsEntries = intent.getBooleanExtra("vaultContainsEntries", false);
|
||||
|
||||
for (ImportEntry entry : entries) {
|
||||
_adapter.addEntry(entry);
|
||||
@@ -67,14 +70,20 @@ public class SelectEntriesActivity extends AegisActivity {
|
||||
}
|
||||
|
||||
FloatingActionButton fabMenu = findViewById(R.id.fab);
|
||||
fabMenu.setOnClickListener(v -> returnSelectedEntries());
|
||||
fabMenu.setOnClickListener(v -> {
|
||||
if (_vaultContainsEntries) {
|
||||
showWipeEntriesDialog();
|
||||
} else {
|
||||
returnSelectedEntries(false);
|
||||
}
|
||||
});
|
||||
_fabScrollHelper = new FabScrollHelper(fabMenu);
|
||||
}
|
||||
|
||||
private void showErrorDialog(List<DatabaseImporterEntryException> errors) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.import_error_title)
|
||||
.setMessage(getString(R.string.import_error_dialog, errors.size()))
|
||||
.setMessage(getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size()))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(getString(R.string.details), (dialog, which) -> showDetailedErrorDialog(errors))
|
||||
.create());
|
||||
@@ -100,10 +109,19 @@ public class SelectEntriesActivity extends AegisActivity {
|
||||
.create());
|
||||
}
|
||||
|
||||
private void returnSelectedEntries() {
|
||||
List<ImportEntry> entries = _adapter.getCheckedEntries();
|
||||
private void showWipeEntriesDialog() {
|
||||
Dialogs.showCheckboxDialog(this, R.string.dialog_wipe_entries_title,
|
||||
R.string.dialog_wipe_entries_message,
|
||||
R.string.dialog_wipe_entries_checkbox,
|
||||
this::returnSelectedEntries
|
||||
);
|
||||
}
|
||||
|
||||
private void returnSelectedEntries(boolean wipeEntries) {
|
||||
Intent intent = new Intent();
|
||||
List<ImportEntry> entries = _adapter.getCheckedEntries();
|
||||
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
|
||||
intent.putExtra("wipeEntries", wipeEntries);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TransferEntriesActivity extends AegisActivity {
|
||||
private List<GoogleAuthInfo> _authInfos;
|
||||
private ImageView _qrImage;
|
||||
private TextView _issuer;
|
||||
private TextView _accountName;
|
||||
private TextView _entriesCount;
|
||||
private Button _nextButton;
|
||||
private Button _previousButton;
|
||||
|
||||
private VaultManager _vault;
|
||||
private int _currentEntryCount = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_share_entry);
|
||||
_vault = getApp().getVaultManager();
|
||||
|
||||
_qrImage = findViewById(R.id.ivQrCode);
|
||||
_issuer = findViewById(R.id.tvIssuer);
|
||||
_accountName = findViewById(R.id.tvAccountName);
|
||||
_entriesCount = findViewById(R.id.tvEntriesCount);
|
||||
_nextButton = findViewById(R.id.btnNext);
|
||||
_previousButton = findViewById(R.id.btnPrevious);
|
||||
|
||||
if (getSupportActionBar() != null){
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
_authInfos = (ArrayList<GoogleAuthInfo>) intent.getSerializableExtra("authInfos");
|
||||
|
||||
int controlVisibility = _authInfos.size() != 1 ? View.VISIBLE : View.INVISIBLE;
|
||||
_nextButton.setVisibility(controlVisibility);
|
||||
|
||||
_nextButton.setOnClickListener(v -> {
|
||||
if (_currentEntryCount < _authInfos.size()) {
|
||||
_previousButton.setVisibility(View.VISIBLE);
|
||||
_currentEntryCount++;
|
||||
generateQR();
|
||||
|
||||
if (_currentEntryCount == _authInfos.size()) {
|
||||
_nextButton.setText(R.string.done);
|
||||
}
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
_previousButton.setOnClickListener(v -> {
|
||||
if (_currentEntryCount > 1 ) {
|
||||
_nextButton.setText(R.string.next);
|
||||
_currentEntryCount--;
|
||||
generateQR();
|
||||
|
||||
if (_currentEntryCount == 1) {
|
||||
_previousButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
generateQR();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void generateQR() {
|
||||
GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1);
|
||||
_issuer.setText(selectedEntry.getIssuer());
|
||||
_accountName.setText(selectedEntry.getAccountName());
|
||||
_entriesCount.setText(getResources().getQuantityString(R.plurals.entries_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
|
||||
|
||||
QRCodeWriter writer = new QRCodeWriter();
|
||||
BitMatrix bitMatrix = null;
|
||||
try {
|
||||
bitMatrix = writer.encode(selectedEntry.getUri().toString(), BarcodeFormat.QR_CODE, 512, 512);
|
||||
} catch (WriterException e) {
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
|
||||
return;
|
||||
}
|
||||
|
||||
int width = bitMatrix.getWidth();
|
||||
int height = bitMatrix.getHeight();
|
||||
int[] pixels = new int[width * height];
|
||||
for (int y = 0; y < height; y++) {
|
||||
int offset = y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
|
||||
_qrImage.setImageBitmap(bitmap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.beemdevelopment.aegis.ui.intro;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface IntroActivityInterface {
|
||||
/**
|
||||
* Navigate to the next slide.
|
||||
*/
|
||||
void goToNextSlide();
|
||||
|
||||
/**
|
||||
* Navigate to the previous slide.
|
||||
*/
|
||||
void goToPreviousSlide();
|
||||
|
||||
/**
|
||||
* Navigate to the slide of the given type.
|
||||
*/
|
||||
void skipToSlide(Class<? extends SlideFragment> type);
|
||||
|
||||
/**
|
||||
* Retrieves the state of the intro. The state is shared among all slides and is
|
||||
* properly restored after a configuration change. This method may only be called
|
||||
* after onAttach has been called.
|
||||
*/
|
||||
@NonNull
|
||||
Bundle getState();
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package com.beemdevelopment.aegis.ui.intro;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ui.AegisActivity;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class IntroBaseActivity extends AegisActivity implements IntroActivityInterface {
|
||||
private Bundle _state;
|
||||
private ViewPager2 _pager;
|
||||
private ScreenSlidePagerAdapter _adapter;
|
||||
private List<Class<? extends SlideFragment>> _slides;
|
||||
private WeakReference<SlideFragment> _currentSlide;
|
||||
|
||||
private ImageButton _btnPrevious;
|
||||
private ImageButton _btnNext;
|
||||
private SlideIndicator _slideIndicator;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_intro);
|
||||
|
||||
_slides = new ArrayList<>();
|
||||
_state = new Bundle();
|
||||
|
||||
_btnPrevious = findViewById(R.id.btnPrevious);
|
||||
_btnPrevious.setOnClickListener(v -> goToPreviousSlide());
|
||||
_btnNext = findViewById(R.id.btnNext);
|
||||
_btnNext.setOnClickListener(v -> goToNextSlide());
|
||||
_slideIndicator = findViewById(R.id.slideIndicator);
|
||||
|
||||
_adapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
|
||||
_pager = findViewById(R.id.pager);
|
||||
_pager.setAdapter(_adapter);
|
||||
_pager.setUserInputEnabled(false);
|
||||
_pager.registerOnPageChangeCallback(new SlideSkipBlocker());
|
||||
|
||||
View pagerChild = _pager.getChildAt(0);
|
||||
if (pagerChild instanceof RecyclerView) {
|
||||
pagerChild.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
_state = savedInstanceState.getBundle("introState");
|
||||
updatePagerControls();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBundle("introState", _state);
|
||||
}
|
||||
|
||||
void setCurrentSlide(SlideFragment slide) {
|
||||
_currentSlide = new WeakReference<>(slide);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goToNextSlide() {
|
||||
int pos = _pager.getCurrentItem();
|
||||
if (pos != _slides.size() - 1) {
|
||||
SlideFragment currentSlide = _currentSlide.get();
|
||||
if (currentSlide.isFinished()) {
|
||||
currentSlide.onSaveIntroState(_state);
|
||||
setPagerPosition(pos, 1);
|
||||
} else {
|
||||
currentSlide.onNotFinishedError();
|
||||
}
|
||||
} else {
|
||||
onDonePressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goToPreviousSlide() {
|
||||
int pos = _pager.getCurrentItem();
|
||||
if (pos != 0 && pos != _slides.size() - 1) {
|
||||
setPagerPosition(pos, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skipToSlide(Class<? extends SlideFragment> type) {
|
||||
int i = _slides.indexOf(type);
|
||||
if (i == -1) {
|
||||
throw new IllegalStateException(String.format("Cannot skip to slide of type %s because it is not in the slide list", type.getName()));
|
||||
}
|
||||
|
||||
setPagerPosition(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before a slide change is made. Overriding gives implementers the
|
||||
* opportunity to block a slide change. onSaveIntroState is guaranteed to have been
|
||||
* called on oldSlide before onBeforeSlideChanged is called.
|
||||
* @param oldSlide the slide that is currently shown.
|
||||
* @param newSlide the next slide that will be shown.
|
||||
* @return whether to block the transition.
|
||||
*/
|
||||
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a slide change was made.
|
||||
* @param oldSlide the slide that was previously shown.
|
||||
* @param newSlide the slide that is now shown.
|
||||
*/
|
||||
protected void onAfterSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
|
||||
|
||||
}
|
||||
|
||||
private void setPagerPosition(int pos) {
|
||||
Class<? extends SlideFragment> oldSlide = _currentSlide.get().getClass();
|
||||
Class<? extends SlideFragment> newSlide = _slides.get(pos);
|
||||
|
||||
if (!onBeforeSlideChanged(oldSlide, newSlide)) {
|
||||
_pager.setCurrentItem(pos);
|
||||
}
|
||||
onAfterSlideChanged(oldSlide, newSlide);
|
||||
|
||||
updatePagerControls();
|
||||
}
|
||||
|
||||
private void setPagerPosition(int pos, int delta) {
|
||||
pos += delta;
|
||||
setPagerPosition(pos);
|
||||
}
|
||||
|
||||
private void updatePagerControls() {
|
||||
int pos = _pager.getCurrentItem();
|
||||
_btnPrevious.setVisibility(
|
||||
pos != 0 && pos != _slides.size() - 1
|
||||
? View.VISIBLE
|
||||
: View.INVISIBLE);
|
||||
if (pos == _slides.size() - 1) {
|
||||
_btnNext.setImageResource(R.drawable.circular_button_done);
|
||||
}
|
||||
_slideIndicator.setSlideCount(_slides.size());
|
||||
_slideIndicator.setCurrentSlide(pos);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Bundle getState() {
|
||||
return _state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
goToPreviousSlide();
|
||||
}
|
||||
|
||||
protected abstract void onDonePressed();
|
||||
|
||||
public void addSlide(Class<? extends SlideFragment> type) {
|
||||
if (_slides.contains(type)) {
|
||||
throw new IllegalStateException(String.format("Only one slide of type %s may be added to the intro", type.getName()));
|
||||
}
|
||||
|
||||
_slides.add(type);
|
||||
_slideIndicator.setSlideCount(_slides.size());
|
||||
}
|
||||
|
||||
private class ScreenSlidePagerAdapter extends FragmentStateAdapter {
|
||||
public ScreenSlidePagerAdapter(FragmentManager fm) {
|
||||
super(fm, getLifecycle());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
Class<? extends SlideFragment> type = _slides.get(position);
|
||||
|
||||
try {
|
||||
return type.newInstance();
|
||||
} catch (IllegalAccessException | InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return _slides.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class SlideSkipBlocker extends ViewPager2.OnPageChangeCallback {
|
||||
@Override
|
||||
public void onPageScrollStateChanged(@ViewPager2.ScrollState int state) {
|
||||
// disable the buttons while scrolling to prevent disallowed skipping of slides
|
||||
boolean enabled = state == ViewPager2.SCROLL_STATE_IDLE;
|
||||
_btnNext.setEnabled(enabled);
|
||||
_btnPrevious.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.beemdevelopment.aegis.ui.intro;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public abstract class SlideFragment extends Fragment implements IntroActivityInterface {
|
||||
private WeakReference<IntroBaseActivity> _parent;
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(context instanceof IntroBaseActivity)) {
|
||||
throw new ClassCastException("Parent context is expected to be of type IntroBaseActivity");
|
||||
}
|
||||
|
||||
_parent = new WeakReference<>((IntroBaseActivity) context);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
getParent().setCurrentSlide(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether or not all required user actions are finished on this slide,
|
||||
* indicating that we're ready to move to the next slide.
|
||||
*/
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called if the user tried to move to the next slide, but isFinished returned false.
|
||||
*/
|
||||
protected void onNotFinishedError() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the SlideFragment is expected to write its state to the given shared
|
||||
* introState. This is only called if the user navigates to the next slide, not
|
||||
* when a previous slide is next to be shown.
|
||||
*/
|
||||
protected void onSaveIntroState(@NonNull Bundle introState) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goToNextSlide() {
|
||||
getParent().goToNextSlide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goToPreviousSlide() {
|
||||
getParent().goToPreviousSlide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skipToSlide(Class<? extends SlideFragment> type) {
|
||||
getParent().skipToSlide(type);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Bundle getState() {
|
||||
return getParent().getState();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private IntroBaseActivity getParent() {
|
||||
if (_parent == null || _parent.get() == null) {
|
||||
throw new IllegalStateException("This method must not be called before onAttach()");
|
||||
}
|
||||
|
||||
return _parent.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.beemdevelopment.aegis.ui.intro;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class SlideIndicator extends View {
|
||||
private Paint _paint;
|
||||
private int _slideCount;
|
||||
private int _slideIndex;
|
||||
|
||||
private float _dotRadius;
|
||||
private float _dotSeparator;
|
||||
private int _dotColor;
|
||||
private int _dotColorSelected;
|
||||
|
||||
public SlideIndicator(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
_paint = new Paint();
|
||||
_paint.setAntiAlias(true);
|
||||
_paint.setStyle(Paint.Style.FILL);
|
||||
|
||||
TypedArray array = null;
|
||||
try {
|
||||
array = context.obtainStyledAttributes(attrs, R.styleable.SlideIndicator);
|
||||
_dotRadius = array.getDimension(R.styleable.SlideIndicator_dot_radius, 5f);
|
||||
_dotSeparator = array.getDimension(R.styleable.SlideIndicator_dot_separation, 5f);
|
||||
_dotColor = array.getColor(R.styleable.SlideIndicator_dot_color, Color.GRAY);
|
||||
_dotColorSelected = array.getColor(R.styleable.SlideIndicator_dot_color_selected, Color.BLACK);
|
||||
} finally {
|
||||
if (array != null) {
|
||||
array.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setSlideCount(int slideCount) {
|
||||
if (slideCount < 0) {
|
||||
throw new IllegalArgumentException("Slide count cannot be negative");
|
||||
}
|
||||
|
||||
_slideCount = slideCount;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setCurrentSlide(int index) {
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("Slide index cannot be negative");
|
||||
}
|
||||
|
||||
if (index + 1 > _slideCount) {
|
||||
throw new IllegalStateException(String.format("Slide index out of range, slides: %d, index: %d", _slideCount, index));
|
||||
}
|
||||
|
||||
_slideIndex = index;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (_slideCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
float dotDp = density * _dotRadius * 2;
|
||||
float spaceDp = density * _dotSeparator;
|
||||
|
||||
float offset;
|
||||
if (_slideCount % 2 == 0) {
|
||||
offset = (spaceDp / 2) + (dotDp / 2) + dotDp * (_slideCount / 2f - 1) + spaceDp * (_slideCount / 2f - 1);
|
||||
} else {
|
||||
int spaces = _slideCount > 1 ? _slideCount - 2 : 0;
|
||||
offset = (_slideCount - 1) * (dotDp / 2) + spaces * spaceDp;
|
||||
}
|
||||
|
||||
canvas.translate((getWidth() / 2f) - offset,getHeight() / 2f);
|
||||
|
||||
for (int i = 0; i < _slideCount; i++) {
|
||||
int slideIndex = isRtl() ? (_slideCount - 1) - _slideIndex : _slideIndex;
|
||||
_paint.setColor(i == slideIndex ? _dotColorSelected : _dotColor);
|
||||
canvas.drawCircle(0,0, dotDp / 2, _paint);
|
||||
canvas.translate(dotDp + spaceDp,0);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRtl() {
|
||||
return getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.beemdevelopment.aegis.ui.slides;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.github.appintro.SlidePolicy;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
public class CustomAuthenticationSlide extends Fragment implements SlidePolicy, RadioGroup.OnCheckedChangeListener {
|
||||
public static final int CRYPT_TYPE_INVALID = 0;
|
||||
public static final int CRYPT_TYPE_NONE = 1;
|
||||
public static final int CRYPT_TYPE_PASS = 2;
|
||||
public static final int CRYPT_TYPE_BIOMETRIC = 3;
|
||||
|
||||
private RadioGroup _buttonGroup;
|
||||
private int _bgColor;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.fragment_authentication_slide, container, false);
|
||||
_buttonGroup = view.findViewById(R.id.rg_authenticationMethod);
|
||||
_buttonGroup.setOnCheckedChangeListener(this);
|
||||
onCheckedChanged(_buttonGroup, _buttonGroup.getCheckedRadioButtonId());
|
||||
|
||||
// only enable the fingerprint option if the api version is new enough, permission is granted and a scanner is found
|
||||
if (BiometricsHelper.isAvailable(getContext())) {
|
||||
RadioButton button = view.findViewById(R.id.rb_biometrics);
|
||||
TextView text = view.findViewById(R.id.text_rb_biometrics);
|
||||
button.setEnabled(true);
|
||||
text.setEnabled(true);
|
||||
_buttonGroup.check(R.id.rb_biometrics);
|
||||
}
|
||||
|
||||
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setBgColor(int color) {
|
||||
_bgColor = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPolicyRespected() {
|
||||
return _buttonGroup.getCheckedRadioButtonId() != -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserIllegallyRequestedNextPage() {
|
||||
Snackbar snackbar = Snackbar.make(getView(), getString(R.string.snackbar_authentication_method), Snackbar.LENGTH_LONG);
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(RadioGroup radioGroup, int i) {
|
||||
if (i == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
int id;
|
||||
switch (i) {
|
||||
case R.id.rb_none:
|
||||
id = CRYPT_TYPE_NONE;
|
||||
break;
|
||||
case R.id.rb_password:
|
||||
id = CRYPT_TYPE_PASS;
|
||||
break;
|
||||
case R.id.rb_biometrics:
|
||||
id = CRYPT_TYPE_BIOMETRIC;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException(String.format("Unsupported security setting: %d", i));
|
||||
}
|
||||
|
||||
Intent intent = getActivity().getIntent();
|
||||
intent.putExtra("cryptType", id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.beemdevelopment.aegis.ui.slides;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
|
||||
|
||||
public class DoneSlide extends SlideFragment {
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_done_slide, container, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.beemdevelopment.aegis.ui.slides;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
|
||||
|
||||
public class SecurityPickerSlide extends SlideFragment {
|
||||
public static final int CRYPT_TYPE_INVALID = 0;
|
||||
public static final int CRYPT_TYPE_NONE = 1;
|
||||
public static final int CRYPT_TYPE_PASS = 2;
|
||||
public static final int CRYPT_TYPE_BIOMETRIC = 3;
|
||||
|
||||
private RadioGroup _buttonGroup;
|
||||
private RadioButton _bioButton;
|
||||
private TextView _bioText;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_security_picker_slide, container, false);
|
||||
_buttonGroup = view.findViewById(R.id.rg_authenticationMethod);
|
||||
_bioButton = view.findViewById(R.id.rb_biometrics);
|
||||
_bioText = view.findViewById(R.id.text_rb_biometrics);
|
||||
updateBiometricsOption(true);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateBiometricsOption(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the status of the biometrics option. Auto-selects the biometrics option
|
||||
* if the API version is new enough, permission is granted and a scanner is found.
|
||||
*/
|
||||
private void updateBiometricsOption(boolean autoSelect) {
|
||||
boolean canUseBio = BiometricsHelper.isAvailable(getContext());
|
||||
_bioButton.setEnabled(canUseBio);
|
||||
_bioText.setEnabled(canUseBio);
|
||||
|
||||
if (!canUseBio && _buttonGroup.getCheckedRadioButtonId() == R.id.rb_biometrics) {
|
||||
_buttonGroup.check(R.id.rb_password);
|
||||
}
|
||||
|
||||
if (canUseBio && autoSelect) {
|
||||
_buttonGroup.check(R.id.rb_biometrics);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return _buttonGroup.getCheckedRadioButtonId() != -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotFinishedError() {
|
||||
Toast.makeText(getContext(), R.string.snackbar_authentication_method, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveIntroState(@NonNull Bundle introState) {
|
||||
int buttonId = _buttonGroup.getCheckedRadioButtonId();
|
||||
|
||||
int type;
|
||||
switch (buttonId) {
|
||||
case R.id.rb_none:
|
||||
type = CRYPT_TYPE_NONE;
|
||||
break;
|
||||
case R.id.rb_password:
|
||||
type = CRYPT_TYPE_PASS;
|
||||
break;
|
||||
case R.id.rb_biometrics:
|
||||
type = CRYPT_TYPE_BIOMETRIC;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException(String.format("Unsupported security type: %d", buttonId));
|
||||
}
|
||||
|
||||
introState.putInt("cryptType", type);
|
||||
}
|
||||
}
|
||||
+70
-48
@@ -1,52 +1,68 @@
|
||||
package com.beemdevelopment.aegis.ui.slides;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.PasswordTransformationMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
|
||||
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.IntroActivity;
|
||||
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
|
||||
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
|
||||
import com.beemdevelopment.aegis.vault.slots.Slot;
|
||||
import com.beemdevelopment.aegis.vault.slots.SlotException;
|
||||
import com.github.appintro.SlidePolicy;
|
||||
import com.github.appintro.SlideSelectionListener;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.nulabinc.zxcvbn.Strength;
|
||||
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, SlideSelectionListener {
|
||||
private int _bgColor;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_INVALID;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_NONE;
|
||||
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_PASS;
|
||||
|
||||
public class SecuritySetupSlide extends SlideFragment {
|
||||
private EditText _textPassword;
|
||||
private EditText _textPasswordConfirm;
|
||||
private CheckBox _checkPasswordVisibility;
|
||||
private ProgressBar _barPasswordStrength;
|
||||
private TextView _textPasswordStrength;
|
||||
private TextInputLayout _textPasswordWrapper;
|
||||
|
||||
private int _cryptType;
|
||||
private VaultFileCredentials _creds;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.fragment_authenticated_slide, container, false);
|
||||
View view = inflater.inflate(R.layout.fragment_security_setup_slide, container, false);
|
||||
_textPassword = view.findViewById(R.id.text_password);
|
||||
_textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
|
||||
_checkPasswordVisibility = view.findViewById(R.id.check_toggle_visibility);
|
||||
_barPasswordStrength = view.findViewById(R.id.progressBar);
|
||||
_textPasswordStrength = view.findViewById(R.id.text_password_strength);
|
||||
_textPasswordWrapper = view.findViewById(R.id.text_password_wrapper);
|
||||
|
||||
_checkPasswordVisibility.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
@@ -60,20 +76,41 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
|
||||
}
|
||||
});
|
||||
|
||||
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
|
||||
_textPassword.addTextChangedListener(new TextWatcher() {
|
||||
private Zxcvbn _zxcvbn = new Zxcvbn();
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
Strength strength = _zxcvbn.measure(_textPassword.getText());
|
||||
_barPasswordStrength.setProgress(strength.getScore());
|
||||
_barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(PasswordStrengthHelper.getColor(strength.getScore()))));
|
||||
_textPasswordStrength.setText((_textPassword.getText().length() != 0) ? PasswordStrengthHelper.getString(strength.getScore(), getContext()) : "");
|
||||
_textPasswordWrapper.setError(strength.getFeedback().getWarning());
|
||||
strength.wipe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public int getCryptType() {
|
||||
return _cryptType;
|
||||
}
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
public VaultFileCredentials getCredentials() {
|
||||
return _creds;
|
||||
}
|
||||
_cryptType = getState().getInt("cryptType", CRYPT_TYPE_INVALID);
|
||||
if (_cryptType == CRYPT_TYPE_INVALID || _cryptType == CRYPT_TYPE_NONE) {
|
||||
throw new RuntimeException(String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d", _cryptType));
|
||||
}
|
||||
|
||||
public void setBgColor(int color) {
|
||||
_bgColor = color;
|
||||
_creds = new VaultFileCredentials();
|
||||
}
|
||||
|
||||
private void showBiometricPrompt() {
|
||||
@@ -88,34 +125,21 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
|
||||
private void deriveKey() {
|
||||
PasswordSlot slot = new PasswordSlot();
|
||||
KeyDerivationTask.Params params = new KeyDerivationTask.Params(slot, EditTextHelper.getEditTextChars(_textPassword));
|
||||
new KeyDerivationTask(getContext(), new PasswordDerivationListener()).execute(params);
|
||||
KeyDerivationTask task = new KeyDerivationTask(getContext(), new PasswordDerivationListener());
|
||||
task.execute(getLifecycle(), params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlideSelected() {
|
||||
Intent intent = getActivity().getIntent();
|
||||
_cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
|
||||
if (_cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
_creds = new VaultFileCredentials();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlideDeselected() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPolicyRespected() {
|
||||
public boolean isFinished() {
|
||||
switch (_cryptType) {
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
|
||||
case CRYPT_TYPE_NONE:
|
||||
return true;
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC:
|
||||
case CRYPT_TYPE_BIOMETRIC:
|
||||
if (!_creds.getSlots().has(BiometricSlot.class)) {
|
||||
return false;
|
||||
}
|
||||
// intentional fallthrough
|
||||
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
|
||||
case CRYPT_TYPE_PASS:
|
||||
if (EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
|
||||
return _creds.getSlots().has(PasswordSlot.class);
|
||||
}
|
||||
@@ -127,23 +151,21 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserIllegallyRequestedNextPage() {
|
||||
String message;
|
||||
public void onNotFinishedError() {
|
||||
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
|
||||
message = getString(R.string.password_equality_error);
|
||||
|
||||
View view = getView();
|
||||
if (view != null) {
|
||||
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
|
||||
snackbar.show();
|
||||
}
|
||||
} else if (_cryptType != CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
|
||||
Toast.makeText(getContext(), R.string.password_equality_error, Toast.LENGTH_SHORT).show();
|
||||
} else if (_cryptType != SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC) {
|
||||
deriveKey();
|
||||
} else if (!_creds.getSlots().has(BiometricSlot.class)) {
|
||||
showBiometricPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveIntroState(@NonNull Bundle introState) {
|
||||
introState.putSerializable("creds", _creds);
|
||||
}
|
||||
|
||||
private class PasswordDerivationListener implements KeyDerivationTask.Callback {
|
||||
@Override
|
||||
public void onTaskFinished(PasswordSlot slot, SecretKey key) {
|
||||
@@ -157,7 +179,7 @@ public class CustomAuthenticatedSlide extends Fragment implements SlidePolicy, S
|
||||
return;
|
||||
}
|
||||
|
||||
((IntroActivity) getActivity()).goToNextSlide();
|
||||
goToNextSlide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.beemdevelopment.aegis.ui.slides;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
|
||||
|
||||
public class WelcomeSlide extends SlideFragment {
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_welcome_slide, container, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.beemdevelopment.aegis.ui.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class ExportTask extends ProgressDialogTask<ExportTask.Params, Exception> {
|
||||
private final Callback _cb;
|
||||
|
||||
public ExportTask(Context context, Callback cb) {
|
||||
super(context, context.getString(R.string.exporting_vault));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Exception doInBackground(ExportTask.Params... args) {
|
||||
setPriority();
|
||||
|
||||
ExportTask.Params params = args[0];
|
||||
try (InputStream inStream = new FileInputStream(params.getFile());
|
||||
OutputStream outStream = getDialog().getContext().getContentResolver().openOutputStream(params.getDestUri(), "w")) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Exception e) {
|
||||
super.onPostExecute(e);
|
||||
_cb.onTaskFinished(e);
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
private final File _file;
|
||||
private final Uri _destUri;
|
||||
|
||||
public Params(File file, Uri destUri) {
|
||||
_file = file;
|
||||
_destUri = destUri;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return _file;
|
||||
}
|
||||
|
||||
public Uri getDestUri() {
|
||||
return _destUri;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(Exception e);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
package com.beemdevelopment.aegis.ui.tasks;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Process;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.OnLifecycleEvent;
|
||||
|
||||
import com.beemdevelopment.aegis.ui.Dialogs;
|
||||
|
||||
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, String, Result> {
|
||||
private ProgressDialog _dialog;
|
||||
@@ -47,4 +52,29 @@ public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Param
|
||||
protected final ProgressDialog getDialog() {
|
||||
return _dialog;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final void execute(@Nullable Lifecycle lifecycle, Params... params) {
|
||||
if (lifecycle != null) {
|
||||
LifecycleObserver observer = new Observer(getDialog());
|
||||
lifecycle.addObserver(observer);
|
||||
}
|
||||
execute(params);
|
||||
}
|
||||
|
||||
private static class Observer implements LifecycleObserver {
|
||||
private Dialog _dialog;
|
||||
|
||||
public Observer(Dialog dialog) {
|
||||
_dialog = dialog;
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
void onPause() {
|
||||
if (_dialog != null && _dialog.isShowing()) {
|
||||
_dialog.dismiss();
|
||||
_dialog = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@@ -18,6 +19,7 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
@@ -35,8 +37,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
private boolean _showAccountName;
|
||||
private boolean _searchAccountName;
|
||||
private boolean _highlightEntry;
|
||||
private boolean _tempHighlightEntry;
|
||||
private boolean _tapToReveal;
|
||||
private int _tapToRevealTime;
|
||||
private boolean _copyOnTap;
|
||||
private String _groupFilter;
|
||||
private SortCategory _sortCategory;
|
||||
private ViewMode _viewMode;
|
||||
@@ -47,6 +51,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
|
||||
// keeps track of the viewholders that are currently bound
|
||||
private List<EntryHolder> _holders;
|
||||
private EntryHolder _dragHandleHolder; // holder with enabled drag handle
|
||||
|
||||
public EntryAdapter(EntryListView view) {
|
||||
_entries = new ArrayList<>();
|
||||
@@ -88,17 +93,25 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
_highlightEntry = highlightEntry;
|
||||
}
|
||||
|
||||
public void setTempHighlightEntry(boolean highlightEntry) {
|
||||
_tempHighlightEntry = highlightEntry;
|
||||
}
|
||||
|
||||
public void setIsCopyOnTapEnabled(boolean enabled) {
|
||||
_copyOnTap = enabled;
|
||||
}
|
||||
|
||||
public VaultEntry getEntryAt(int position) {
|
||||
return _shownEntries.get(position);
|
||||
}
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
public int addEntry(VaultEntry entry) {
|
||||
_entries.add(entry);
|
||||
if (isEntryFiltered(entry)) {
|
||||
return;
|
||||
return -1;
|
||||
}
|
||||
|
||||
boolean added = false;
|
||||
int position = -1;
|
||||
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
|
||||
if (comparator != null) {
|
||||
// insert the entry in the correct order
|
||||
@@ -107,16 +120,16 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
if (comparator.compare(_shownEntries.get(i), entry) > 0) {
|
||||
_shownEntries.add(i, entry);
|
||||
notifyItemInserted(i);
|
||||
added = true;
|
||||
position = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!added){
|
||||
if (position < 0){
|
||||
_shownEntries.add(entry);
|
||||
|
||||
int position = getItemCount() - 1;
|
||||
position = getItemCount() - 1;
|
||||
if (position == 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
@@ -124,10 +137,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
}
|
||||
}
|
||||
|
||||
_view.onListChange();
|
||||
checkPeriodUniformity();
|
||||
return position;
|
||||
}
|
||||
|
||||
public void addEntries(List<VaultEntry> entries) {
|
||||
public void addEntries(Collection<VaultEntry> entries) {
|
||||
_entries.addAll(entries);
|
||||
updateShownEntries();
|
||||
checkPeriodUniformity(true);
|
||||
@@ -142,6 +157,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
_view.onListChange();
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
@@ -218,7 +234,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
for (EntryHolder holder : _holders) {
|
||||
holder.refreshCode();
|
||||
holder.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,6 +283,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
Collections.sort(_shownEntries, comparator);
|
||||
}
|
||||
|
||||
_view.onListChange();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -330,7 +347,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
VaultEntry entry = _shownEntries.get(position);
|
||||
|
||||
boolean hidden = _tapToReveal && entry != _focusedEntry;
|
||||
boolean dimmed = _highlightEntry && _focusedEntry != null && _focusedEntry != entry;
|
||||
boolean dimmed = (_highlightEntry || _tempHighlightEntry) && _focusedEntry != null && _focusedEntry != entry;
|
||||
boolean showProgress = entry.getInfo() instanceof TotpInfo && ((TotpInfo) entry.getInfo()).getPeriod() != getMostFrequentPeriod();
|
||||
holder.setData(entry, _codeGroupSize, _showAccountName, showProgress, hidden, dimmed);
|
||||
holder.setFocused(_selectedEntries.contains(entry));
|
||||
@@ -342,14 +359,17 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
boolean handled = false;
|
||||
|
||||
if (_selectedEntries.isEmpty()) {
|
||||
holder.animateCopyText();
|
||||
if (_copyOnTap) {
|
||||
_view.onEntryCopy(entry);
|
||||
holder.animateCopyText();
|
||||
}
|
||||
|
||||
if (_highlightEntry || _tapToReveal) {
|
||||
if (_highlightEntry || _tempHighlightEntry || _tapToReveal) {
|
||||
if (_focusedEntry == entry) {
|
||||
resetFocus();
|
||||
handled = true;
|
||||
} else {
|
||||
focusEntry(entry);
|
||||
focusEntry(entry, _tapToRevealTime);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -377,7 +397,29 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
holder.setFocusedAndAnimate(true);
|
||||
}
|
||||
|
||||
return _view.onLongEntryClick(_shownEntries.get(position));
|
||||
boolean returnVal = _view.onLongEntryClick(_shownEntries.get(position));
|
||||
|
||||
boolean dragEnabled = _selectedEntries.size() == 0
|
||||
|| _selectedEntries.size() == 1 && _selectedEntries.get(0) == holder.getEntry();
|
||||
if (dragEnabled && isDragAndDropAllowed()) {
|
||||
_view.startDrag(_dragHandleHolder);
|
||||
}
|
||||
|
||||
return returnVal;
|
||||
}
|
||||
});
|
||||
holder.itemView.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
// Start drag if this is the only item selected
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_MOVE
|
||||
&& _selectedEntries.size() == 1
|
||||
&& _selectedEntries.get(0) == holder.getEntry()
|
||||
&& isDragAndDropAllowed()) {
|
||||
_view.startDrag(_dragHandleHolder);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
holder.setOnRefreshClickListener(new View.OnClickListener() {
|
||||
@@ -461,20 +503,20 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
return maxValue > 1 ? maxKey : -1;
|
||||
}
|
||||
|
||||
private void focusEntry(VaultEntry entry) {
|
||||
public void focusEntry(VaultEntry entry, int secondsToFocus) {
|
||||
_focusedEntry = entry;
|
||||
_dimHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
for (EntryHolder holder : _holders) {
|
||||
if (holder.getEntry() != _focusedEntry) {
|
||||
if (_highlightEntry) {
|
||||
if (_highlightEntry || _tempHighlightEntry) {
|
||||
holder.dim();
|
||||
}
|
||||
if (_tapToReveal) {
|
||||
holder.hideCode();
|
||||
}
|
||||
} else {
|
||||
if (_highlightEntry) {
|
||||
if (_highlightEntry || _tempHighlightEntry) {
|
||||
holder.highlight();
|
||||
}
|
||||
if (_tapToReveal) {
|
||||
@@ -483,12 +525,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
}
|
||||
}
|
||||
|
||||
_dimHandler.postDelayed(this::resetFocus, _tapToRevealTime * 1000);
|
||||
_dimHandler.postDelayed(this::resetFocus, secondsToFocus * 1000);
|
||||
}
|
||||
|
||||
private void resetFocus() {
|
||||
for (EntryHolder holder : _holders) {
|
||||
if (_highlightEntry) {
|
||||
if (_focusedEntry != null) {
|
||||
holder.highlight();
|
||||
}
|
||||
if (_tapToReveal) {
|
||||
@@ -497,18 +539,45 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
}
|
||||
|
||||
_focusedEntry = null;
|
||||
_tempHighlightEntry = false;
|
||||
}
|
||||
|
||||
private void updateDraggableStatus() {
|
||||
if (!isDragAndDropAllowed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedEntries.size() == 1 && _dragHandleHolder == null) {
|
||||
// Find and enable dragging for the single selected EntryHolder
|
||||
// Not nice but this is the best method I could find
|
||||
for (int i = 0; i < _holders.size(); i++) {
|
||||
if (_holders.get(i).getEntry() == _selectedEntries.get(0)) {
|
||||
_dragHandleHolder = _holders.get(i);
|
||||
_dragHandleHolder.setShowDragHandle(true);
|
||||
_view.setSelectedEntry(_selectedEntries.get(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (_dragHandleHolder != null) {
|
||||
// Disable dragging if necessary when more/less than 1 selected entry
|
||||
_dragHandleHolder.setShowDragHandle(false);
|
||||
_dragHandleHolder = null;
|
||||
_view.setSelectedEntry(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSelectedEntry(VaultEntry entry) {
|
||||
_selectedEntries.remove(entry);
|
||||
updateDraggableStatus();
|
||||
}
|
||||
|
||||
public void addSelectedEntry(VaultEntry entry) {
|
||||
if (_highlightEntry) {
|
||||
if (_focusedEntry != null) {
|
||||
resetFocus();
|
||||
}
|
||||
|
||||
_selectedEntries.add(entry);
|
||||
updateDraggableStatus();
|
||||
}
|
||||
|
||||
public void deselectAllEntries() {
|
||||
@@ -522,6 +591,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
}
|
||||
|
||||
_selectedEntries.clear();
|
||||
|
||||
updateDraggableStatus();
|
||||
}
|
||||
|
||||
public boolean isDragAndDropAllowed() {
|
||||
@@ -547,8 +618,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
|
||||
void onEntryDrop(VaultEntry entry);
|
||||
void onEntryChange(VaultEntry entry);
|
||||
void onEntryCopy(VaultEntry entry);
|
||||
void onPeriodUniformityChanged(boolean uniform, int period);
|
||||
void onSelect(VaultEntry entry);
|
||||
void onDeselect(VaultEntry entry);
|
||||
void onListChange();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,12 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.google.zxing.common.StringUtils;
|
||||
|
||||
public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private static final float DEFAULT_ALPHA = 1.0f;
|
||||
private static final float DIMMED_ALPHA = 0.2f;
|
||||
private static final char HIDDEN_CHAR = '●';
|
||||
|
||||
private TextView _profileName;
|
||||
private TextView _profileCode;
|
||||
@@ -37,6 +39,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private VaultEntry _entry;
|
||||
private ImageView _buttonRefresh;
|
||||
private RelativeLayout _description;
|
||||
private ImageView _dragHandle;
|
||||
|
||||
private final ImageView _selected;
|
||||
private final Handler _selectedHandler;
|
||||
@@ -45,7 +48,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private boolean _hidden;
|
||||
|
||||
private PeriodProgressBar _progressBar;
|
||||
private TotpProgressBar _progressBar;
|
||||
private View _view;
|
||||
|
||||
private UiRefresher _refresher;
|
||||
@@ -67,7 +70,10 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_profileDrawable = view.findViewById(R.id.ivTextDrawable);
|
||||
_buttonRefresh = view.findViewById(R.id.buttonRefresh);
|
||||
_selected = view.findViewById(R.id.ivSelected);
|
||||
_dragHandle = view.findViewById(R.id.drag_handle);
|
||||
|
||||
_selectedHandler = new Handler();
|
||||
_animationHandler = new Handler();
|
||||
|
||||
_progressBar = view.findViewById(R.id.progressBar);
|
||||
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
|
||||
@@ -83,8 +89,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
if (!_hidden) {
|
||||
refreshCode();
|
||||
}
|
||||
|
||||
_progressBar.refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -106,6 +110,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_selected.clearAnimation();
|
||||
_selected.setVisibility(View.GONE);
|
||||
_selectedHandler.removeCallbacksAndMessages(null);
|
||||
_animationHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
// only show the progress bar if there is no uniform period and the entry type is TotpInfo
|
||||
setShowProgress(showProgress);
|
||||
@@ -156,6 +161,14 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_buttonRefresh.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
public void setShowDragHandle(boolean showDragHandle) {
|
||||
if (showDragHandle) {
|
||||
_dragHandle.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
_dragHandle.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setShowProgress(boolean showProgress) {
|
||||
if (_entry.getInfo() instanceof HotpInfo) {
|
||||
showProgress = false;
|
||||
@@ -197,10 +210,17 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public void startRefreshLoop() {
|
||||
_refresher.start();
|
||||
_progressBar.start();
|
||||
}
|
||||
|
||||
public void stopRefreshLoop() {
|
||||
_refresher.stop();
|
||||
_progressBar.stop();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
_progressBar.restart();
|
||||
refreshCode();
|
||||
}
|
||||
|
||||
public void refreshCode() {
|
||||
@@ -214,26 +234,34 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
String otp = info.getOtp();
|
||||
if (!(info instanceof SteamInfo)) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < otp.length(); i++) {
|
||||
if (i != 0 && i % _codeGroupSize == 0) {
|
||||
sb.append(" ");
|
||||
}
|
||||
sb.append(otp.charAt(i));
|
||||
}
|
||||
otp = sb.toString();
|
||||
otp = formatCode(otp);
|
||||
}
|
||||
|
||||
_profileCode.setText(otp);
|
||||
}
|
||||
|
||||
private String formatCode(String code) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < code.length(); i++) {
|
||||
if (i != 0 && i % _codeGroupSize == 0) {
|
||||
sb.append(" ");
|
||||
}
|
||||
sb.append(code.charAt(i));
|
||||
}
|
||||
code = sb.toString();
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
public void revealCode() {
|
||||
updateCode();
|
||||
_hidden = false;
|
||||
}
|
||||
|
||||
public void hideCode() {
|
||||
_profileCode.setText(R.string.tap_to_reveal);
|
||||
String hiddenText = new String(new char[_entry.getInfo().getDigits()]).replace("\0", Character.toString(HIDDEN_CHAR));
|
||||
hiddenText = formatCode(hiddenText);
|
||||
_profileCode.setText(hiddenText);
|
||||
_hidden = true;
|
||||
}
|
||||
|
||||
@@ -246,9 +274,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
}
|
||||
|
||||
public void animateCopyText() {
|
||||
if (_animationHandler != null) {
|
||||
_animationHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
_animationHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
Animation slideDownFadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.slide_down_fade_in);
|
||||
Animation slideDownFadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.slide_down_fade_out);
|
||||
Animation fadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_out);
|
||||
@@ -257,14 +284,9 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_profileCopied.startAnimation(slideDownFadeIn);
|
||||
_description.startAnimation(slideDownFadeOut);
|
||||
|
||||
|
||||
_animationHandler = new Handler();
|
||||
_animationHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
_profileCopied.startAnimation(fadeOut);
|
||||
_description.startAnimation(fadeIn);
|
||||
}
|
||||
_animationHandler.postDelayed(() -> {
|
||||
_profileCopied.startAnimation(fadeOut);
|
||||
_description.startAnimation(fadeIn);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AnimationUtils;
|
||||
@@ -22,6 +24,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
import com.beemdevelopment.aegis.helpers.MetricsHelper;
|
||||
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
@@ -33,6 +36,7 @@ import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -41,11 +45,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private EntryAdapter _adapter;
|
||||
private Listener _listener;
|
||||
private SimpleItemTouchHelperCallback _touchCallback;
|
||||
private ItemTouchHelper _touchHelper;
|
||||
|
||||
private RecyclerView _recyclerView;
|
||||
private RecyclerView.ItemDecoration _dividerDecoration;
|
||||
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
|
||||
private PeriodProgressBar _progressBar;
|
||||
private TotpProgressBar _progressBar;
|
||||
private boolean _showProgress;
|
||||
private ViewMode _viewMode;
|
||||
private LinearLayout _emptyStateView;
|
||||
@@ -89,8 +94,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(view.getContext());
|
||||
_recyclerView.setLayoutManager(layoutManager);
|
||||
_touchCallback = new SimpleItemTouchHelperCallback(_adapter);
|
||||
ItemTouchHelper touchHelper = new ItemTouchHelper(_touchCallback);
|
||||
touchHelper.attachToRecyclerView(_recyclerView);
|
||||
_touchHelper = new ItemTouchHelper(_touchCallback);
|
||||
_touchHelper.attachToRecyclerView(_recyclerView);
|
||||
_recyclerView.setAdapter(_adapter);
|
||||
|
||||
int resId = R.anim.layout_animation_fall_down;
|
||||
@@ -125,8 +130,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
|
||||
public void setGroupFilter(String group, boolean apply) {
|
||||
_touchCallback.setIsLongPressDragEnabled(group == null);
|
||||
_adapter.setGroupFilter(group, apply);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
|
||||
if (apply) {
|
||||
runEntriesAnimation();
|
||||
@@ -134,7 +139,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
|
||||
public void setIsLongPressDragEnabled(boolean enabled) {
|
||||
_touchCallback.setIsLongPressDragEnabled(enabled);
|
||||
_touchCallback.setIsLongPressDragEnabled(enabled && _adapter.isDragAndDropAllowed());
|
||||
}
|
||||
|
||||
public void setIsCopyOnTapEnabled(boolean enabled) {
|
||||
_adapter.setIsCopyOnTapEnabled(enabled);
|
||||
}
|
||||
|
||||
public void setActionModeState(boolean enabled, VaultEntry entry) {
|
||||
@@ -149,8 +158,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
|
||||
public void setSortCategory(SortCategory sortCategory, boolean apply) {
|
||||
_touchCallback.setIsLongPressDragEnabled(sortCategory == SortCategory.CUSTOM);
|
||||
_adapter.setSortCategory(sortCategory, apply);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
|
||||
if (apply) {
|
||||
runEntriesAnimation();
|
||||
@@ -158,8 +167,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
|
||||
public void setSearchFilter(String search) {
|
||||
_touchCallback.setIsLongPressDragEnabled(search == null);
|
||||
_adapter.setSearchFilter(search);
|
||||
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
|
||||
}
|
||||
|
||||
public void setSelectedEntry(VaultEntry entry) {
|
||||
_touchCallback.setSelectedEntry(entry);
|
||||
}
|
||||
|
||||
public void setViewMode(ViewMode mode) {
|
||||
@@ -168,10 +181,15 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_adapter.setViewMode(_viewMode);
|
||||
}
|
||||
|
||||
public void startDrag(RecyclerView.ViewHolder viewHolder) {
|
||||
_touchHelper.startDrag(viewHolder);
|
||||
}
|
||||
|
||||
public void refresh(boolean hard) {
|
||||
if (_showProgress) {
|
||||
_progressBar.refresh();
|
||||
_progressBar.restart();
|
||||
}
|
||||
|
||||
_adapter.refresh(hard);
|
||||
}
|
||||
|
||||
@@ -204,6 +222,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
_listener.onEntryChange(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryCopy(VaultEntry entry) {
|
||||
_listener.onEntryCopy(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelect(VaultEntry entry) { _listener.onSelect(entry); }
|
||||
|
||||
@@ -214,16 +237,20 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
public void onPeriodUniformityChanged(boolean isUniform, int period) {
|
||||
setShowProgress(isUniform);
|
||||
if (_showProgress) {
|
||||
_refresher.stop();
|
||||
_progressBar.setVisibility(View.VISIBLE);
|
||||
_progressBar.setPeriod(period);
|
||||
_progressBar.start();
|
||||
_refresher.start();
|
||||
} else {
|
||||
_progressBar.setVisibility(View.GONE);
|
||||
_progressBar.stop();
|
||||
_refresher.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListChange() { _listener.onListChange(); }
|
||||
|
||||
public void setCodeGroupSize(int codeGrouping) {
|
||||
_adapter.setCodeGroupSize(codeGrouping);
|
||||
}
|
||||
@@ -249,11 +276,42 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
}
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
_adapter.addEntry(entry);
|
||||
updateEmptyState();
|
||||
addEntry(entry, false);
|
||||
}
|
||||
|
||||
public void addEntries(List<VaultEntry> entries) {
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void addEntry(VaultEntry entry, boolean focusEntry) {
|
||||
int position = _adapter.addEntry(entry);
|
||||
updateEmptyState();
|
||||
|
||||
if (focusEntry && position >= 0) {
|
||||
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
_recyclerView.removeOnScrollListener(this);
|
||||
_adapter.setTempHighlightEntry(true);
|
||||
|
||||
final int secondsToFocus = 3;
|
||||
_adapter.focusEntry(entry, secondsToFocus);
|
||||
}
|
||||
}
|
||||
};
|
||||
_recyclerView.addOnScrollListener(scrollListener);
|
||||
_recyclerView.setOnTouchListener((v, event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
_recyclerView.removeOnScrollListener(scrollListener);
|
||||
_recyclerView.stopScroll();
|
||||
_recyclerView.setOnTouchListener(null);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
_recyclerView.smoothScrollToPosition(position);
|
||||
}
|
||||
}
|
||||
|
||||
public void addEntries(Collection<VaultEntry> entries) {
|
||||
_adapter.addEntries(entries);
|
||||
updateEmptyState();
|
||||
}
|
||||
@@ -322,10 +380,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
|
||||
void onEntryDrop(VaultEntry entry);
|
||||
void onEntryChange(VaultEntry entry);
|
||||
void onEntryCopy(VaultEntry entry);
|
||||
void onLongEntryClick(VaultEntry entry);
|
||||
void onScroll(int dx, int dy);
|
||||
void onSelect(VaultEntry entry);
|
||||
void onDeselect(VaultEntry entry);
|
||||
void onListChange();
|
||||
}
|
||||
|
||||
private class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {
|
||||
@@ -333,7 +393,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
|
||||
private VerticalSpaceItemDecoration(float dp) {
|
||||
// convert dp to pixels
|
||||
_height = (int) (dp * (getContext().getResources().getDisplayMetrics().densityDpi / 160f));
|
||||
_height = MetricsHelper.convertDpToPixels(getContext(), dp);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,12 +4,12 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||
private GroupAdapter.Listener _listener;
|
||||
private ArrayList<String> _groups;
|
||||
@@ -38,7 +38,7 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
|
||||
|
||||
@Override
|
||||
public GroupHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_slot, parent, false);
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_group, parent, false);
|
||||
return new GroupHolder(view);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class GroupHolder extends RecyclerView.ViewHolder {
|
||||
private TextView _slotName;
|
||||
private ImageView _buttonDelete;
|
||||
|
||||
public GroupHolder(final View view) {
|
||||
super(view);
|
||||
_slotName = view.findViewById(R.id.text_slot_name);
|
||||
_slotName = view.findViewById(R.id.text_group_name);
|
||||
_buttonDelete = view.findViewById(R.id.button_delete);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class PeriodProgressBar extends ProgressBar {
|
||||
private int _period;
|
||||
|
||||
public PeriodProgressBar(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public PeriodProgressBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public PeriodProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public PeriodProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public void setPeriod(int period) {
|
||||
_period = period;
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
// reset the progress bar
|
||||
int maxProgress = getMax();
|
||||
setProgress(maxProgress);
|
||||
|
||||
// calculate the progress the bar should start at
|
||||
long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
|
||||
long period = _period * maxProgress;
|
||||
int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress);
|
||||
|
||||
// start progress animation
|
||||
ObjectAnimator animation = ObjectAnimator.ofInt(this, "progress", currentProgress, 0);
|
||||
animation.setDuration(millisTillRotation);
|
||||
animation.setInterpolator(new LinearInterpolator());
|
||||
animation.start();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.provider.Settings;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
|
||||
public class TotpProgressBar extends ProgressBar {
|
||||
private int _period = 30;
|
||||
private Handler _handler;
|
||||
private float _animDurationScale;
|
||||
|
||||
public TotpProgressBar(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public TotpProgressBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public TotpProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
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);
|
||||
}
|
||||
|
||||
public void setPeriod(int period) {
|
||||
_period = period;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
stop();
|
||||
_handler = new Handler();
|
||||
_animDurationScale = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f);
|
||||
refresh();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (_handler != null) {
|
||||
_handler.removeCallbacksAndMessages(null);
|
||||
_handler = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void restart() {
|
||||
stop();
|
||||
start();
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
// calculate the current progress the bar should start at
|
||||
int maxProgress = getMax();
|
||||
long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
|
||||
int currentProgress = (int) (maxProgress * ((float) millisTillRotation / (_period * 1000)));
|
||||
|
||||
// start progress animation, compensating for any changes to the animator duration scale settings
|
||||
float animPart = (float) maxProgress / _period;
|
||||
int animEnd = (int) (Math.floor(currentProgress / animPart) * animPart);
|
||||
int animPartDuration = _animDurationScale > 0 ? (int) (1000 / _animDurationScale) : 0;
|
||||
float animDurationFraction = (float) (currentProgress - animEnd) / animPart;
|
||||
int realAnimDuration = (int) (1000 * animDurationFraction);
|
||||
int animDuration = (int) (animPartDuration * animDurationFraction);
|
||||
|
||||
ObjectAnimator animation = ObjectAnimator.ofInt(this, "progress", currentProgress, animEnd);
|
||||
animation.setDuration(animDuration);
|
||||
animation.setInterpolator(new LinearInterpolator());
|
||||
animation.start();
|
||||
|
||||
// the animation only lasts for (less than) one second, so restart it after that
|
||||
_handler.postDelayed(this::refresh, realAnimDuration);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.beemdevelopment.aegis.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -34,4 +35,21 @@ public class IOUtils {
|
||||
outStream.write(buf, 0, read);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearDirectory(File dir, boolean deleteRoot) {
|
||||
File[] files = dir.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
clearDirectory(file, true);
|
||||
} else {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteRoot) {
|
||||
dir.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.beemdevelopment.aegis.util;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class JsonUtils {
|
||||
private JsonUtils() {
|
||||
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String optString(JSONObject obj, String key) {
|
||||
return obj.isNull(key) ? null : obj.optString(key, null);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,13 @@ public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializa
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the internal map.
|
||||
*/
|
||||
public void wipe() {
|
||||
_map.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an old value (with the same UUID as the new given value) in the
|
||||
* internal map with the new given value.
|
||||
|
||||
@@ -9,8 +9,10 @@ import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
@@ -25,6 +27,8 @@ import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class VaultBackupManager {
|
||||
private static final String TAG = VaultBackupManager.class.getSimpleName();
|
||||
@@ -34,42 +38,67 @@ public class VaultBackupManager {
|
||||
|
||||
public static final String FILENAME_PREFIX = "aegis-backup";
|
||||
|
||||
private Context _context;
|
||||
private final Context _context;
|
||||
private final Preferences _prefs;
|
||||
private final ExecutorService _executor;
|
||||
|
||||
public VaultBackupManager(Context context) {
|
||||
_context = context;
|
||||
_prefs = new Preferences(context);
|
||||
_executor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
public void create(Uri dirUri, int versionsToKeep) throws VaultManagerException {
|
||||
public void destroy() {
|
||||
Log.i(TAG, "Shutting down backup manager thread");
|
||||
_executor.shutdown();
|
||||
}
|
||||
|
||||
public void scheduleBackup(File tempFile, Uri dirUri, int versionsToKeep) {
|
||||
_executor.execute(() -> {
|
||||
try {
|
||||
createBackup(tempFile, dirUri, versionsToKeep);
|
||||
_prefs.setBackupsError(null);
|
||||
} catch (VaultManagerException e) {
|
||||
e.printStackTrace();
|
||||
_prefs.setBackupsError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createBackup(File tempFile, Uri dirUri, int versionsToKeep) throws VaultManagerException {
|
||||
FileInfo fileInfo = new FileInfo(FILENAME_PREFIX);
|
||||
DocumentFile dir = DocumentFile.fromTreeUri(_context, dirUri);
|
||||
|
||||
Log.i(TAG, String.format("Creating backup at %s: %s", Uri.decode(dir.getUri().toString()), fileInfo.toString()));
|
||||
try {
|
||||
Log.i(TAG, String.format("Creating backup at %s: %s", Uri.decode(dir.getUri().toString()), fileInfo.toString()));
|
||||
|
||||
if (!hasPermissionsAt(dirUri)) {
|
||||
Log.e(TAG, "Unable to create file for backup, no persisted URI permissions");
|
||||
throw new VaultManagerException("No persisted URI permissions");
|
||||
}
|
||||
if (!hasPermissionsAt(dirUri)) {
|
||||
throw new VaultManagerException("No persisted URI permissions");
|
||||
}
|
||||
|
||||
// If we create a file with a name that already exists, SAF will append a number
|
||||
// to the filename and write to that instead. We can't overwrite existing files, so
|
||||
// just avoid that altogether by checking beforehand.
|
||||
if (dir.findFile(fileInfo.toString()) != null) {
|
||||
throw new VaultManagerException("Backup file already exists");
|
||||
}
|
||||
// If we create a file with a name that already exists, SAF will append a number
|
||||
// to the filename and write to that instead. We can't overwrite existing files, so
|
||||
// just avoid that altogether by checking beforehand.
|
||||
if (dir.findFile(fileInfo.toString()) != null) {
|
||||
throw new VaultManagerException("Backup file already exists");
|
||||
}
|
||||
|
||||
DocumentFile file = dir.createFile("application/json", fileInfo.toString());
|
||||
if (file == null) {
|
||||
Log.e(TAG, "Unable to create file for backup, createFile returned null");
|
||||
throw new VaultManagerException("createFile returned null");
|
||||
}
|
||||
DocumentFile file = dir.createFile("application/json", fileInfo.toString());
|
||||
if (file == null) {
|
||||
throw new VaultManagerException("createFile returned null");
|
||||
}
|
||||
|
||||
try (FileInputStream inStream = _context.openFileInput(VaultManager.FILENAME);
|
||||
OutputStream outStream = _context.getContentResolver().openOutputStream(file.getUri())) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to create backup", e);
|
||||
throw new VaultManagerException(e);
|
||||
try (FileInputStream inStream = new FileInputStream(tempFile);
|
||||
OutputStream outStream = _context.getContentResolver().openOutputStream(file.getUri())) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
} catch (IOException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
} catch (VaultManagerException e) {
|
||||
Log.e(TAG, String.format("Unable to create backup: %s", e.toString()));
|
||||
throw e;
|
||||
} finally {
|
||||
tempFile.delete();
|
||||
}
|
||||
|
||||
enforceVersioning(dir, versionsToKeep);
|
||||
@@ -88,22 +117,20 @@ public class VaultBackupManager {
|
||||
private void enforceVersioning(DocumentFile dir, int versionsToKeep) {
|
||||
Log.i(TAG, String.format("Scanning directory %s for backup files", Uri.decode(dir.getUri().toString())));
|
||||
|
||||
List<File> files = new ArrayList<>();
|
||||
List<BackupFile> files = new ArrayList<>();
|
||||
for (DocumentFile docFile : dir.listFiles()) {
|
||||
if (docFile.isFile() && !docFile.isVirtual()) {
|
||||
try {
|
||||
files.add(new File(docFile));
|
||||
files.add(new BackupFile(docFile));
|
||||
} catch (ParseException ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(files, new FileComparator());
|
||||
for (File file : files) {
|
||||
Log.i(TAG, file.getFile().getName());
|
||||
}
|
||||
Log.i(TAG, String.format("Found %d backup files, keeping the %d most recent", files.size(), versionsToKeep));
|
||||
|
||||
Collections.sort(files, new FileComparator());
|
||||
if (files.size() > versionsToKeep) {
|
||||
for (File file : files.subList(0, files.size() - versionsToKeep)) {
|
||||
for (BackupFile file : files.subList(0, files.size() - versionsToKeep)) {
|
||||
Log.i(TAG, String.format("Deleting %s", file.getFile().getName()));
|
||||
if (!file.getFile().delete()) {
|
||||
Log.e(TAG, String.format("Unable to delete %s", file.getFile().getName()));
|
||||
@@ -114,17 +141,27 @@ public class VaultBackupManager {
|
||||
|
||||
public static class FileInfo {
|
||||
private String _filename;
|
||||
private String _ext;
|
||||
private Date _date;
|
||||
|
||||
public FileInfo(String filename, Date date) {
|
||||
public FileInfo(String filename, String extension, Date date) {
|
||||
_filename = filename;
|
||||
_ext = extension;
|
||||
_date = date;
|
||||
}
|
||||
|
||||
public FileInfo(String filename, Date date) {
|
||||
this(filename, "json", date);
|
||||
}
|
||||
|
||||
public FileInfo(String filename) {
|
||||
this(filename, Calendar.getInstance().getTime());
|
||||
}
|
||||
|
||||
public FileInfo(String filename, String extension) {
|
||||
this(filename, extension, Calendar.getInstance().getTime());
|
||||
}
|
||||
|
||||
public static FileInfo parseFilename(String filename) throws ParseException {
|
||||
if (filename == null) {
|
||||
throw new ParseException("The filename must not be null", 0);
|
||||
@@ -163,6 +200,10 @@ public class VaultBackupManager {
|
||||
return _filename;
|
||||
}
|
||||
|
||||
public String getExtension() {
|
||||
return _ext;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return _date;
|
||||
}
|
||||
@@ -170,15 +211,15 @@ public class VaultBackupManager {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s-%s.json", _filename, _dateFormat.format(_date));
|
||||
return String.format("%s-%s.%s", _filename, _dateFormat.format(_date), _ext);
|
||||
}
|
||||
}
|
||||
|
||||
private static class File {
|
||||
private static class BackupFile {
|
||||
private DocumentFile _file;
|
||||
private FileInfo _info;
|
||||
|
||||
public File(DocumentFile file) throws ParseException {
|
||||
public BackupFile(DocumentFile file) throws ParseException {
|
||||
_file = file;
|
||||
_info = FileInfo.parseFilename(file.getName());
|
||||
}
|
||||
@@ -192,9 +233,9 @@ public class VaultBackupManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileComparator implements Comparator<File> {
|
||||
private static class FileComparator implements Comparator<BackupFile> {
|
||||
@Override
|
||||
public int compare(File o1, File o2) {
|
||||
public int compare(BackupFile o1, BackupFile o2) {
|
||||
return o1.getInfo().getDate().compareTo(o2.getInfo().getDate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,18 @@ import android.content.Context;
|
||||
import androidx.core.util.AtomicFile;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.Collator;
|
||||
import java.util.Collection;
|
||||
import java.util.TreeSet;
|
||||
@@ -19,8 +24,9 @@ import java.util.UUID;
|
||||
|
||||
public class VaultManager {
|
||||
public static final String FILENAME = "aegis.json";
|
||||
public static final String FILENAME_EXPORT = "aegis-export.json";
|
||||
public static final String FILENAME_EXPORT_PLAIN = "aegis-export-plain.json";
|
||||
public static final String FILENAME_PREFIX_EXPORT = "aegis-export";
|
||||
public static final String FILENAME_PREFIX_EXPORT_PLAIN = "aegis-export-plain";
|
||||
public static final String FILENAME_PREFIX_EXPORT_URI = "aegis-export-uri";
|
||||
|
||||
private Vault _vault;
|
||||
private VaultFileCredentials _creds;
|
||||
@@ -41,13 +47,21 @@ public class VaultManager {
|
||||
this(context, vault, null);
|
||||
}
|
||||
|
||||
private static AtomicFile getAtomicFile(Context context) {
|
||||
return new AtomicFile(new File(context.getFilesDir(), FILENAME));
|
||||
}
|
||||
|
||||
public static boolean fileExists(Context context) {
|
||||
File file = new File(context.getFilesDir(), FILENAME);
|
||||
File file = getAtomicFile(context).getBaseFile();
|
||||
return file.exists() && file.isFile();
|
||||
}
|
||||
|
||||
public static void deleteFile(Context context) {
|
||||
getAtomicFile(context).delete();
|
||||
}
|
||||
|
||||
public static VaultFile readFile(Context context) throws VaultManagerException {
|
||||
AtomicFile file = new AtomicFile(new File(context.getFilesDir(), FILENAME));
|
||||
AtomicFile file = getAtomicFile(context);
|
||||
|
||||
try {
|
||||
byte[] fileBytes = file.readFully();
|
||||
@@ -81,7 +95,7 @@ public class VaultManager {
|
||||
|
||||
public static void save(Context context, VaultFile vaultFile) throws VaultManagerException {
|
||||
byte[] bytes = vaultFile.toBytes();
|
||||
AtomicFile file = new AtomicFile(new File(context.getFilesDir(), FILENAME));
|
||||
AtomicFile file = getAtomicFile(context);
|
||||
|
||||
FileOutputStream stream = null;
|
||||
try {
|
||||
@@ -96,7 +110,11 @@ public class VaultManager {
|
||||
}
|
||||
}
|
||||
|
||||
public void save() throws VaultManagerException {
|
||||
public void destroy() {
|
||||
_backups.destroy();
|
||||
}
|
||||
|
||||
public void save(boolean backup) throws VaultManagerException {
|
||||
try {
|
||||
JSONObject obj = _vault.toJson();
|
||||
|
||||
@@ -108,20 +126,37 @@ public class VaultManager {
|
||||
}
|
||||
|
||||
save(_context, file);
|
||||
|
||||
if (_prefs.isBackupsEnabled()) {
|
||||
backup();
|
||||
}
|
||||
} catch (VaultFileException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
|
||||
if (backup && _prefs.isBackupsEnabled()) {
|
||||
try {
|
||||
backup();
|
||||
_prefs.setBackupsError(null);
|
||||
} catch (VaultManagerException e) {
|
||||
_prefs.setBackupsError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void export(OutputStream stream, boolean encrypt) throws VaultManagerException {
|
||||
/**
|
||||
* Exports the vault bt serializing it and writing it to the given OutputStream. If encryption
|
||||
* is enabled, the vault will be encrypted automatically.
|
||||
*/
|
||||
public void export(OutputStream stream) throws VaultManagerException {
|
||||
export(stream, getCredentials());
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the vault by serializing it and writing it to the given OutputStream. If creds is
|
||||
* not null, it will be used to encrypt the vault first.
|
||||
*/
|
||||
public void export(OutputStream stream, VaultFileCredentials creds) throws VaultManagerException {
|
||||
try {
|
||||
VaultFile vaultFile = new VaultFile();
|
||||
if (encrypt && isEncryptionEnabled()) {
|
||||
vaultFile.setContent(_vault.toJson(), _creds);
|
||||
if (creds != null) {
|
||||
vaultFile.setContent(_vault.toJson(), creds);
|
||||
} else {
|
||||
vaultFile.setContent(_vault.toJson());
|
||||
}
|
||||
@@ -133,8 +168,38 @@ public class VaultManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the vault by serializing the list of entries to a newline-separated list of
|
||||
* Google Authenticator URI's and writing it to the given OutputStream.
|
||||
*/
|
||||
public void exportGoogleUris(OutputStream outStream) throws VaultManagerException {
|
||||
try (PrintStream stream = new PrintStream(outStream, false, StandardCharsets.UTF_8.name())) {
|
||||
for (VaultEntry entry : getEntries()) {
|
||||
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
|
||||
stream.println(info.getUri().toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void backup() throws VaultManagerException {
|
||||
_backups.create(_prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
|
||||
try {
|
||||
File dir = new File(_context.getCacheDir(), "backup");
|
||||
if (!dir.exists() && !dir.mkdir()) {
|
||||
throw new IOException(String.format("Unable to create directory %s", dir));
|
||||
}
|
||||
|
||||
File tempFile = File.createTempFile(VaultBackupManager.FILENAME_PREFIX, ".json", dir);
|
||||
try (InputStream inStream = getAtomicFile(_context).openRead();
|
||||
OutputStream outStream = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
}
|
||||
|
||||
_backups.scheduleBackup(tempFile, _prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
|
||||
} catch (IOException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
@@ -149,6 +214,10 @@ public class VaultManager {
|
||||
return _vault.getEntries().remove(entry);
|
||||
}
|
||||
|
||||
public void wipeEntries() {
|
||||
_vault.getEntries().wipe();
|
||||
}
|
||||
|
||||
public VaultEntry replaceEntry(VaultEntry entry) {
|
||||
return _vault.getEntries().replace(entry);
|
||||
}
|
||||
@@ -190,11 +259,11 @@ public class VaultManager {
|
||||
|
||||
public void enableEncryption(VaultFileCredentials creds) throws VaultManagerException {
|
||||
_creds = creds;
|
||||
save();
|
||||
save(true);
|
||||
}
|
||||
|
||||
public void disableEncryption() throws VaultManagerException {
|
||||
_creds = null;
|
||||
save();
|
||||
save(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,23 @@ option java_outer_classname = "GoogleAuthProtos";
|
||||
|
||||
message MigrationPayload {
|
||||
enum Algorithm {
|
||||
ALGO_INVALID = 0;
|
||||
ALGO_SHA1 = 1;
|
||||
ALGORITHM_UNSPECIFIED = 0;
|
||||
ALGORITHM_SHA1 = 1;
|
||||
ALGORITHM_SHA256 = 2;
|
||||
ALGORITHM_SHA512 = 3;
|
||||
ALGORITHM_MD5 = 4;
|
||||
}
|
||||
|
||||
enum DigitCount {
|
||||
DIGIT_COUNT_UNSPECIFIED = 0;
|
||||
DIGIT_COUNT_SIX = 1;
|
||||
DIGIT_COUNT_EIGHT = 2;
|
||||
}
|
||||
|
||||
enum OtpType {
|
||||
OTP_INVALID = 0;
|
||||
OTP_HOTP = 1;
|
||||
OTP_TOTP = 2;
|
||||
OTP_TYPE_UNSPECIFIED = 0;
|
||||
OTP_TYPE_HOTP = 1;
|
||||
OTP_TYPE_TOTP = 2;
|
||||
}
|
||||
|
||||
message OtpParameters {
|
||||
@@ -20,7 +29,7 @@ message MigrationPayload {
|
||||
string name = 2;
|
||||
string issuer = 3;
|
||||
Algorithm algorithm = 4;
|
||||
int32 digits = 5;
|
||||
DigitCount digits = 5;
|
||||
OtpType type = 6;
|
||||
int64 counter = 7;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/auth_button"/>
|
||||
<stroke android:color="@color/auth_button" android:width="1dp" />
|
||||
<!--corners allow us to make the rounded corners button-->
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#59000000" />
|
||||
<size
|
||||
android:width="50dp"
|
||||
android:height="50dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/circular_button_background" />
|
||||
<item
|
||||
android:drawable="@drawable/ic_check_black_24dp"
|
||||
android:bottom="16dp"
|
||||
android:top="16dp"
|
||||
android:left="16dp"
|
||||
android:right="16dp" />
|
||||
</layer-list>
|
||||
@@ -0,0 +1,9 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/circular_button_background" />
|
||||
<item
|
||||
android:drawable="@drawable/ic_arrow_right_black_24dp"
|
||||
android:bottom="16dp"
|
||||
android:top="16dp"
|
||||
android:left="16dp"
|
||||
android:right="16dp" />
|
||||
</layer-list>
|
||||
@@ -0,0 +1,9 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/circular_button_background" />
|
||||
<item
|
||||
android:drawable="@drawable/ic_arrow_left_black_24dp"
|
||||
android:bottom="16dp"
|
||||
android:top="16dp"
|
||||
android:left="16dp"
|
||||
android:right="16dp" />
|
||||
</layer-list>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/divider"/>
|
||||
<solid android:color="?attr/divider"/>
|
||||
<size android:height="0.1dp"/>
|
||||
</shape>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- drawable/ic_arrow_left_black_24dp.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:autoMirrored="true">
|
||||
<path android:fillColor="#000" android:pathData="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- drawable/ic_arrow_right_black_24dp.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:autoMirrored="true">
|
||||
<path android:fillColor="#000" android:pathData="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="32dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27 -7.38,5.74zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,8 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:top="7dp" android:left="7dp" android:right="7dp" android:bottom="7dp">
|
||||
<vector android:height="24dp" android:viewportHeight="3328" android:tint="#FFFFFF"
|
||||
android:viewportWidth="3333" android:width="24.036058dp">
|
||||
<path android:fillColor="#FF000000" android:fillType="evenOdd" android:pathData="M5,0h1211v1206L5,1206L5,0zM3025,3025h309v304h-309v-304zM2432,3025h309v290h-618v-592h299v-300h304v-598h309v294h294v304h-294v304h-603v299zM1515,2417h299v-304h-289v-304h289v-304h-294v304h-309v-304h304L1515,603h309v902h299v304h294v-304h309v304h-294v304h-309v598h-299v603h-309v-897zM3020,1505h309v304h-309v-304zM608,1505h309v304L608,1809v-304zM5,1505h309v304L5,1809v-304zM1515,0h309v304h-309L1515,0zM0,2118h1211v1206L0,3324L0,2118zM294,2411h623v620L294,3031v-620zM2118,0h1211v1206L2118,1206L2118,0zM2412,293h623v620h-623L2412,293zM299,293h623v620L299,913L299,293z"/>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user