Compare commits

...
106 Commits
Author SHA1 Message Date
Alexander Bakker eb22101b02 Release v2.0.1
build / build (push) Canceled after 0s
2021-06-23 18:41:26 +02:00
Alexander Bakker 44f69e755c Update translations from Crowdin 2021-06-23 18:27:04 +02:00
Alexander Bakker 327b7ccbae Perform case changes using Locale.ROOT where applicable
Fixes #777
2021-06-23 18:06:01 +02:00
Alexander BakkerandGitHub 6585513120 Merge pull request #780 from jhenninger/suggested-icons-matching
Improve icon suggestions
2021-06-23 12:33:08 +02:00
Johannes Henninger acbe73834d Improve icon suggestions
Check if the icon issuers contain the token issuer or vice versa.

Fixes #761
2021-06-23 01:31:50 +02:00
Alexander BakkerandGitHub c27c4f0ac5 Merge pull request #760 from michaelschattgen/feature/usage-count
Add usage count to entries
2021-06-17 20:04:51 +02:00
Michael Schättgen 34cd16f240 Add usage count to entries
Update Preferences.java

Fix build error and review improvements
2021-06-16 16:22:11 +02:00
Alexander BakkerandGitHub 78fc37d1e9 Merge pull request #763 from jhenninger/fix-blank-text-delete-dialog
Fix blank text in delete dialog for entries with no issuer
2021-06-06 11:33:50 +02:00
Johannes Henninger df9684a31c Fix blank text in delete dialog for entries with no issuer
Fixes #758
2021-06-04 14:29:53 +02:00
Alexander Bakker 520c587d09 Collapse the search view when locking the app
Addresses part of #750
2021-06-02 18:14:18 +02:00
Alexander Bakker 7506c70236 Release v2.0
build / build (push) Canceled after 0s
2021-05-26 10:50:24 +02:00
Alexander Bakker d1e3224cba Store the group filter as JSON in preferences
This ensures we don't run into issues if the group name contains a ','
2021-05-26 10:46:49 +02:00
Alexander Bakker d2eed8a356 Update translations from Crowdin 2021-05-26 10:37:07 +02:00
Alexander Bakker 668823507d Put the feature list in full_description.txt on a single line
This is an attempt to work around the broken rendering on the F-Droid website
2021-05-26 10:23:40 +02:00
Alexander Bakker 2b83dde58a Update in-app URL to aegis-icons latest release page 2021-05-26 10:18:03 +02:00
Alexander Bakker e97a53bbe9 Force search filter to null if it's empty
Fixes #705
2021-05-22 13:09:52 +02:00
Alexander Bakker 77f59d41ad Add group filter back in the UI tests 2021-05-22 12:45:58 +02:00
Alexander Bakker e2ddc8a80f Try to guess the MIME type if SAF doesn't provide it
This fixes a crash that would occur if SAF doesn't provide a MIME type
2021-05-20 21:46:56 +02:00
Alexander Bakker b6093c2bfa Explicitly disable backups preferences when disabling encryption 2021-05-20 21:25:56 +02:00
Alexander Bakker fc25312d12 Add an option to save the current group filter 2021-05-20 20:58:20 +02:00
Alexander Bakker 51eade900c Fix a number of issues related to updating the group chip after changes 2021-05-19 12:56:35 +02:00
Alexander Bakker d152d13580 Make sure text and radio button are aligned in importer dialog 2021-05-09 11:58:24 +02:00
Alexander Bakker 7dff3fbad1 Release v2.0-beta2
build / build (push) Canceled after 0s
2021-05-08 11:04:59 +02:00
Alexander Bakker 7dc7f6ccc0 Update translations from Crowdin 2021-05-08 10:57:05 +02:00
Alexander Bakker ad0088eca6 Don't rely on the intent action to decide whether to block auto lock 2021-05-08 10:42:04 +02:00
Alexander Bakker 813bf292fa Update dependencies 2021-05-08 10:10:08 +02:00
Alexander Bakker 80bf5bc854 Fix the QR code background color for the light theme 2021-05-07 18:13:46 +02:00
Alexander Bakker e319a2eceb Fix a crash when opening the "Import from app" dialog 2021-05-07 18:13:17 +02:00
Alexander Bakker 0a9bfde438 Hide the keyboard before every slide change in the intro 2021-05-07 18:12:52 +02:00
Alexander Bakker c3c4e1fb4b Reflect changes in the group list to the group filter
Fixes #723
2021-05-07 18:08:39 +02:00
Alexander Bakker 1eff91594f Fix multiple layout issues on small-screen devices
Close #560
2021-05-06 23:17:35 +02:00
Alexander Bakker fddc29880a Add support from importing from 2FAS Authenticator 2021-05-05 10:58:22 +02:00
Alexander BakkerandGitHub 2ccfcd62e1 Merge pull request #734 from EwoutH/gradle
Gradle: Validate wrapper in CI and update to 6.8.3
2021-05-04 22:40:29 +02:00
Ewout ter Hoeven 993b29bd83 Gradle: Update wrapper and scripts to 6.8.3 2021-05-04 21:59:25 +02:00
Ewout ter Hoeven e371485a1b CI: Add step that validates the Gradle wrapper
Validates the gradle-wrapper.jar file on each build, which is a binary blob of executable code. This action ensures it's legit and doesn't execute malicious code.

See https://github.com/gradle/wrapper-validation-action
2021-05-04 21:59:25 +02:00
Alexander Bakker fb58c877d1 Remove padding from Y plane data before passing it to ZXing
Fixes #726
Fixes #657
2021-05-04 19:21:57 +02:00
Alexander BakkerandGitHub a62e474e33 Merge pull request #735 from EwoutH/imgbot
[ImgBot] Optimize images
2021-04-29 21:03:38 +02:00
ImgBotApp 5975b929b7 [ImgBot] Optimize images
*Total -- 569.55kb -> 504.66kb (11.39%)

/app/src/main/res/drawable-xhdpi/ic_qr_code.png -- 3.27kb -> 2.13kb (34.81%)
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -- 6.07kb -> 3.97kb (34.54%)
/app/src/main/res/drawable-xxhdpi/ic_qr_code.png -- 6.25kb -> 4.32kb (30.86%)
/metadata/en-US/images/icon.png -- 33.36kb -> 23.19kb (30.47%)
/app/src/main/res/drawable-hdpi/ic_qr_code.png -- 2.62kb -> 1.86kb (29.11%)
/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -- 4.00kb -> 2.95kb (26.2%)
/app/src/main/res/drawable-mdpi/ic_qr_code.png -- 1.55kb -> 1.17kb (24.7%)
/app/src/main/res/drawable/app_icon.png -- 143.85kb -> 113.38kb (21.18%)
/app/src/main/res/drawable/ic_aegis_iconx.png -- 19.38kb -> 15.30kb (21.04%)
/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -- 2.21kb -> 1.96kb (11.42%)
/metadata/en-US/images/phoneScreenshots/screenshot4.png -- 186.71kb -> 177.50kb (4.93%)
/docs/diagram.svg -- 11.00kb -> 10.58kb (3.85%)
/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -- 1.58kb -> 1.53kb (3.22%)
/metadata/en-US/images/phoneScreenshots/screenshot5.png -- 147.70kb -> 144.82kb (1.95%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2021-04-28 09:18:59 +00:00
Alexander Bakker a5d3de55a2 Add support for selecting SVG icons through the file selection dialog
Fixes #724
2021-04-14 22:52:00 +02:00
Alexander Bakker b195d0dadd Fix multiple icon rendering issues in card_icon
- Use software rendering on API <= 27
- Use ImageView instead of CircleImageView

Fixes #725
2021-04-14 21:43:59 +02:00
Alexander Bakker 7d3666a5d0 Make the password configuration dialog scrollable
Fixes #722
2021-04-14 17:33:16 +02:00
Alexander Bakker 61ac83b134 Release v2.0-beta1
build / build (push) Canceled after 0s
2021-04-14 13:40:04 +02:00
Alexander Bakker 9c907cf14a Apply top margin to the first entry if there are no groups 2021-04-14 13:33:40 +02:00
Alexander Bakker 5f737582fb Fix syntax issue in arrays.xml 2021-04-14 12:56:31 +02:00
Alexander Bakker 89256dfef5 Update translations from Crowdin 2021-04-14 12:54:21 +02:00
Alexander Bakker 94b4c098de Update icons packs documentation 2021-04-14 12:15:19 +02:00
Alexander Bakker 1d848947d9 Only set the query if its non-empty when switching icon packs
This fixes an issue where the categories would be hidden when switching icon packs
2021-04-13 17:21:10 +02:00
Alexander Bakker 43ebe402ce Fix crash caused by incorrect cast to PreferencesFragment 2021-04-11 13:54:05 +02:00
Alexander BakkerandGitHub ae0b00573f Merge pull request #539 from alexbakker/iconpacks
Introduce support for icon packs
2021-04-10 10:50:36 +02:00
Alexander BakkerandGitHub efb5b10a76 Merge pull request #716 from DerEnderKeks/fix-recent-apps-behavior
Fixed that app vanished from recent apps list after locking
2021-04-09 14:01:58 +02:00
Alexander Bakker 4f38988c0d Introduce support for icon packs 2021-04-07 17:28:08 +02:00
Alexander Bakker c977b9a064 Update dependencies 2021-04-03 11:24:13 +02:00
DerEnderKeks 5353e7c3b1 Changed method to finish activities when locking 2021-04-01 18:03:16 +02:00
DerEnderKeks b999ccc83b Fixed that app vanished from recent apps list after locking 2021-03-31 17:06:39 +02:00
Alexander BakkerandGitHub fea5b8e75a Merge pull request #715 from DerEnderKeks/patch-1
Fixed minor markup irregularities in bug report template
2021-03-31 12:58:47 +02:00
DerEnderKeksandGitHub 6ce661180e Fixed minor markup irregularities in bug report template 2021-03-31 12:50:43 +02:00
Alexander Bakker 230f21b9f0 Update dependencies 2021-03-20 13:11:17 +01:00
Alexander Bakker 801e677928 Silence an overly enthousiastic linker check 2021-03-01 18:44:14 +01:00
Alexander Bakker 143b9a85b4 Update dependencies 2021-03-01 18:21:20 +01:00
Alexander Bakker 71f66e3142 Remove old testdata and update documentation to new location 2021-02-21 17:16:40 +01:00
Alexander Bakker a6ab669811 Don't crash when uiMode is set to UI_MODE_NIGHT_UNDEFINED 2021-02-21 17:08:05 +01:00
Alexander Bakker a519b31bd3 Fix typo in CONTRIBUTING.md 2021-02-18 13:10:26 +01:00
Alexander Bakker c7ef10367f Fix an issue where advanced fields were disabled inadvertently 2021-02-17 12:59:59 +01:00
Alexander BakkerandGitHub 085e8e51dc Merge pull request #688 from jhenninger/feature/receive-shared-image
Add support for creating entries from shared images
2021-02-17 12:01:17 +01:00
Johannes Henninger 167f2d6dad Add support for creating entries from shared images 2021-02-14 16:05:13 +01:00
Alexander Bakker e653e42455 Update dependencies 2021-02-14 16:03:30 +01:00
Alexander Bakker 158eb4609c Remove unused 'stateless' support code from AuthActivity 2021-02-13 19:15:30 +01:00
Alexander Bakker 2242dbc6cd Update build badge for GitHub Actions 2021-02-10 12:56:49 +01:00
Alexander Bakker ca8f3e49ea Hide the vault wipe option in the menu of the import activity
Always showing the dialog can be confusing for users.
2021-02-08 19:11:57 +01:00
Alexander Bakker c3d9ab86c7 Improve the wording of a couple of strings 2021-02-08 18:55:23 +01:00
Alexander Bakker 441a943a9d Update dependencies 2021-02-08 18:46:56 +01:00
Alexander Bakker 293e2ddeb0 When importing, read the file from SAF on a background thread
Fixes issue with importing from Nextcloud. Close #673.
2021-02-07 13:12:41 +01:00
Alexander Bakker 2c8a64f943 Disable some fields if Steam OTP type is selected
Also, move some magic default OTP parameters to constants
2021-01-30 16:41:25 +01:00
Alexander Bakker bc6cb35dc0 Clean up unused variables 2021-01-27 20:44:47 +01:00
Alexander Bakker ad138e4a4c Fix behavior of the new entry highlight in case no scroll is needed 2021-01-27 20:42:39 +01:00
Alexander Bakker f4bdf4645b Hide some entry editing settings by default to reduce user confusion 2021-01-27 20:09:53 +01:00
Alexander Bakker fd9a387b4e Rename Filter -> Sort in menu and remove old references to Filter 2021-01-27 18:32:44 +01:00
Alexander Bakker 778000aa24 Minor fixes for group chips, remove remnants of filter menu item 2021-01-27 18:24:52 +01:00
Alexander Bakker 40fbe39474 Update list of third-party licenses 2021-01-27 14:00:27 +01:00
Alexander Bakker bda1a1d5af Add a dialog for Aegis' license 2021-01-27 13:54:11 +01:00
Alexander Bakker e6e2809c9c Fix the appearance of the toolbar in the scanner activity 2021-01-27 12:36:48 +01:00
Alexander Bakker b91355da47 Remove usage of HtmlCompat 2021-01-24 21:04:25 +01:00
Alexander Bakker 4bb5ee52d9 Fix disabled alert dialog button color by setting alpha using selector 2021-01-24 20:52:24 +01:00
Alexander Bakker de3fcfdb26 Fix the background color of the imported entries list 2021-01-24 20:44:01 +01:00
Alexander Bakker ae71febf10 Move import logic to separate activity to fix a couple of issues
Fixes #456.
Fixes #670.
2021-01-24 20:19:29 +01:00
Alexander Bakker 7be1a74cfd Force higher resolution for analysis and run it on a background thread 2021-01-24 15:02:02 +01:00
Alexander Bakker 22c93bf5c6 Show some additional warning info in the entry deletion dialog
Close #565.
2021-01-24 14:20:29 +01:00
Alexander Bakker 68436fba9c Rewrite layout of EditEntryActivity to not use TableLayout
This prevents excessive overdraw and fixes the UI tests
2021-01-24 12:53:29 +01:00
Alexander Bakker 44ff321f8f Remove pref_tools_group_title from all locales 2021-01-23 19:36:57 +01:00
Alexander Bakker d9d6733878 Improve preference organization and add summaries to the sections 2021-01-23 19:29:41 +01:00
Alexander Bakker eae31ef94b Switch from Appcompat to Material Components theme
Close #354.
2021-01-23 15:23:02 +01:00
Alexander BakkerandGitHub 29ea2df696 Merge pull request #671 from michaelschattgen/feature/chips
Start working on bottomsheet for group chips
2021-01-23 12:08:01 +01:00
Alexander BakkerandMichael Schättgen a6bf3b7c87 Add bottomsheet for chips 2021-01-21 10:53:37 +01:00
Alexander Bakker c6cb390990 Fix the UI tests 2021-01-16 17:43:07 +01:00
Alexander BakkerandGitHub f7a428b8cb Merge pull request #654 from alexbakker/android-backups
Add support for participating in Android's backup system
2021-01-16 16:24:08 +01:00
Alexander Bakker f080eaa8f9 Add support for participating in Android's backup system
This adds an option for participating in Android's backup system. Two items are
backed up: the ``files/aegis.json`` file and the entire ``shared_prefs``
directory. The option is disabled by default and can only be enabled if
encryption is enabled as well.

I tested this with Local Transport and Seedvault. To test with Local Transport,
see: https://developer.android.com/guide/topics/data/testingbackup.
2021-01-16 16:19:25 +01:00
Alexander BakkerandGitHub 07c768893a Merge pull request #666 from alexbakker/separate-prefs
Split preferences into separate fragments
2021-01-16 15:51:26 +01:00
Alexander Bakker ef8c116bdc Split preferences into separate fragments
<img src="https://alexbakker.me/u/5phth0i1ga.png" width="300" />
2021-01-16 15:46:13 +01:00
Alexander BakkerandGitHub 260a3b9c78 Merge pull request #651 from alexbakker/replace-fab
Replace the FAB with a bottom sheet dialog
2021-01-16 15:39:31 +01:00
Alexander BakkerandGitHub a5ec7666ec Merge pull request #647 from alexbakker/more-tests
Add some more tests
2021-01-16 15:39:02 +01:00
Alexander BakkerandGitHub ff450c3edf Merge pull request #644 from alexbakker/remove-acc-name-opt
Remove the "search in account names" option and do so by default
2021-01-16 15:37:08 +01:00
Alexander BakkerandGitHub e6e179307d Merge pull request #663 from alexbakker/fix-650
Explicitly opt out of WebView usage statistics collection
2021-01-16 15:36:36 +01:00
Alexander Bakker e324f649ee Replace the FAB with a bottom sheet dialog
This also removes the dependency on ``com.getbase:floatingactionbutton``.
2021-01-10 14:49:24 +01:00
Alexander Bakker 4f8a0b9020 Add some more tests
This adds tests for all of the importers, a new scrypt test and some more OTP tests. More to come.
2021-01-10 14:47:27 +01:00
Alexander Bakker b78b22acbb Explicitly opt out of WebView usage statistics collection 2021-01-09 15:58:21 +01:00
Alexander Bakker 1d44e05866 Remove the "search in account names" option and do so by default 2020-12-27 20:34:04 +01:00
259 changed files with 11635 additions and 4416 deletions
+3 -3
View File
@@ -11,9 +11,9 @@ guidelines](/CONTRIBUTING.md#bug-reports) before submitting an issue. -->
* __Version__:
* __Source__: (Google Play/GitHub/F-Droid/?)
* __Vault encrypted:__ Yes (with biometric unlock)/Yes/No
* __Device:__
* __Android version and ROM:__
* __Vault encrypted__: Yes (with biometric unlock)/Yes/No
* __Device__:
* __Android version and ROM__:
###### Steps to reproduce
+4 -2
View File
@@ -1,4 +1,4 @@
name: Build
name: build
on: [pull_request, push]
jobs:
build:
@@ -6,5 +6,7 @@ jobs:
steps:
- name: Checkout the code
uses: actions/checkout@v2
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@e2c57acffb2c9aa5a8dc6eda2bbae0b6e495bd4c
- name: Build the app
run: ./gradlew build
run: ./gradlew build
+1 -1
View File
@@ -8,7 +8,7 @@ contribution.
We use [Crowdin](https://crowdin.com/project/aegis-authenticator) to crowdsource
translations of Aegis for lots of different languages. __Pull requests that
add/update a translation are not longer accepted.__ Crowdin is our single source
add/update a translation are no longer accepted.__ Crowdin is our single source
of truth for translations, to keep things easy to maintain.
The top 30 languages are available for translation. It's possible that yours is
+23 -12
View File
@@ -1,7 +1,7 @@
<img align="left" width="80" height="80" src="metadata/en-US/images/icon.png"
alt="App icon">
# Aegis Authenticator [![CI](https://api.travis-ci.org/beemdevelopment/Aegis.svg?branch=master)](https://travis-ci.org/github/beemdevelopment/aegis) [![Crowdin](https://badges.crowdin.net/aegis-authenticator/localized.svg)](https://crowdin.com/project/aegis-authenticator) [![Donate](https://img.shields.io/badge/donate-buy%20us%20a%20beer-%23FF813F)](https://www.buymeacoffee.com/beemdevelopment) [![Matrix](https://img.shields.io/badge/chat-Matrix-blue)](https://matrix.to/#/#aegis:matrix.org)
# Aegis Authenticator [![CI](https://github.com/beemdevelopment/Aegis/workflows/build/badge.svg)](https://github.com/beemdevelopment/Aegis/actions?query=workflow%3Abuild) [![Crowdin](https://badges.crowdin.net/aegis-authenticator/localized.svg)](https://crowdin.com/project/aegis-authenticator) [![Donate](https://img.shields.io/badge/donate-buy%20us%20a%20beer-%23FF813F)](https://www.buymeacoffee.com/beemdevelopment) [![Matrix](https://img.shields.io/badge/chat-Matrix-blue)](https://matrix.to/#/#aegis:matrix.org)
__Aegis Authenticator__ is a free, secure and open source 2FA app for Android.
It aims to provide a secure authenticator for your online services, while also
@@ -28,10 +28,10 @@ The security design of the app and the vault format is described in detail in
- 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)
- Import from other authenticator apps: 2FAS Authenticator, 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
- Alphabetic/custom sorting
- Custom or automatically generated icons
@@ -103,16 +103,27 @@ Certificate fingerprints:
SHA256: C6:DB:80:A8:E1:4E:52:30:C1:DE:84:15:EF:82:0D:13:DC:90:1D:8F:E3:3C:F3:AC:B5:7B:68:62:D8:58:A8:23
```
### Community
### Icon packs
- [aegis-icons](https://github.com/krisu5/aegis-icons)
Aegis supports icon packs to make it easier to assign icons to the entries in
your vault. There are no official icon packs, but the community maintains a
number of third-party icon packs you may want to check out. To learn how to
create your own Aegis-compatible icon pack, see [the
documenation](docs/iconpacks.md).
While we're still [working on better icon
support](https://github.com/beemdevelopment/Aegis/issues/509) in Aegis, krisu5
has started a third-party repository for icons that you may want to check out.
- [aegis-icons](https://github.com/aegis-icons/aegis-icons)
[<img width=500 alt="Aegis-icons preview"
src="https://raw.githubusercontent.com/krisu5/aegis-icons/master/showcase.png">](https://github.com/krisu5/aegis-icons)
Unofficial monochrome-styled 2FA icons.
[<img width=500 alt="aegis-icons preview"
src="https://raw.githubusercontent.com/aegis-icons/aegis-icons/master/showcase.png">](https://github.com/aegis-icons/aegis-icons)
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons)
This project periodically generates an icon pack for Aegis based on [Simple
Icons](https://simpleicons.org/). The icons are automatically generated, so
not all of them are as high quality as the ones you'll find in
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
## Contributing
+42 -29
View File
@@ -24,8 +24,8 @@ android {
applicationId "${packageName}"
minSdkVersion 21
targetSdkVersion 30
versionCode 45
versionName "1.4.2"
versionCode 49
versionName "2.0.1"
multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
@@ -42,13 +42,22 @@ android {
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests.all {
useJUnitPlatform()
unitTests {
all {
maxHeapSize "3g"
ignoreFailures false
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
ignoreFailures false
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
showExceptions true
exceptionFormat "full"
showCauses true
showStackTraces true
}
}
includeAndroidResources true
}
}
@@ -111,31 +120,33 @@ protobuf {
}
dependencies {
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'
def androidTestVersion = '1.3.0'
def cameraxVersion = '1.0.0'
def glideVersion = '4.12.0'
def guavaVersion = '30.1.1'
def junitVersion = '4.13.2'
def libsuVersion = '3.1.2'
annotationProcessor 'androidx.annotation:annotation:1.1.0'
annotationProcessor 'androidx.annotation:annotation:1.2.0'
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.biometric:biometric:1.0.1"
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.camera:camera-camera2:$cameraxVersion"
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
implementation "androidx.camera:camera-view:1.0.0-alpha20"
implementation "androidx.camera:camera-view:1.0.0-alpha24"
implementation 'androidx.cardview:cardview:1.0.0'
implementation "androidx.core:core:1.3.2"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation "androidx.lifecycle:lifecycle-process:2.2.0"
implementation "androidx.lifecycle:lifecycle-process:2.3.1"
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.getbase:floatingactionbutton:1.10.1'
implementation 'com.github.avito-tech:krop:0.51'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.github.avito-tech:krop:0.52'
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
implementation "com.github.bumptech.glide:glide:${glideVersion}"
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
@@ -144,12 +155,12 @@ dependencies {
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
implementation "com.google.guava:guava:${guavaVersion}-android"
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.protobuf:protobuf-javalite:3.14.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.protobuf:protobuf-javalite:3.16.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 'com.nulab-inc:zxcvbn:1.5.0'
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)
@@ -157,19 +168,21 @@ dependencies {
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:core:${androidTestVersion}"
androidTestImplementation "androidx.test:runner:${androidTestVersion}"
androidTestImplementation "androidx.test:rules:${androidTestVersion}"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
androidTestImplementation 'junit:junit:4.13.1'
androidTestImplementation "junit:junit:${junitVersion}"
androidTestUtil 'androidx.test:orchestrator:1.3.0'
testImplementation "androidx.test:core:${androidTestVersion}"
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
testImplementation "junit:junit:${junitVersion}"
testImplementation "org.json:json:20210307"
testImplementation 'org.robolectric:robolectric:4.5.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}
+1
View File
@@ -18,6 +18,7 @@
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
-keep class com.beemdevelopment.aegis.ui.fragments.*
-keep class com.beemdevelopment.aegis.importers.** { *; }
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
@@ -39,6 +39,19 @@ public abstract class AegisTest {
}
protected VaultManager initVault() {
VaultFileCredentials creds = generateCredentials();
VaultManager vault = getApp().initVaultManager(new Vault(), creds);
try {
vault.save(false);
} catch (VaultManagerException e) {
throw new RuntimeException(e);
}
getApp().getPreferences().setIntroDone(true);
return vault;
}
protected VaultFileCredentials generateCredentials() {
PasswordSlot slot = new PasswordSlot();
byte[] salt = CryptoUtils.generateSalt();
SCryptParameters scryptParams = new SCryptParameters(
@@ -59,17 +72,9 @@ public abstract class AegisTest {
| SlotException e) {
throw new RuntimeException(e);
}
creds.getSlots().add(slot);
VaultManager vault = getApp().initVaultManager(new Vault(), creds);
try {
vault.save(false);
} catch (VaultManagerException e) {
throw new RuntimeException(e);
}
getApp().getPreferences().setIntroDone(true);
return vault;
return creds;
}
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
@@ -1,10 +1,9 @@
package com.beemdevelopment.aegis;
import androidx.annotation.IdRes;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.AmbiguousViewMatcherException;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
@@ -26,7 +25,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
import static androidx.test.espresso.action.ViewActions.clearText;
@@ -42,7 +40,6 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.Matchers.anything;
@RunWith(AndroidJUnit4.class)
@LargeTest
@@ -79,7 +76,8 @@ public class OverallTest extends AegisTest {
List<VaultEntry> realEntries = new ArrayList<>(vault.getEntries());
for (int i = 0; i < realEntries.size(); i++) {
assertTrue(realEntries.get(i).equivalates(entries.get(i)));
String message = String.format("%s != %s", realEntries.get(i).toJson().toString(), entries.get(i).toJson().toString());
assertTrue(message, realEntries.get(i).equivalates(entries.get(i)));
}
for (int i = 0; i < 10; i++) {
@@ -92,8 +90,8 @@ public class OverallTest extends AegisTest {
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
onView(withId(R.id.action_edit)).perform(click());
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
onView(withId(R.id.spinner_group)).perform(click());
onData(anything()).atPosition(1).perform(click());
onView(withId(R.id.dropdown_group)).perform(click());
onView(withText(R.string.new_group)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(isRoot()).perform(pressBack());
@@ -105,9 +103,8 @@ public class OverallTest extends AegisTest {
changeSort(R.string.sort_alphabetically_reverse);
changeSort(R.string.sort_custom);
changeFilter(_groupName);
changeFilter(R.string.filter_ungrouped);
changeFilter(R.string.all);
changeGroupFilter(_groupName);
changeGroupFilter(null);
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
@@ -127,13 +124,14 @@ public class OverallTest extends AegisTest {
openContextualActionModeOverflowMenu();
onView(withText(R.string.action_settings)).perform(click());
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
onView(withId(android.R.id.button1)).perform(click());
assertFalse(vault.isEncryptionEnabled());
assertNull(vault.getCredentials());
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
@@ -147,39 +145,38 @@ public class OverallTest extends AegisTest {
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 changeGroupFilter(String text) {
onView(withId(R.id.chip_group)).perform(click());
if (text == null) {
onView(withId(R.id.btnClear)).perform(click());
} else {
onView(withText(text)).perform(click());
onView(isRoot()).perform(pressBack());
}
}
private void addEntry(VaultEntry entry) {
onView(withId(R.id.fab_expand_menu_button)).perform(click());
onView(withId(R.id.fab)).perform(click());
onView(withId(R.id.fab_enter)).perform(click());
onView(withId(R.id.accordian_header)).perform(click());
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
if (entry.getInfo().getClass() != TotpInfo.class) {
int i = entry.getInfo() instanceof HotpInfo ? 1 : 2;
try {
onView(withId(R.id.spinner_type)).perform(click());
onData(anything()).atPosition(i).perform(click());
} catch (AmbiguousViewMatcherException e) {
// for some reason, clicking twice is sometimes necessary, otherwise the test fails on the next line
onView(withId(R.id.spinner_type)).perform(click());
onData(anything()).atPosition(i).perform(click());
}
String otpType;
if (entry.getInfo() instanceof HotpInfo) {
onView(withId(R.id.text_counter)).perform(typeText("0"), closeSoftKeyboard());
}
if (entry.getInfo() instanceof SteamInfo) {
onView(withId(R.id.text_digits)).perform(clearText(), typeText("5"), closeSoftKeyboard());
otpType = "HOTP";
} else if (entry.getInfo() instanceof SteamInfo) {
otpType = "Steam";
} else if (entry.getInfo() instanceof TotpInfo) {
otpType = "TOTP";
} else {
throw new RuntimeException(String.format("Unexpected entry type: %s", entry.getInfo().getClass().getSimpleName()));
}
onView(withId(R.id.dropdown_type)).perform(click());
onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
}
String secret = Base32.encode(entry.getInfo().getSecret());
@@ -0,0 +1,39 @@
package com.beemdevelopment.aegis.vault;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.beemdevelopment.aegis.AegisTest;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VaultManagerTest extends AegisTest {
@Before
public void before() {
initVault();
}
@Test
public void testToggleEncryption() throws VaultManagerException {
getVault().disableEncryption();
assertFalse(getVault().isEncryptionEnabled());
assertNull(getVault().getCredentials());
VaultFileCredentials creds = generateCredentials();
getVault().enableEncryption(creds);
assertTrue(getVault().isEncryptionEnabled());
assertNotNull(getVault().getCredentials());
assertEquals(getVault().getCredentials().getSlots().findAll(PasswordSlot.class).size(), 1);
}
}
+17 -8
View File
@@ -15,12 +15,14 @@
<application
android:name=".AegisApplication"
android:allowBackup="false"
android:allowBackup="true"
android:fullBackupOnly="true"
android:fullBackupContent="@xml/backup_rules"
android:backupAgent=".AegisBackupAgent"
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning"
android:theme="@style/Theme.Aegis.Launch"
tools:replace="android:theme">
<activity android:name=".ui.TransferEntriesActivity"
android:label="@string/title_activity_transfer" />
@@ -28,8 +30,8 @@
android:name=".ui.AboutActivity"
android:label="@string/title_activity_about" />
<activity
android:name=".ui.SelectEntriesActivity"
android:label="@string/title_activity_select_entries" />
android:name=".ui.ImportEntriesActivity"
android:label="@string/title_activity_import_entries" />
<activity
android:name=".ui.MainActivity"
android:label="${title}">
@@ -46,6 +48,13 @@
<data android:scheme="otpauth" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity
android:name=".ui.ScannerActivity"
@@ -57,11 +66,9 @@
android:label="@string/title_activity_edit_entry" />
<activity
android:name=".ui.IntroActivity"
android:theme="@style/AppTheme.NoActionBar"
android:screenOrientation="portrait" />
<activity
android:name=".ui.AuthActivity"
android:theme="@style/AppTheme.NoActionBar" />
android:name=".ui.AuthActivity" />
<activity
android:name=".ui.PreferencesActivity"
android:label="@string/title_activity_preferences" />
@@ -95,6 +102,8 @@
android:resource="@xml/file_paths">
</meta-data>
</provider>
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
</application>
<queries>
+1
View File
@@ -0,0 +1 @@
../../../../LICENSE
+36
View File
@@ -31,6 +31,42 @@
</head>
<body>
<div></div>
<h3>Version 2.0.1</h3>
<h4>New</h4>
<ul>
<li>Support for sorting on most used tokens</li>
<li>Some minor UX and stability improvements</li>
<li>New translation: Vietnamese</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>QR code information was decoded incorrectly in some cases if the app was set to a certain language (Turkish, for example)</li>
</ul>
<h3>Version 2.0</h3>
<h4>New</h4>
<ul>
<li>Support for icon packs</li>
<li>Support for participation in Android's backup system (Google Drive, Seedvault)</li>
<li>UI refresh (switched to the Material Components theme)</li>
<li>Bottom sheet with chips to filter on groups</li>
<li>Support for importing from 2FAS Authenticator</li>
<li>Search in account names by default (and remove the setting)</li>
<li>Replaced the FAB with a bottom sheet dialog</li>
<li>Reorganization of settings into separate categories</li>
<li>Ability to 'share' images of QR codes to scan in Aegis</li>
<li>Option to save the current group filter</li>
<li>New translations for Bulgarian, Danish, Latvian, Swedish and Ukranian</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The QR code scanner had trouble detecting QR codes on some devices due to low resolution image capture</li>
<li>The app would vanish from the recent apps list after locking</li>
<li>When importing from Nextcloud, Aegis would report that the file could not be found.</li>
<li>The biometrics prompt would not appear on some devices</li>
<li>The app would lock when selecting a file/icon on certain devices and configurations</li>
<li>There were multiple layout issues on small screen devices</li>
<li>Various other usability, performance and stability improvements</li>
</ul>
<h3>Version 1.4.2</h3>
<h4>Fixes</h4>
<ul>
+14
View File
@@ -0,0 +1,14 @@
<html>
<head>
<style type="text/css">
body {
background-color: %2$s;
color: %3$s;
font-size: 0.8em;
}
</style>
</head>
<body>
<pre>%1$s</pre>
</body>
</html>
@@ -19,6 +19,7 @@ import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.util.IOUtils;
@@ -41,13 +42,14 @@ public class AegisApplication extends Application {
private Preferences _prefs;
private List<LockListener> _lockListeners;
private boolean _blockAutoLock;
private IconPackManager _iconPackManager;
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
static {
// to access other app's internal storage directory, run libsu commands inside the global mount namespace
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER);
Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER));
}
@Override
@@ -55,6 +57,7 @@ public class AegisApplication extends Application {
super.onCreate();
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
_iconPackManager = new IconPackManager(this);
Iconics.init(this);
Iconics.registerFont(new MaterialDesignIconic());
@@ -96,7 +99,7 @@ public class AegisApplication extends Application {
}
if (_vaultFile == null) {
_vaultFile = VaultManager.readFile(this);
_vaultFile = VaultManager.readVaultFile(this);
}
return _vaultFile;
@@ -126,6 +129,10 @@ public class AegisApplication extends Application {
return _manager;
}
public IconPackManager getIconPackManager() {
return _iconPackManager;
}
public Preferences getPreferences() {
return _prefs;
}
@@ -0,0 +1,118 @@
package com.beemdevelopment.aegis;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.FullBackupDataOutput;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class AegisBackupAgent extends BackupAgent {
private static final String TAG = BackupAgent.class.getSimpleName();
private Preferences _prefs;
@Override
public void onCreate() {
super.onCreate();
_prefs = new Preferences(this);
}
@Override
public synchronized void onFullBackup(FullBackupDataOutput data) throws IOException {
Log.i(TAG, String.format("onFullBackup() called: flags=%d, quota=%d",
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? data.getTransportFlags() : -1,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1));
if (!_prefs.isAndroidBackupsEnabled()) {
Log.i(TAG, "onFullBackup() skipped: Android backups disabled in preferences");
return;
}
// first copy the vault to the files/backup directory
createBackupDir();
File vaultBackupFile = getVaultBackupFile();
try (FileInputStream inStream = VaultManager.getAtomicFile(this).openRead();
FileOutputStream outStream = new FileOutputStream(vaultBackupFile)) {
IOUtils.copy(inStream, outStream);
} catch (IOException e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
deleteBackupDir();
throw e;
}
// then call the original implementation so that fullBackupContent specified in AndroidManifest is read
try {
super.onFullBackup(data);
} catch (IOException e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
throw e;
} finally {
deleteBackupDir();
}
Log.i(TAG, "onFullBackup() finished");
}
@Override
public synchronized void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime) throws IOException {
Log.i(TAG, String.format("onRestoreFile() called: dest=%s", destination));
super.onRestoreFile(data, size, destination, type, mode, mtime);
File vaultBackupFile = getVaultBackupFile();
if (destination.getCanonicalFile().equals(vaultBackupFile.getCanonicalFile())) {
try (InputStream inStream = new FileInputStream(vaultBackupFile)) {
VaultManager.writeToFile(this, inStream);
} catch (IOException e) {
Log.e(TAG, String.format("onRestoreFile() failed: dest=%s, error=%s", destination, e));
throw e;
} finally {
deleteBackupDir();
}
}
Log.i(TAG, String.format("onRestoreFile() finished: dest=%s", destination));
}
@Override
public synchronized void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
super.onQuotaExceeded(backupDataBytes, quotaBytes);
Log.e(TAG, String.format("onQuotaExceeded() called: backupDataBytes=%d, quotaBytes=%d", backupDataBytes, quotaBytes));
}
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException {
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException {
}
private void createBackupDir() throws IOException {
File dir = getVaultBackupFile().getParentFile();
if (!dir.exists() && !dir.mkdir()) {
throw new IOException(String.format("Unable to create backup directory: %s", dir.toString()));
}
}
private void deleteBackupDir() {
File dir = getVaultBackupFile().getParentFile();
IOUtils.clearDirectory(dir, true);
}
private File getVaultBackupFile() {
return new File(new File(getFilesDir(), "backup"), VaultManager.FILENAME);
}
}
@@ -7,8 +7,20 @@ import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Collections;
import org.json.JSONObject;
import java.util.Date;
import java.util.List;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class Preferences {
@@ -37,10 +49,6 @@ public class Preferences {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
public boolean isSearchAccountNameEnabled() {
return _prefs.getBoolean("pref_search_names", false);
}
public boolean isEntryHighlightEnabled() {
return _prefs.getBoolean("pref_highlight_entry", false);
}
@@ -145,6 +153,56 @@ public class Preferences {
_prefs.edit().putInt("pref_current_view_mode", viewMode.ordinal()).apply();
}
public Integer getUsageCount(UUID uuid) {
Integer usageCount = getUsageCounts().get(uuid);
return usageCount != null ? usageCount : 0;
}
public void resetUsageCount(UUID uuid) {
Map<UUID, Integer> usageCounts = getUsageCounts();
usageCounts.put(uuid, 0);
setUsageCount(usageCounts);
}
public void clearUsageCount() {
_prefs.edit().remove("pref_usage_count").apply();
}
public Map<UUID, Integer> getUsageCounts() {
Map<UUID, Integer> usageCounts = new HashMap<>();
String usageCount = _prefs.getString("pref_usage_count", "");
try {
JSONArray arr = new JSONArray(usageCount);
for(int i = 0; i < arr.length(); i++) {
JSONObject json = arr.getJSONObject(i);
usageCounts.put(UUID.fromString(json.getString("uuid")), json.getInt("count"));
}
} catch (JSONException e) {
e.printStackTrace();
}
return usageCounts;
}
public void setUsageCount(Map<UUID, Integer> usageCounts) {
JSONArray usageCountJson = new JSONArray();
for (Map.Entry<UUID, Integer> entry : usageCounts.entrySet()) {
JSONObject entryJson = new JSONObject();
try {
entryJson.put("uuid", entry.getKey());
entryJson.put("count", entry.getValue());
usageCountJson.put(entryJson);
} catch (JSONException e) {
e.printStackTrace();
}
}
_prefs.edit().putString("pref_usage_count", usageCountJson.toString()).apply();
}
public int getTimeout() {
return _prefs.getInt("pref_timeout", -1);
}
@@ -168,6 +226,14 @@ public class Preferences {
return new Locale(parts[0], parts[1]);
}
public boolean isAndroidBackupsEnabled() {
return _prefs.getBoolean("pref_android_backups", false);
}
public void setIsAndroidBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_android_backups", enabled).apply();
}
public boolean isBackupsEnabled() {
return _prefs.getBoolean("pref_backups", false);
}
@@ -220,4 +286,27 @@ public class Preferences {
public boolean isCopyOnTapEnabled() {
return _prefs.getBoolean("pref_copy_on_tap", false);
}
public void setGroupFilter(List<String> groupFilter) {
JSONArray json = new JSONArray(groupFilter);
_prefs.edit().putString("pref_group_filter", json.toString()).apply();
}
public List<String> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter", null);
if (raw == null || raw.isEmpty()) {
return Collections.emptyList();
}
try {
JSONArray json = new JSONArray(raw);
List<String> filter = new ArrayList<>();
for (int i = 0; i < json.length(); i++) {
filter.add(json.getString(i));
}
return filter;
} catch (JSONException e) {
return Collections.emptyList();
}
}
}
@@ -1,5 +1,6 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.helpers.comparators.UsageCountComparator;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
@@ -12,7 +13,8 @@ public enum SortCategory {
ACCOUNT,
ACCOUNT_REVERSED,
ISSUER,
ISSUER_REVERSED;
ISSUER_REVERSED,
USAGE_COUNT;
private static SortCategory[] _values;
@@ -40,6 +42,9 @@ public enum SortCategory {
case ISSUER_REVERSED:
comparator = Collections.reverseOrder(new IssuerNameComparator());
break;
case USAGE_COUNT:
comparator = Collections.reverseOrder(new UsageCountComparator());
break;
}
return comparator;
@@ -57,6 +62,8 @@ public enum SortCategory {
return R.id.menu_sort_alphabetically;
case ISSUER_REVERSED:
return R.id.menu_sort_alphabetically_reverse;
case USAGE_COUNT:
return R.id.menu_sort_usage_count;
default:
return R.id.menu_sort_custom;
}
@@ -10,20 +10,20 @@ public class ThemeMap {
}
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
Theme.LIGHT, R.style.AppTheme,
Theme.DARK, R.style.AppTheme_Dark,
Theme.AMOLED, R.style.AppTheme_TrueBlack
Theme.LIGHT, R.style.Theme_Aegis_Light_Default,
Theme.DARK, R.style.Theme_Aegis_Dark_Default,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Default
);
public static final Map<Theme, Integer> NO_ACTION_BAR = ImmutableMap.of(
Theme.LIGHT, R.style.AppTheme_Light_NoActionBar,
Theme.DARK, R.style.AppTheme_Dark_NoActionBar,
Theme.AMOLED, R.style.AppTheme_TrueBlack_NoActionBar
Theme.LIGHT, R.style.Theme_Aegis_Light_NoActionBar,
Theme.DARK, R.style.Theme_Aegis_Dark_NoActionBar,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_NoActionBar
);
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
Theme.LIGHT, R.style.AppTheme_Fullscreen,
Theme.DARK, R.style.AppTheme_Fullscreen_Dark,
Theme.AMOLED, R.style.AppTheme_Fullscreen_TrueBlack
Theme.LIGHT, R.style.Theme_Aegis_Light_Fullscreen,
Theme.DARK, R.style.Theme_Aegis_Dark_Fullscreen,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Fullscreen
);
}
@@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.util.Locale;
public class Base32 {
private Base32() {
@@ -9,7 +11,7 @@ public class Base32 {
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base32().decode(s.toUpperCase());
return BaseEncoding.base32().decode(s.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
@@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.util.Locale;
public class Hex {
private Hex() {
@@ -9,7 +11,7 @@ public class Hex {
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base16().decode(s.toUpperCase());
return BaseEncoding.base16().decode(s.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
@@ -2,8 +2,8 @@ package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.biometric.BiometricConstants;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
public class BiometricsHelper {
private BiometricsHelper() {
@@ -12,16 +12,16 @@ public class BiometricsHelper {
public static BiometricManager getManager(Context context) {
BiometricManager manager = BiometricManager.from(context);
if (manager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
if (manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS) {
return manager;
}
return null;
}
public static boolean isCanceled(int errorCode) {
return errorCode == BiometricConstants.ERROR_CANCELED
|| errorCode == BiometricConstants.ERROR_USER_CANCELED
|| errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON;
return errorCode == BiometricPrompt.ERROR_CANCELED
|| errorCode == BiometricPrompt.ERROR_USER_CANCELED
|| errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON;
}
public static boolean isAvailable(Context context) {
@@ -0,0 +1,27 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import androidx.annotation.ArrayRes;
import com.beemdevelopment.aegis.R;
import java.util.List;
public class DropdownHelper {
private DropdownHelper() {
}
public static void fillDropdown(Context context, AutoCompleteTextView dropdown, @ArrayRes int textArrayResId) {
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, R.layout.dropdown_list_item);
dropdown.setAdapter(adapter);
}
public static <T> void fillDropdown(Context context, AutoCompleteTextView dropdown, List<T> items) {
ArrayAdapter<T> adapter = new ArrayAdapter<>(context, R.layout.dropdown_list_item, items);
dropdown.setAdapter(adapter);
}
}
@@ -6,7 +6,6 @@ import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
public class FabScrollHelper {
private View _fabMenu;
@@ -0,0 +1,26 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Build;
import android.widget.ImageView;
import com.beemdevelopment.aegis.icons.IconType;
public class IconViewHelper {
private IconViewHelper() {
}
/**
* Sets the layer type of the given ImageView based on the given IconType. If the
* icon type is SVG and SDK <= 27, the layer type is set to software. Otherwise, it
* is set to hardware.
*/
public static void setLayerType(ImageView view, IconType iconType) {
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return;
}
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
}
}
@@ -1,6 +1,9 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.camera.core.ImageAnalysis;
@@ -23,6 +26,7 @@ import static android.graphics.ImageFormat.YUV_444_888;
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
public static final Size RESOLUTION = new Size(1200, 1600);
private final QrCodeAnalyzer.Listener _listener;
@@ -39,11 +43,7 @@ public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
return;
}
ByteBuffer buf = image.getPlanes()[0].getBuffer();
byte[] data = new byte[buf.remaining()];
buf.get(data);
buf.rewind();
byte[] data = getLuminancePlaneData(image);
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
data, image.getWidth(), image.getHeight(), 0, 0, image.getWidth(), image.getHeight(), false
);
@@ -53,7 +53,7 @@ public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
try {
Result result = reader.decode(bitmap);
if (_listener != null) {
_listener.onQrCodeDetected(result);
new Handler(Looper.getMainLooper()).post(() -> _listener.onQrCodeDetected(result));
}
} catch (ChecksumException | FormatException | NotFoundException ignored) {
@@ -62,6 +62,33 @@ public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
}
}
private static byte[] getLuminancePlaneData(ImageProxy image) {
ImageProxy.PlaneProxy plane = image.getPlanes()[0];
ByteBuffer buf = plane.getBuffer();
byte[] data = new byte[buf.remaining()];
buf.get(data);
buf.rewind();
int width = image.getWidth();
int height = image.getHeight();
int rowStride = plane.getRowStride();
int pixelStride = plane.getPixelStride();
if (width != rowStride || pixelStride != 1) {
// remove padding from the Y plane data
byte[] cleanData = new byte[width * height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
cleanData[y * width + x] = data[y * rowStride + x * pixelStride];
}
}
return cleanData;
}
return data;
}
public interface Listener {
void onQrCodeDetected(Result result);
}
@@ -1,31 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import java.util.List;
import androidx.annotation.ArrayRes;
public class SpinnerHelper {
private SpinnerHelper() {
}
public static void fillSpinner(Context context, Spinner spinner, @ArrayRes int textArrayResId) {
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, android.R.layout.simple_spinner_item);
initSpinner(spinner, adapter);
}
public static <T> void fillSpinner(Context context, Spinner spinner, List<T> items) {
ArrayAdapter adapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, items);
initSpinner(spinner, adapter);
}
private static void initSpinner(Spinner spinner, ArrayAdapter adapter) {
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.invalidate();
}
}
@@ -48,6 +48,6 @@ public class TextDrawableHelper {
.width(view.getLayoutParams().width)
.height(view.getLayoutParams().height)
.endConfig()
.buildRect(text.substring(0, 1).toUpperCase(), color);
.buildRound(text.substring(0, 1).toUpperCase(), color);
}
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class UsageCountComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return Integer.compare(a.getUsageCount(), b.getUsageCount());
}
}
@@ -0,0 +1,185 @@
package com.beemdevelopment.aegis.icons;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.common.base.Objects;
import com.google.common.io.Files;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class IconPack {
private UUID _uuid;
private String _name;
private int _version;
private List<Icon> _icons;
private File _dir;
private IconPack(UUID uuid, String name, int version, List<Icon> icons) {
_uuid = uuid;
_name = name;
_version = version;
_icons = icons;
}
public UUID getUUID() {
return _uuid;
}
public String getName() {
return _name;
}
public int getVersion() {
return _version;
}
public List<Icon> getIcons() {
return Collections.unmodifiableList(_icons);
}
/**
* Retrieves a list of icons suggested for the given issuer.
*/
public List<Icon> getSuggestedIcons(String issuer) {
if (issuer == null || issuer.isEmpty()) {
return new ArrayList<>();
}
return _icons.stream()
.filter(i -> i.isSuggestedFor(issuer))
.collect(Collectors.toList());
}
@Nullable
public File getDirectory() {
return _dir;
}
void setDirectory(@NonNull File dir) {
_dir = dir;
}
/**
* Indicates whether some other object is "equal to" this one. The object does not
* necessarily have to be the same instance. Equality of UUID and version will make
* this method return true;
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof IconPack)) {
return false;
}
IconPack pack = (IconPack) o;
return super.equals(pack) || (getUUID().equals(pack.getUUID()) && getVersion() == pack.getVersion());
}
@Override
public int hashCode() {
return Objects.hashCode(_uuid, _version);
}
public static IconPack fromJson(JSONObject obj) throws JSONException {
UUID uuid;
String uuidString = obj.getString("uuid");
try {
uuid = UUID.fromString(uuidString);
} catch (IllegalArgumentException e) {
throw new JSONException(String.format("Bad UUID format: %s", uuidString));
}
String name = obj.getString("name");
int version = obj.getInt("version");
JSONArray array = obj.getJSONArray("icons");
List<Icon> icons = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
Icon icon = Icon.fromJson(array.getJSONObject(i));
icons.add(icon);
}
return new IconPack(uuid, name, version, icons);
}
public static IconPack fromBytes(byte[] data) throws JSONException {
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
return IconPack.fromJson(obj);
}
public static class Icon implements Serializable {
private final String _relFilename;
private final String _category;
private final List<String> _issuers;
private File _file;
protected Icon(String filename, String category, List<String> issuers) {
_relFilename = filename;
_category = category;
_issuers = issuers;
}
public String getRelativeFilename() {
return _relFilename;
}
@Nullable
public File getFile() {
return _file;
}
void setFile(@NonNull File file) {
_file = file;
}
public IconType getIconType() {
return IconType.fromFilename(_relFilename);
}
@SuppressWarnings("UnstableApiUsage")
public String getName() {
return Files.getNameWithoutExtension(new File(_relFilename).getName());
}
public String getCategory() {
return _category;
}
public List<String> getIssuers() {
return Collections.unmodifiableList(_issuers);
}
public boolean isSuggestedFor(String issuer) {
String lowerIssuer = issuer.toLowerCase();
return getIssuers().stream()
.map(String::toLowerCase)
.anyMatch(is -> is.contains(lowerIssuer) || lowerIssuer.contains(is));
}
public static Icon fromJson(JSONObject obj) throws JSONException {
String filename = obj.getString("filename");
String category = obj.isNull("category") ? null : obj.getString("category");
JSONArray array = obj.getJSONArray("issuer");
List<String> issuers = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
String issuer = array.getString(i);
issuers.add(issuer);
}
return new Icon(filename, category, issuers);
}
}
}
@@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.icons;
public class IconPackException extends Exception {
public IconPackException(Throwable cause) {
super(cause);
}
public IconPackException(String message) {
super(message);
}
}
@@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.icons;
public class IconPackExistsException extends IconPackException {
private IconPack _pack;
public IconPackExistsException(IconPack pack) {
super(String.format("Icon pack %s (%d) already exists", pack.getName(), pack.getVersion()));
_pack = pack;
}
public IconPack getIconPack() {
return _pack;
}
}
@@ -0,0 +1,219 @@
package com.beemdevelopment.aegis.icons;
import android.content.Context;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.IOUtils;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.FileHeader;
import org.json.JSONException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class IconPackManager {
private static final String _packDefFilename = "pack.json";
private File _iconsBaseDir;
private List<IconPack> _iconPacks;
public IconPackManager(Context context) {
_iconPacks = new ArrayList<>();
_iconsBaseDir = new File(context.getFilesDir(), "icons");
rescanIconPacks();
}
private IconPack getIconPackByUUID(UUID uuid) {
List<IconPack> packs = _iconPacks.stream().filter(i -> i.getUUID().equals(uuid)).collect(Collectors.toList());
if (packs.size() == 0) {
return null;
}
return packs.get(0);
}
public List<IconPack> getIconPacks() {
return new ArrayList<>(_iconPacks);
}
public void removeIconPack(IconPack pack) throws IconPackException {
try {
File dir = getIconPackDir(pack);
deleteDir(dir);
} catch (IOException e) {
throw new IconPackException(e);
}
_iconPacks.remove(pack);
}
public IconPack importPack(File inFile) throws IconPackException {
try {
// read and parse the icon pack definition file of the icon pack
ZipFile zipFile = new ZipFile(inFile);
FileHeader packHeader = zipFile.getFileHeader(_packDefFilename);
if (packHeader == null) {
throw new IOException("Unable to find pack.json in the root of the ZIP file");
}
IconPack pack;
byte[] defBytes;
try (ZipInputStream inStream = zipFile.getInputStream(packHeader)) {
defBytes = IOUtils.readAll(inStream);
pack = IconPack.fromBytes(defBytes);
}
// create a new directory to store the icon pack, based on the UUID and version
File packDir = getIconPackDir(pack);
if (!packDir.getCanonicalPath().startsWith(_iconsBaseDir.getCanonicalPath() + File.separator)) {
throw new IOException("Attempted to write outside of the parent directory");
}
if (packDir.exists()) {
throw new IconPackExistsException(pack);
}
IconPack existingPack = getIconPackByUUID(pack.getUUID());
if (existingPack != null) {
throw new IconPackExistsException(existingPack);
}
if (!packDir.exists() && !packDir.mkdirs()) {
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
}
// extract each of the defined icons to the icon pack directory
for (IconPack.Icon icon : pack.getIcons()) {
File destFile = new File(packDir, icon.getRelativeFilename());
FileHeader iconHeader = zipFile.getFileHeader(icon.getRelativeFilename());
if (iconHeader == null) {
throw new IOException(String.format("Unable to find %s relative to the root of the ZIP file", icon.getRelativeFilename()));
}
// create new directories for this file if needed
File parent = destFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
}
try (ZipInputStream inStream = zipFile.getInputStream(iconHeader);
FileOutputStream outStream = new FileOutputStream(destFile)) {
IOUtils.copy(inStream, outStream);
}
// after successful copy of the icon, store the new filename
icon.setFile(destFile);
}
// write the icon pack definition file to the newly created directory
try (FileOutputStream outStream = new FileOutputStream(new File(packDir, _packDefFilename))) {
outStream.write(defBytes);
}
// after successful extraction of the icon pack, store the new directory
pack.setDirectory(packDir);
_iconPacks.add(pack);
return pack;
} catch (IOException | JSONException e) {
throw new IconPackException(e);
}
}
private void rescanIconPacks() {
_iconPacks.clear();
File[] dirs = _iconsBaseDir.listFiles();
if (dirs == null) {
return;
}
for (File dir : dirs) {
if (!dir.isDirectory()) {
continue;
}
UUID uuid;
try {
uuid = UUID.fromString(dir.getName());
} catch (IllegalArgumentException e) {
e.printStackTrace();
continue;
}
File versionDir = getLatestVersionDir(dir);
if (versionDir != null) {
IconPack pack;
try (FileInputStream inStream = new FileInputStream(new File(versionDir, _packDefFilename))) {
byte[] bytes = IOUtils.readAll(inStream);
pack = IconPack.fromBytes(bytes);
pack.setDirectory(versionDir);
} catch (JSONException | IOException e) {
e.printStackTrace();
continue;
}
for (IconPack.Icon icon : pack.getIcons()) {
icon.setFile(new File(versionDir, icon.getRelativeFilename()));
}
// do a sanity check on the UUID and version
if (pack.getUUID().equals(uuid) && pack.getVersion() == Integer.parseInt(versionDir.getName())) {
_iconPacks.add(pack);
}
}
}
}
private File getIconPackDir(IconPack pack) {
return new File(_iconsBaseDir, pack.getUUID() + File.separator + pack.getVersion());
}
@Nullable
private static File getLatestVersionDir(File packDir) {
File[] dirs = packDir.listFiles();
if (dirs == null) {
return null;
}
int latestVersion = -1;
for (File versionDir : dirs) {
int version;
try {
version = Integer.parseInt(versionDir.getName());
} catch (NumberFormatException ignored) {
continue;
}
if (latestVersion == -1 || version > latestVersion) {
latestVersion = version;
}
}
if (latestVersion == -1) {
return null;
}
return new File(packDir, Integer.toString(latestVersion));
}
private static void deleteDir(File dir) throws IOException {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
if (children != null) {
for (File child : children) {
deleteDir(child);
}
}
}
if (!dir.delete()) {
throw new IOException(String.format("Unable to delete directory: %s", dir));
}
}
}
@@ -0,0 +1,54 @@
package com.beemdevelopment.aegis.icons;
import com.google.common.io.Files;
import java.util.Locale;
public enum IconType {
INVALID,
SVG,
PNG,
JPEG;
public static IconType fromMimeType(String mimeType) {
switch (mimeType) {
case "image/svg+xml":
return SVG;
case "image/png":
return PNG;
case "image/jpeg":
return JPEG;
default:
return INVALID;
}
}
@SuppressWarnings("UnstableApiUsage")
public static IconType fromFilename(String filename) {
switch (Files.getFileExtension(filename).toLowerCase(Locale.ROOT)) {
case "svg":
return SVG;
case "png":
return PNG;
case "jpg":
// intentional fallthrough
case "jpeg":
return JPEG;
default:
return INVALID;
}
}
public String toMimeType() {
switch (this) {
case SVG:
return "image/svg+xml";
case PNG:
return "image/png";
case JPEG:
return "image/jpeg";
default:
throw new RuntimeException(String.format("Can't convert icon type %s to MIME type", this));
}
}
}
@@ -1,14 +1,20 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.DialogInterface;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryException;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.topjohnwu.superuser.io.SuFile;
@@ -18,6 +24,7 @@ import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class AegisImporter extends DatabaseImporter {
@@ -56,14 +63,46 @@ public class AegisImporter extends DatabaseImporter {
return _file.getHeader().getSlots();
}
public State decrypt(VaultFileCredentials creds) throws VaultFileException {
JSONObject obj = _file.getContent(creds);
public State decrypt(VaultFileCredentials creds) throws DatabaseImporterException {
JSONObject obj;
try {
obj = _file.getContent(creds);
} catch (VaultFileException e) {
throw new DatabaseImporterException(e);
}
return new DecryptedState(obj);
}
public State decrypt(char[] password) throws DatabaseImporterException {
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Result result = PasswordSlotDecryptTask.decrypt(slots, password);
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
return decrypt(creds);
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, (Dialogs.TextInputListener) password -> {
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(context, result -> {
try {
if (result == null) {
throw new DatabaseImporterException("Password incorrect");
}
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
State state = decrypt(creds);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, (DialogInterface.OnCancelListener) dialog -> listener.onCanceled());
}
}
@@ -100,7 +139,7 @@ public class AegisImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultEntry.fromJson(obj);
} catch (JSONException | OtpInfoException | EncodingException e) {
} catch (VaultEntryException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@@ -17,7 +17,7 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
@@ -38,6 +38,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Locale;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -95,7 +96,7 @@ public class AndOtpImporter extends DatabaseImporter {
_data = data;
}
private DecryptedState decryptData(SecretKey key, int offset) throws DatabaseImporterException {
private DecryptedState decryptContent(SecretKey key, int offset) throws DatabaseImporterException {
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
CryptParameters params = new CryptParameters(nonce, tag);
@@ -116,35 +117,52 @@ public class AndOtpImporter extends DatabaseImporter {
}
}
private KeyDerivationParams getKeyDerivationParams(char[] password) throws DatabaseImporterException {
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, INT_SIZE + SALT_SIZE);
return new KeyDerivationParams(password, salt, iterations);
}
protected DecryptedState decryptOldFormat(char[] password) throws DatabaseImporterException {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support the old andOTP backup format
// it is not a secure way to derive a key from a password
MessageDigest hash;
try {
hash = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
return decryptContent(key, 0);
}
protected DecryptedState decryptNewFormat(SecretKey key) throws DatabaseImporterException {
return decryptContent(key, INT_SIZE + SALT_SIZE);
}
protected DecryptedState decryptNewFormat(char[] password)
throws DatabaseImporterException {
KeyDerivationParams params = getKeyDerivationParams(password);
SecretKey key = AndOtpKeyDerivationTask.deriveKey(params);
return decryptNewFormat(key);
}
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
if (oldFormat) {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support the old andOTP backup format
// it is not a secure way to derive a key from a password
MessageDigest hash;
try {
hash = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
DecryptedState state = decryptData(key, 0);
DecryptedState state = decryptOldFormat(password);
listener.onStateDecrypted(state);
} else {
int offset = INT_SIZE + SALT_SIZE;
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
AndOtpKeyDerivationTask.Params params = new AndOtpKeyDerivationTask.Params(password, salt, iterations);
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key1 -> {
KeyDerivationParams params = getKeyDerivationParams(password);
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key -> {
try {
DecryptedState state = decryptData(key1, offset);
DecryptedState state = decryptNewFormat(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
@@ -173,7 +191,7 @@ public class AndOtpImporter extends DatabaseImporter {
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
}, dialog1 -> listener.onCanceled());
})
.create());
}
@@ -208,7 +226,7 @@ public class AndOtpImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String type = obj.getString("type").toLowerCase(Locale.ROOT);
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret"));
@@ -222,7 +240,7 @@ public class AndOtpImporter extends DatabaseImporter {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "steam":
info = new SteamInfo(secret, algo, digits, obj.optInt("period", 30));
info = new SteamInfo(secret, algo, digits, obj.optInt("period", TotpInfo.DEFAULT_PERIOD));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
@@ -251,7 +269,7 @@ public class AndOtpImporter extends DatabaseImporter {
}
}
private static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpKeyDerivationTask.Params, SecretKey> {
protected static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpImporter.KeyDerivationParams, SecretKey> {
private Callback _cb;
public AndOtpKeyDerivationTask(Context context, Callback cb) {
@@ -260,20 +278,22 @@ public class AndOtpImporter extends DatabaseImporter {
}
@Override
protected SecretKey doInBackground(AndOtpKeyDerivationTask.Params... args) {
protected SecretKey doInBackground(AndOtpImporter.KeyDerivationParams... args) {
setPriority();
AndOtpKeyDerivationTask.Params params = args[0];
SecretKey key;
AndOtpImporter.KeyDerivationParams params = args[0];
return deriveKey(params);
}
protected static SecretKey deriveKey(KeyDerivationParams params) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
key = factory.generateSecret(spec);
SecretKey key = factory.generateSecret(spec);
return new SecretKeySpec(key.getEncoded(), "AES");
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
return key;
}
@Override
@@ -282,32 +302,32 @@ public class AndOtpImporter extends DatabaseImporter {
_cb.onTaskFinished(key);
}
public static class Params {
private char[] _password;
private byte[] _salt;
private int _iterations;
public Params(char[] password, byte[] salt, int iterations) {
_iterations = iterations;
_password = password;
_salt = salt;
}
public char[] getPassword() {
return _password;
}
public int getIterations() {
return _iterations;
}
public byte[] getSalt() {
return _salt;
}
}
public interface Callback {
void onTaskFinished(SecretKey key);
}
}
protected static class KeyDerivationParams {
private final char[] _password;
private final byte[] _salt;
private final int _iterations;
public KeyDerivationParams(char[] password, byte[] salt, int iterations) {
_iterations = iterations;
_password = password;
_salt = salt;
}
public char[] getPassword() {
return _password;
}
public int getIterations() {
return _iterations;
}
public byte[] getSalt() {
return _salt;
}
}
}
@@ -2,7 +2,7 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
@@ -37,34 +37,41 @@ public class AuthenticatorPlusImporter extends DatabaseImporter {
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
private final byte[] _data;
private EncryptedState(byte[] data) {
super(true);
_data = data;
}
protected State decrypt(char[] password) throws DatabaseImporterException {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
LocalFileHeader header;
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(null);
return importer.read(zipStream);
}
}
throw new FileNotFoundException(FILENAME);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, password -> {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
LocalFileHeader header;
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(context);
DatabaseImporter.State state = importer.read(zipStream);
listener.onStateDecrypted(state);
return;
}
}
throw new FileNotFoundException(FILENAME);
} catch (IOException | DatabaseImporterException e) {
try {
DatabaseImporter.State state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
}, dialog1 -> listener.onCanceled());
}
}
}
@@ -12,7 +12,7 @@ import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
@@ -42,6 +42,7 @@ import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs";
@@ -154,47 +155,56 @@ public class AuthyImporter extends DatabaseImporter {
_array = array;
}
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject obj = _array.getJSONObject(i);
String secretString = JsonUtils.optString(obj, "encryptedSecret");
if (secretString == null) {
continue;
}
byte[] encryptedSecret = Base64.decode(secretString);
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
key = new SecretKeySpec(key.getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] secret = cipher.doFinal(encryptedSecret);
obj.remove("encryptedSecret");
obj.remove("salt");
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
}
return new DecryptedState(_array);
} catch (JSONException
| EncodingException
| NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| BadPaddingException
| IllegalBlockSizeException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject obj = _array.getJSONObject(i);
String secretString = JsonUtils.optString(obj, "encryptedSecret");
if (secretString == null) {
continue;
}
byte[] encryptedSecret = Base64.decode(secretString);
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] secret = cipher.doFinal(encryptedSecret);
obj.remove("encryptedSecret");
obj.remove("salt");
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
}
DecryptedState state = new DecryptedState(_array);
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (JSONException
| EncodingException
| NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| BadPaddingException
| IllegalBlockSizeException e) {
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
}, dialog1 -> listener.onCanceled());
}
}
@@ -246,7 +256,7 @@ public class AuthyImporter extends DatabaseImporter {
}
int digits = entry.getInt("digits");
OtpInfo info = new TotpInfo(secret, "SHA1", digits, isAuthy ? 10 : 30);
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, digits, isAuthy ? 10 : TotpInfo.DEFAULT_PERIOD);
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
@@ -274,6 +284,10 @@ public class AuthyImporter extends DatabaseImporter {
info.Issuer = info.Name;
info.Name = "";
}
if (info.Name.startsWith(": ")) {
info.Name = info.Name.substring(2);
}
}
}
@@ -27,10 +27,11 @@ public abstract class DatabaseImporter {
static {
// note: keep these lists sorted alphabetically
_importers = new ArrayList<>();
_importers.add(new Definition("2FAS Authenticator", TwoFASImporter.class, R.string.importer_help_2fas, false));
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
@@ -167,8 +168,9 @@ public abstract class DatabaseImporter {
}
}
public interface DecryptListener {
void onStateDecrypted(State state);
void onError(Exception e);
public static abstract class DecryptListener {
protected abstract void onStateDecrypted(State state);
protected abstract void onError(Exception e);
protected abstract void onCanceled();
}
}
@@ -23,6 +23,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class FreeOtpImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
@@ -83,7 +84,7 @@ public class FreeOtpImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String type = obj.getString("type").toLowerCase(Locale.ROOT);
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
@@ -115,30 +116,6 @@ public class FreeOtpImporter extends DatabaseImporter {
}
}
private static List<JSONObject> parseXml(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException {
List<JSONObject> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (!parser.getName().equals("string")) {
skip(parser);
continue;
}
JSONObject entry = parseXmlEntry(parser);
if (entry != null) {
entries.add(entry);
}
}
return entries;
}
private static byte[] toBytes(JSONArray array) throws JSONException {
byte[] bytes = new byte[array.length()];
for (int i = 0; i < array.length(); i++) {
@@ -146,46 +123,4 @@ public class FreeOtpImporter extends DatabaseImporter {
}
return bytes;
}
private static JSONObject parseXmlEntry(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException {
parser.require(XmlPullParser.START_TAG, null, "string");
String name = parser.getAttributeValue(null, "name");
String value = parseXmlText(parser);
parser.require(XmlPullParser.END_TAG, null, "string");
if (name.equals("tokenOrder")) {
return null;
}
return new JSONObject(value);
}
private static String parseXmlText(XmlPullParser parser) throws IOException, XmlPullParserException {
String text = "";
if (parser.next() == XmlPullParser.TEXT) {
text = parser.getText();
parser.nextTag();
}
return text;
}
private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException {
// source: https://developer.android.com/training/basics/network-ops/xml.html
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
}
@@ -91,7 +91,7 @@ public class MicrosoftAuthImporter extends DatabaseImporter {
throw new DatabaseImporterEntryException(String.format("Unsupported OTP type: %d", entry.getType()), entry.toString());
}
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, digits, TotpInfo.DEFAULT_PERIOD);
return new VaultEntry(info, entry.getUserName(), entry.getIssuer());
} catch (EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
@@ -1,5 +1,6 @@
package com.beemdevelopment.aegis.importers;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
@@ -117,10 +118,12 @@ public class SqlImporterHelper {
}
}
@SuppressLint("Range")
public static String getString(Cursor cursor, String columnName) {
return cursor.getString(cursor.getColumnIndex(columnName));
}
@SuppressLint("Range")
public static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
@@ -129,10 +132,12 @@ public class SqlImporterHelper {
return res;
}
@SuppressLint("Range")
public static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
@SuppressLint("Range")
public static long getLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
@@ -14,7 +14,7 @@ import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
@@ -104,8 +104,8 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); ++i) {
String s = array.getString(i);
entries.add(new JSONObject(s));
JSONObject obj = array.getJSONObject(i);
entries.add(obj);
}
return entries;
@@ -119,7 +119,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
_data = data;
}
private DecryptedState decrypt(char[] password) throws DatabaseImporterException {
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this is not a secure way to derive a key from a password
@@ -127,7 +127,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec spec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
@@ -159,7 +159,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
.setPositiveButton(R.string.yes, (dialog, which) -> {
Dialogs.showPasswordInputDialog(context, password -> {
decrypt(password, listener);
});
}, dialog1 -> listener.onCanceled());
})
.setNegativeButton(R.string.no, (dialog, which) -> {
decrypt(PASSWORD, listener);
@@ -221,7 +221,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
}
TotpInfo info = new TotpInfo(secret, "SHA1", 6, 30);
TotpInfo info = new TotpInfo(secret);
String name = obj.optString("name");
String issuer = obj.optString("issuer");
@@ -0,0 +1,94 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
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.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class TwoFASImporter extends DatabaseImporter {
public TwoFASImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
int version = obj.getInt("schemaVersion");
if (version > 1) {
throw new DatabaseImporterException(String.format("Unsupported schema version: %d", version));
}
JSONArray array = obj.getJSONArray("services");
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
entries.add(array.getJSONObject(i));
}
return new TwoFASImporter.State(entries);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private final List<JSONObject> _entries;
public State(List<JSONObject> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _entries) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(obj.getString("secret"));
JSONObject info = obj.getJSONObject("otp");
String issuer = info.getString("issuer");
String name = info.optString("account");
OtpInfo otp = new TotpInfo(secret);
return new VaultEntry(otp, name, issuer);
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -221,7 +221,7 @@ public class GoogleAuthInfo implements Serializable {
case DIGIT_COUNT_UNSPECIFIED:
// intentional fallthrough
case DIGIT_COUNT_SIX:
digits = 6;
digits = TotpInfo.DEFAULT_DIGITS;
break;
case DIGIT_COUNT_EIGHT:
digits = 8;
@@ -252,7 +252,7 @@ public class GoogleAuthInfo implements Serializable {
case OTP_TYPE_UNSPECIFIED:
// intentional fallthrough
case OTP_TYPE_TOTP:
otp = new TotpInfo(secret, algo, digits, 30);
otp = new TotpInfo(secret, algo, digits, TotpInfo.DEFAULT_PERIOD);
break;
case OTP_TYPE_HOTP:
otp = new HotpInfo(secret, algo, digits, params.getCounter());
@@ -11,6 +11,7 @@ import java.security.NoSuchAlgorithmException;
public class HotpInfo extends OtpInfo {
public static final String ID = "hotp";
public static final int DEFAULT_COUNTER = 0;
private long _counter;
@@ -20,7 +21,7 @@ public class HotpInfo extends OtpInfo {
}
public HotpInfo(byte[] secret) throws OtpInfoException {
this(secret, 0);
this(secret, DEFAULT_COUNTER);
}
public HotpInfo(byte[] secret, String algorithm, int digits, long counter) throws OtpInfoException {
@@ -39,7 +40,7 @@ public class HotpInfo extends OtpInfo {
}
@Override
public String getType() {
public String getTypeId() {
return ID;
}
@@ -8,14 +8,18 @@ import org.json.JSONObject;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Locale;
public abstract class OtpInfo implements Serializable {
public static final int DEFAULT_DIGITS = 6;
public static final String DEFAULT_ALGORITHM = "SHA1";
private byte[] _secret;
private String _algorithm;
private int _digits;
public OtpInfo(byte[] secret) throws OtpInfoException {
this(secret, "SHA1", 6);
this(secret, DEFAULT_ALGORITHM, DEFAULT_DIGITS);
}
public OtpInfo(byte[] secret, String algorithm, int digits) throws OtpInfoException {
@@ -26,7 +30,11 @@ public abstract class OtpInfo implements Serializable {
public abstract String getOtp();
public abstract String getType();
public abstract String getTypeId();
public String getType() {
return getTypeId().toUpperCase(Locale.ROOT);
}
public JSONObject toJson() {
JSONObject obj = new JSONObject();
@@ -69,7 +77,7 @@ public abstract class OtpInfo implements Serializable {
if (algorithm.startsWith("Hmac")) {
algorithm = algorithm.substring(4);
}
algorithm = algorithm.toUpperCase();
algorithm = algorithm.toUpperCase(Locale.ROOT);
if (!isAlgorithmValid(algorithm)) {
throw new OtpInfoException(String.format("unsupported algorithm: %s", algorithm));
@@ -127,7 +135,7 @@ public abstract class OtpInfo implements Serializable {
}
OtpInfo info = (OtpInfo) o;
return getType().equals(info.getType())
return getTypeId().equals(info.getTypeId())
&& Arrays.equals(getSecret(), info.getSecret())
&& getAlgorithm(false).equals(info.getAlgorithm(false))
&& getDigits() == info.getDigits();
@@ -5,12 +5,14 @@ import com.beemdevelopment.aegis.crypto.otp.TOTP;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
public class SteamInfo extends TotpInfo {
public static final String ID = "steam";
public static final int DIGITS = 5;
public SteamInfo(byte[] secret) throws OtpInfoException {
super(secret, "SHA1", 5, 30);
super(secret, OtpInfo.DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD);
}
public SteamInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
@@ -28,7 +30,13 @@ public class SteamInfo extends TotpInfo {
}
@Override
public String getType() {
public String getTypeId() {
return ID;
}
@Override
public String getType() {
String id = getTypeId();
return id.substring(0, 1).toUpperCase(Locale.ROOT) + id.substring(1);
}
}
@@ -11,12 +11,13 @@ import java.security.NoSuchAlgorithmException;
public class TotpInfo extends OtpInfo {
public static final String ID = "totp";
public static final int DEFAULT_PERIOD = 30;
private int _period;
public TotpInfo(byte[] secret) throws OtpInfoException {
super(secret);
setPeriod(30);
setPeriod(DEFAULT_PERIOD);
}
public TotpInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
@@ -34,8 +35,17 @@ public class TotpInfo extends OtpInfo {
}
}
public String getOtp(long time) {
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
return otp.toString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getType() {
public String getTypeId() {
return ID;
}
@@ -21,6 +21,8 @@ import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.licenses.GlideLicense;
import com.beemdevelopment.aegis.licenses.ProtobufLicense;
import com.beemdevelopment.aegis.ui.dialogs.ChangelogDialog;
import com.beemdevelopment.aegis.ui.dialogs.LicenseDialog;
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
import de.psdev.licensesdialog.LicenseResolver;
@@ -42,14 +44,22 @@ public class AboutActivity extends AegisActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
setSupportActionBar(findViewById(R.id.toolbar));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
View btnLicenses = findViewById(R.id.btn_licenses);
btnLicenses.setOnClickListener(v -> showLicenseDialog());
View btnLicense = findViewById(R.id.btn_license);
btnLicense.setOnClickListener(v -> {
LicenseDialog.create()
.setTheme(getConfiguredTheme())
.show(getSupportFragmentManager(), null);
});
View btnThirdPartyLicenses = findViewById(R.id.btn_third_party_licenses);
btnThirdPartyLicenses.setOnClickListener(v -> showThirdPartyLicenseDialog());
TextView appVersion = findViewById(R.id.app_version);
appVersion.setText(getCurrentAppVersion());
@@ -79,7 +89,9 @@ public class AboutActivity extends AegisActivity {
View btnChangelog = findViewById(R.id.btn_changelog);
btnChangelog.setOnClickListener(v -> {
ChangelogDialog.create().setTheme(getConfiguredTheme()).show(getSupportFragmentManager(), "CHANGELOG_DIALOG");
ChangelogDialog.create()
.setTheme(getConfiguredTheme())
.show(getSupportFragmentManager(), null);
});
}
@@ -115,7 +127,7 @@ public class AboutActivity extends AegisActivity {
startActivity(Intent.createChooser(mailIntent, this.getString(R.string.email)));
}
private void showLicenseDialog() {
private void showThirdPartyLicenseDialog() {
String stylesheet = getString(R.string.custom_notices_format_style);
int backgroundColorResource = getConfiguredTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
String backgroundColor = getThemeColorAsHex(backgroundColorResource);
@@ -130,7 +142,7 @@ public class AboutActivity extends AegisActivity {
new LicensesDialog.Builder(this)
.setNotices(R.raw.notices)
.setTitle(R.string.licenses)
.setTitle(R.string.third_party_licenses)
.setNoticesCssStyle(stylesheet)
.setIncludeOwnLicense(true)
.build()
@@ -1,5 +1,6 @@
package com.beemdevelopment.aegis.ui;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.res.Configuration;
@@ -10,14 +11,18 @@ import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
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.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.Map;
@@ -53,6 +58,7 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
@Override
@CallSuper
protected void onDestroy() {
_app.unregisterLockListener(this);
super.onDestroy();
@@ -65,29 +71,17 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
_app.setBlockAutoLock(false);
}
@Override
public void startActivityForResult(Intent intent, int requestCode, Bundle bundle) {
if (isAutoLockBypassedForAction(intent.getAction())) {
_app.setBlockAutoLock(true);
}
try {
super.startActivityForResult(intent, requestCode, bundle);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
if (isDocsAction(intent.getAction())) {
Dialogs.showErrorDialog(this, R.string.documentsui_error, e);
} else {
throw e;
}
}
}
@Override
public void onLocked(boolean userInitiated) {
setResult(RESULT_CANCELED, null);
finishAndRemoveTask();
try {
Method method = Activity.class.getDeclaredMethod("finish", int.class);
method.setAccessible(true);
method.invoke(this, 2); // FINISH_TASK_WITH_ACTIVITY = 2
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
finishAndRemoveTask();
}
}
protected AegisApplication getApp() {
@@ -119,13 +113,10 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
switch (currentNightMode) {
case Configuration.UI_MODE_NIGHT_NO:
theme = Theme.LIGHT;
break;
case Configuration.UI_MODE_NIGHT_YES:
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
break;
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
} else {
theme = Theme.LIGHT;
}
}
@@ -159,16 +150,66 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
return !(this instanceof MainActivity) && !(this instanceof AuthActivity) && !(this instanceof IntroActivity) && _app.isVaultLocked();
}
private static boolean isDocsAction(@Nullable String action) {
return action != null && (action.equals(Intent.ACTION_GET_CONTENT)
|| action.equals(Intent.ACTION_CREATE_DOCUMENT)
|| action.equals(Intent.ACTION_OPEN_DOCUMENT)
|| action.equals(Intent.ACTION_OPEN_DOCUMENT_TREE));
}
public static class Helper {
private Helper() {
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)));
}
/**
* Starts an external activity, temporarily blocks automatic lock of Aegis and
* shows an error dialog if the target activity is not found.
*/
public static void startExtActivityForResult(Activity activity, Intent intent, int requestCode) {
AegisApplication app = (AegisApplication) activity.getApplication();
app.setBlockAutoLock(true);
try {
activity.startActivityForResult(intent, requestCode, null);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
if (isDocsAction(intent.getAction())) {
Dialogs.showErrorDialog(activity, R.string.documentsui_error, e);
} else {
throw e;
}
}
}
/**
* Starts an external activity, temporarily blocks automatic lock of Aegis and
* shows an error dialog if the target activity is not found.
*/
public static void startExtActivity(Fragment fragment, Intent intent) {
startExtActivityForResult(fragment, intent, -1);
}
/**
* Starts an external activity, temporarily blocks automatic lock of Aegis and
* shows an error dialog if the target activity is not found.
*/
public static void startExtActivityForResult(Fragment fragment, Intent intent, int requestCode) {
AegisApplication app = (AegisApplication) fragment.getActivity().getApplication();
app.setBlockAutoLock(true);
try {
fragment.startActivityForResult(intent, requestCode, null);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
if (isDocsAction(intent.getAction())) {
Dialogs.showErrorDialog(fragment.getContext(), R.string.documentsui_error, e);
} else {
throw e;
}
}
}
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));
}
}
}
@@ -33,6 +33,7 @@ import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
@@ -64,7 +65,6 @@ public class AuthActivity extends AegisActivity {
private boolean _inhibitBioPrompt;
private Preferences _prefs;
private boolean _stateless;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -93,19 +93,14 @@ public class AuthActivity extends AegisActivity {
} else {
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
}
_slots = (SlotList) intent.getSerializableExtra("slots");
_stateless = _slots != null;
if (!_stateless) {
VaultFile vaultFile;
try {
vaultFile = getApp().loadVaultFile();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> onBackPressed());
return;
}
try {
VaultFile vaultFile = getApp().loadVaultFile();
_slots = vaultFile.getHeader().getSlots();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> onBackPressed());
return;
}
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
@@ -180,11 +175,7 @@ public class AuthActivity extends AegisActivity {
@Override
public void onBackPressed() {
if (_stateless) {
super.onBackPressed();
} else {
finishAffinity();
}
finishAffinity();
}
@Override
@@ -220,11 +211,6 @@ public class AuthActivity extends AegisActivity {
}
}
@Override
protected boolean isOrphan() {
return _stateless && super.isOrphan();
}
private void focusPasswordField() {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
@@ -279,27 +265,19 @@ public class AuthActivity extends AegisActivity {
private void finish(MasterKey key, boolean isSlotRepaired) {
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
if (_stateless) {
// send the master key back to the calling activity
Intent intent = new Intent();
intent.putExtra("creds", creds);
setResult(RESULT_OK, intent);
} else {
try {
AegisApplication app = getApp();
app.initVaultManager(app.loadVaultFile(), creds);
if (isSlotRepaired) {
saveVault(true);
}
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.decryption_corrupt_error, e);
return;
try {
AegisApplication app = getApp();
app.initVaultManager(app.loadVaultFile(), creds);
if (isSlotRepaired) {
saveVault(true);
}
setResult(RESULT_OK);
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.decryption_corrupt_error, e);
return;
}
setResult(RESULT_OK);
finish();
}
@@ -1,94 +0,0 @@
package com.beemdevelopment.aegis.ui;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Bundle;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
public class ChangelogDialog extends DialogFragment {
private Theme _themeStyle;
public static ChangelogDialog create() {
return new ChangelogDialog();
}
@SuppressLint("InflateParams")
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View customView;
try {
customView = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_web_view, null);
} catch (InflateException e) {
e.printStackTrace();
return new AlertDialog.Builder(getActivity())
.setTitle(android.R.string.dialog_alert_title)
.setMessage(getString(R.string.webview_error))
.setPositiveButton(android.R.string.ok, null)
.show();
}
AlertDialog dialog = new AlertDialog.Builder(getActivity())
.setTitle("Changelog")
.setView(customView)
.setPositiveButton(android.R.string.ok, null)
.show();
final WebView webView = customView.findViewById(R.id.web_view);
StringBuilder buf = new StringBuilder();
try (InputStream html = getActivity().getAssets().open("changelog.html")) {
BufferedReader in = new BufferedReader(new InputStreamReader(html, "UTF-8"));
String str;
while ((str = in.readLine()) != null)
buf.append(str);
in.close();
String changelog = buf.toString();
changelog = replaceStylesheet(changelog);
webView.loadData(changelog, "text/html", "UTF-8");
} catch (IOException e) {
webView.loadData("<h1>Unable to load</h1><p>" + e.getLocalizedMessage() + "</p>", "text/html", "UTF-8");
}
return dialog;
}
private String replaceStylesheet(String changelog) {
int backgroundColorResource = _themeStyle == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
String backgroundColor = colorToCSS(ThemeHelper.getThemeColor(backgroundColorResource, getContext().getTheme()));
String textColor = colorToCSS(0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getContext().getTheme()));
return String.format(changelog, backgroundColor, textColor);
}
private static String colorToCSS(int color) {
return String.format("rgb(%d, %d, %d)", Color.red(color), Color.green(color), Color.blue(color));
}
public ChangelogDialog setTheme(Theme theme) {
_themeStyle = theme;
return this;
}
}
@@ -1,6 +1,5 @@
package com.beemdevelopment.aegis.ui;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -13,50 +12,70 @@ import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.webkit.MimeTypeMap;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.AutoCompleteTextView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Spinner;
import android.widget.TableRow;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.documentfile.provider.DocumentFile;
import com.amulyakhare.textdrawable.TextDrawable;
import com.avito.android.krop.KropView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import de.hdodenhof.circleimageview.CircleImageView;
@@ -64,29 +83,31 @@ public class EditEntryActivity extends AegisActivity {
private static final int PICK_IMAGE_REQUEST = 0;
private boolean _isNew = false;
private boolean _isManual = false;
private VaultEntry _origEntry;
private TreeSet<String> _groups;
private boolean _hasCustomIcon = false;
// keep track of icon changes separately as the generated jpeg's are not deterministic
private boolean _hasChangedIcon = false;
private IconPack.Icon _selectedIcon;
private boolean _isEditingIcon;
private CircleImageView _iconView;
private ImageView _saveImageButton;
private EditText _textName;
private EditText _textIssuer;
private EditText _textPeriod;
private EditText _textCounter;
private EditText _textDigits;
private EditText _textSecret;
private TextInputEditText _textName;
private TextInputEditText _textIssuer;
private TextInputEditText _textPeriodCounter;
private TextInputLayout _textPeriodCounterLayout;
private TextInputEditText _textDigits;
private TextInputLayout _textDigitsLayout;
private TextInputEditText _textSecret;
private TextInputEditText _textUsageCount;
private TableRow _rowPeriod;
private TableRow _rowCounter;
private Spinner _spinnerType;
private Spinner _spinnerAlgo;
private Spinner _spinnerGroup;
private List<String> _spinnerGroupList = new ArrayList<>();
private AutoCompleteTextView _dropdownType;
private AutoCompleteTextView _dropdownAlgo;
private TextInputLayout _dropdownAlgoLayout;
private AutoCompleteTextView _dropdownGroup;
private List<String> _dropdownGroupList = new ArrayList<>();
private KropView _kropView;
@@ -99,6 +120,7 @@ public class EditEntryActivity extends AegisActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_entry);
setSupportActionBar(findViewById(R.id.toolbar));
_vault = getApp().getVaultManager();
_groups = _vault.getGroups();
@@ -114,40 +136,60 @@ public class EditEntryActivity extends AegisActivity {
_origEntry = _vault.getEntryByUUID(entryUUID);
} else {
_origEntry = (VaultEntry) intent.getSerializableExtra("newEntry");
_isManual = intent.getBooleanExtra("isManual", false);
_isNew = true;
setTitle(R.string.add_new_entry);
}
String selectedGroup = intent.getStringExtra("selectedGroup");
// set up fields
_iconView = findViewById(R.id.profile_drawable);
_kropView = findViewById(R.id.krop_view);
_saveImageButton = findViewById(R.id.iv_saveImage);
_textName = findViewById(R.id.text_name);
_textIssuer = findViewById(R.id.text_issuer);
_textPeriod = findViewById(R.id.text_period);
_textPeriodCounter = findViewById(R.id.text_period_counter);
_textPeriodCounterLayout = findViewById(R.id.text_period_counter_layout);
_textDigits = findViewById(R.id.text_digits);
_rowPeriod = findViewById(R.id.row_period);
_textCounter = findViewById(R.id.text_counter);
_rowCounter = findViewById(R.id.row_counter);
_textDigitsLayout = findViewById(R.id.text_digits_layout);
_textSecret = findViewById(R.id.text_secret);
_spinnerType = findViewById(R.id.spinner_type);
SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array);
_spinnerAlgo = findViewById(R.id.spinner_algo);
SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
_spinnerGroup = findViewById(R.id.spinner_group);
updateGroupSpinnerList();
SpinnerHelper.fillSpinner(this, _spinnerGroup, _spinnerGroupList);
_textUsageCount = findViewById(R.id.text_usage_count);
_dropdownType = findViewById(R.id.dropdown_type);
DropdownHelper.fillDropdown(this, _dropdownType, R.array.otp_types_array);
_dropdownAlgoLayout = findViewById(R.id.dropdown_algo_layout);
_dropdownAlgo = findViewById(R.id.dropdown_algo);
DropdownHelper.fillDropdown(this, _dropdownAlgo, R.array.otp_algo_array);
_dropdownGroup = findViewById(R.id.dropdown_group);
updateGroupDropdownList();
DropdownHelper.fillDropdown(this, _dropdownGroup, _dropdownGroupList);
// if this is NOT a manually entered entry, move the "Secret" field from basic to advanced settings
if (!_isNew || (_isNew && !_isManual)) {
LinearLayout layoutSecret = findViewById(R.id.layout_secret);
LinearLayout layoutBasic = findViewById(R.id.layout_basic);
LinearLayout layoutAdvanced = findViewById(R.id.layout_advanced);
layoutBasic.removeView(layoutSecret);
layoutAdvanced.addView(layoutSecret, 0);
((LinearLayout.LayoutParams) layoutSecret.getLayoutParams()).topMargin = 0;
if (_isNew && !_isManual) {
setViewEnabled(layoutAdvanced, false);
}
} else {
LinearLayout layoutTypeAlgo = findViewById(R.id.layout_type_algo);
((LinearLayout.LayoutParams) layoutTypeAlgo.getLayoutParams()).topMargin = 0;
}
_advancedSettingsHeader = findViewById(R.id.accordian_header);
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
_advancedSettings = findViewById(R.id.expandableLayout);
// fill the fields with values if possible
if (_origEntry.hasIcon()) {
IconViewHelper.setLayerType(_iconView, _origEntry.getIconType());
Glide.with(this)
.asDrawable()
.load(_origEntry)
.set(IconLoader.ICON_TYPE, _origEntry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
@@ -162,11 +204,11 @@ public class EditEntryActivity extends AegisActivity {
OtpInfo info = _origEntry.getInfo();
if (info instanceof TotpInfo) {
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
_rowPeriod.setVisibility(View.VISIBLE);
_textPeriodCounterLayout.setHint(R.string.period_hint);
_textPeriodCounter.setText(Integer.toString(((TotpInfo) info).getPeriod()));
} else if (info instanceof HotpInfo) {
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
_rowCounter.setVisibility(View.VISIBLE);
_textPeriodCounterLayout.setHint(R.string.counter);
_textPeriodCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
} else {
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", info.getClass()));
}
@@ -178,11 +220,9 @@ public class EditEntryActivity extends AegisActivity {
_textSecret.setText(secretString);
}
String type = _origEntry.getInfo().getType();
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
String algo = _origEntry.getInfo().getAlgorithm(false);
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
_dropdownType.setText(_origEntry.getInfo().getType(), false);
_dropdownAlgo.setText(_origEntry.getInfo().getAlgorithm(false), false);
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
String group = _origEntry.getGroup();
setGroup(group);
@@ -192,81 +232,74 @@ public class EditEntryActivity extends AegisActivity {
_textName.addTextChangedListener(_iconChangeListener);
// show/hide period and counter fields on type change
_spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String type = _spinnerType.getSelectedItem().toString().toLowerCase();
switch (type) {
case TotpInfo.ID:
case SteamInfo.ID:
_rowCounter.setVisibility(View.GONE);
_rowPeriod.setVisibility(View.VISIBLE);
break;
case HotpInfo.ID:
_rowPeriod.setVisibility(View.GONE);
_rowCounter.setVisibility(View.VISIBLE);
break;
default:
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
}
_dropdownType.setOnItemClickListener((parent, view, position, id) -> {
String type = _dropdownType.getText().toString().toLowerCase(Locale.ROOT);
switch (type) {
case SteamInfo.ID:
_dropdownAlgo.setText(OtpInfo.DEFAULT_ALGORITHM, false);
_textPeriodCounterLayout.setHint(R.string.period_hint);
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
_textDigits.setText(String.valueOf(SteamInfo.DIGITS));
break;
case TotpInfo.ID:
_textPeriodCounterLayout.setHint(R.string.period_hint);
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
break;
case HotpInfo.ID:
_textPeriodCounterLayout.setHint(R.string.counter);
_textPeriodCounter.setText(String.valueOf(HotpInfo.DEFAULT_COUNTER));
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
break;
default:
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
updateAdvancedFieldStatus(type);
});
final Activity activity = this;
_spinnerGroup.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
private int prevPosition;
_iconView.setOnClickListener(v -> {
startIconSelection();
});
_dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
private int prevPosition = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position == _spinnerGroupList.size() - 1) {
Dialogs.showTextInputDialog(activity, R.string.set_group, R.string.group_name_hint, text -> {
String str = new String(text);
if (str.isEmpty()) {
return;
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (position == _dropdownGroupList.size() - 1) {
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, text -> {
String groupName = new String(text);
if (!groupName.isEmpty()) {
_groups.add(groupName);
updateGroupDropdownList();
_dropdownGroup.setText(groupName, false);
}
_groups.add(str);
// reset the selection to "No group" to work around a quirk
_spinnerGroup.setSelection(0, false);
updateGroupSpinnerList();
_spinnerGroup.setSelection(_spinnerGroupList.indexOf(str), false);
});
_spinnerGroup.setSelection(prevPosition, false);
_dropdownGroup.setText(_dropdownGroupList.get(prevPosition), false);
} else {
prevPosition = position;
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
_iconView.setOnClickListener(v -> {
startIconSelectionActivity();
});
_textUsageCount.setText(getPreferences().getUsageCount(entryUUID).toString());
}
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
// automatically open advanced settings since 'Secret' is required.
if (_isNew) {
openAdvancedSettings();
setGroup(selectedGroup);
}
private void updateAdvancedFieldStatus(String otpType) {
boolean enabled = !otpType.equals(SteamInfo.ID) && (!_isNew || _isManual);
_textDigitsLayout.setEnabled(enabled);
_textPeriodCounterLayout.setEnabled(enabled);
_dropdownAlgoLayout.setEnabled(enabled);
}
private void setGroup(String groupName) {
if (groupName == null) {
return;
int pos = 0;
if (groupName != null) {
pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() + 1 : 0;
}
int pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() : -1;
_spinnerGroup.setSelection(pos + 1, false);
_dropdownGroup.setText(_dropdownGroupList.get(pos), false);
}
private void openAdvancedSettings() {
@@ -315,12 +348,12 @@ public class EditEntryActivity extends AegisActivity {
});
}
private void updateGroupSpinnerList() {
private void updateGroupDropdownList() {
Resources res = getResources();
_spinnerGroupList.clear();
_spinnerGroupList.add(res.getString(R.string.no_group));
_spinnerGroupList.addAll(_groups);
_spinnerGroupList.add(res.getString(R.string.new_group));
_dropdownGroupList.clear();
_dropdownGroupList.add(res.getString(R.string.no_group));
_dropdownGroupList.addAll(_groups);
_dropdownGroupList.add(res.getString(R.string.new_group));
}
@Override
@@ -370,16 +403,26 @@ public class EditEntryActivity extends AegisActivity {
onSave();
break;
case R.id.action_delete:
Dialogs.showDeleteEntryDialog(this, (dialog, which) -> {
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry), (dialog, which) -> {
deleteAndFinish(_origEntry);
});
break;
case R.id.action_edit_icon:
startIconSelectionActivity();
startIconSelection();
break;
case R.id.action_reset_usage_count:
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.action_reset_usage_count)
.setMessage(R.string.action_reset_usage_count_dialog)
.setPositiveButton(android.R.string.yes, (dialog, which) -> resetUsageCount())
.setNegativeButton(android.R.string.no, null)
.create());
break;
case R.id.action_default_icon:
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
_selectedIcon = null;
_hasCustomIcon = false;
_hasChangedIcon = true;
default:
@@ -389,7 +432,7 @@ public class EditEntryActivity extends AegisActivity {
return true;
}
private void startIconSelectionActivity() {
private void startImageSelectionActivity() {
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
@@ -398,7 +441,50 @@ public class EditEntryActivity extends AegisActivity {
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
startActivityForResult(chooserIntent, PICK_IMAGE_REQUEST);
AegisActivity.Helper.startExtActivityForResult(this, chooserIntent, PICK_IMAGE_REQUEST);
}
private void resetUsageCount() {
getPreferences().resetUsageCount(_origEntry.getUUID());
_textUsageCount.setText("0");
}
private void startIconSelection() {
List<IconPack> iconPacks = getApp().getIconPackManager().getIconPacks().stream()
.sorted(Comparator.comparing(IconPack::getName))
.collect(Collectors.toList());
if (iconPacks.size() == 0) {
startImageSelectionActivity();
return;
}
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
selectIcon(icon);
}
@Override
public void onCustomSelected() {
startImageSelectionActivity();
}
});
Dialogs.showSecureDialog(dialog);
}
private void selectIcon(IconPack.Icon icon) {
_selectedIcon = icon;
_hasCustomIcon = true;
_hasChangedIcon = true;
IconViewHelper.setLayerType(_iconView, icon.getIconType());
Glide.with(EditEntryActivity.this)
.asDrawable()
.load(icon.getFile())
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
}
private void startEditingIcon(Uri data) {
@@ -429,11 +515,12 @@ public class EditEntryActivity extends AegisActivity {
}
private void stopEditingIcon(boolean save) {
if (save) {
if (save && _selectedIcon == null) {
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
}
_iconView.setVisibility(View.VISIBLE);
_kropView.setVisibility(View.GONE);
_hasCustomIcon = _hasCustomIcon || save;
_hasChangedIcon = save;
_isEditingIcon = false;
@@ -485,15 +572,46 @@ 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) {
startEditingIcon(data.getData());
String fileType = getMimeType(data.getData());
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
ImportFileTask task = new ImportFileTask(this, result -> {
if (result.getException() == null) {
CustomSvgIcon icon = new CustomSvgIcon(result.getFile());
selectIcon(icon);
} else {
Dialogs.showErrorDialog(this, R.string.reading_file_error, result.getException());
}
});
task.execute(getLifecycle(), params);
} else {
startEditingIcon(data.getData());
}
}
super.onActivityResult(requestCode, resultCode, data);
}
private String getMimeType(Uri uri) {
DocumentFile file = DocumentFile.fromSingleUri(this, uri);
if (file != null) {
String fileType = file.getType();
if (fileType != null) {
return fileType;
}
String ext = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
if (ext != null) {
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
}
}
return null;
}
private int parsePeriod() throws ParseException {
try {
return Integer.parseInt(_textPeriod.getText().toString());
return Integer.parseInt(_textPeriodCounter.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Period is not an integer.");
}
@@ -504,8 +622,8 @@ public class EditEntryActivity extends AegisActivity {
throw new ParseException("Secret is a required field.");
}
String type = _spinnerType.getSelectedItem().toString();
String algo = _spinnerAlgo.getSelectedItem().toString();
String type = _dropdownType.getText().toString();
String algo = _dropdownAlgo.getText().toString();
int digits;
try {
@@ -527,7 +645,7 @@ public class EditEntryActivity extends AegisActivity {
OtpInfo info;
try {
switch (type.toLowerCase()) {
switch (type.toLowerCase(Locale.ROOT)) {
case TotpInfo.ID:
info = new TotpInfo(secret, algo, digits, parsePeriod());
break;
@@ -537,7 +655,7 @@ public class EditEntryActivity extends AegisActivity {
case HotpInfo.ID:
long counter;
try {
counter = Long.parseLong(_textCounter.getText().toString());
counter = Long.parseLong(_textPeriodCounter.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Counter is not an integer.");
}
@@ -558,9 +676,9 @@ public class EditEntryActivity extends AegisActivity {
entry.setIssuer(_textIssuer.getText().toString());
entry.setName(_textName.getText().toString());
int groupPos = _spinnerGroup.getSelectedItemPosition();
int groupPos = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
if (groupPos != 0) {
String group = _spinnerGroupList.get(_spinnerGroup.getSelectedItemPosition());
String group = _dropdownGroupList.get(groupPos);
entry.setGroup(group);
} else {
entry.setGroup(null);
@@ -568,13 +686,25 @@ public class EditEntryActivity extends AegisActivity {
if (_hasChangedIcon) {
if (_hasCustomIcon) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
byte[] data = stream.toByteArray();
entry.setIcon(data);
if (_selectedIcon == null) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// the quality parameter is ignored for PNG
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] data = stream.toByteArray();
entry.setIcon(data, IconType.PNG);
} else {
byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
iconBytes = IOUtils.readFile(inStream);
} catch (IOException e) {
throw new ParseException(e.getMessage());
}
entry.setIcon(iconBytes, _selectedIcon.getIconType());
}
} else {
entry.setIcon(null);
entry.setIcon(null, IconType.INVALID);
}
}
@@ -606,7 +736,18 @@ public class EditEntryActivity extends AegisActivity {
return true;
}
private TextWatcher _iconChangeListener = new TextWatcher() {
private static void setViewEnabled(View view, boolean enabled) {
view.setEnabled(enabled);
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
for (int i = 0; i < group.getChildCount(); i++) {
setViewEnabled(group.getChildAt(i), enabled);
}
}
}
private final TextWatcher _iconChangeListener = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@@ -624,19 +765,28 @@ public class EditEntryActivity extends AegisActivity {
}
};
private int getStringResourceIndex(@ArrayRes int id, String string) {
String[] res = getResources().getStringArray(id);
for (int i = 0; i < res.length; i++) {
if (res[i].equalsIgnoreCase(string)) {
return i;
}
}
return -1;
}
private static class ParseException extends Exception {
public ParseException(String message) {
super(message);
}
}
private static class CustomSvgIcon extends IconPack.Icon {
private final File _file;
protected CustomSvgIcon(File file) {
super(file.getAbsolutePath(), null, null);
_file = file;
}
@Nullable
public File getFile() {
return _file;
}
@Override
public IconType getIconType() {
return IconType.SVG;
}
}
}
@@ -5,17 +5,18 @@ import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
import java.text.Collator;
import java.util.ArrayList;
import java.util.TreeSet;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
private GroupAdapter _adapter;
private TreeSet<String> _groups;
@@ -26,6 +27,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_groups);
setSupportActionBar(findViewById(R.id.toolbar));
Intent intent = getIntent();
_groups = new TreeSet<>(Collator.getInstance());
@@ -0,0 +1,271 @@
package com.beemdevelopment.aegis.ui;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.topjohnwu.superuser.Shell;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class ImportEntriesActivity extends AegisActivity {
private Menu _menu;
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_import_entries);
setSupportActionBar(findViewById(R.id.toolbar));
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
_adapter = new ImportEntriesAdapter();
RecyclerView entriesView = findViewById(R.id.list_entries);
entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
_fabScrollHelper.onScroll(dx, dy);
}
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
entriesView.setLayoutManager(layoutManager);
entriesView.setAdapter(_adapter);
entriesView.setNestedScrollingEnabled(false);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> {
if (getApp().getVaultManager().getEntries().size() > 0
&& _menu.findItem(R.id.toggle_wipe_vault).isChecked()) {
showWipeEntriesDialog();
} else {
saveAndFinish(false);
}
});
_fabScrollHelper = new FabScrollHelper(fab);
Class<? extends DatabaseImporter> importerType = (Class<? extends DatabaseImporter>) getIntent().getSerializableExtra("importerType");
startImport(importerType, (File) getIntent().getSerializableExtra("file"));
}
private void startImport(@NonNull Class<? extends DatabaseImporter> importerType, @Nullable File file) {
if (file == null) {
startImportApp(importerType);
} else {
startImportFile(importerType, file);
}
}
private void startImportFile(@NonNull Class<? extends DatabaseImporter> importerType, @NonNull File file) {
try (InputStream stream = new FileInputStream(file)) {
DatabaseImporter importer = DatabaseImporter.create(this, importerType);
DatabaseImporter.State state = importer.read(stream);
processImporterState(state);
} catch (FileNotFoundException e) {
Toast.makeText(this, R.string.file_not_found, Toast.LENGTH_SHORT).show();
} catch (DatabaseImporterException | IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.reading_file_error, e, (dialog, which) -> finish());
}
}
private void startImportApp(@NonNull Class<? extends DatabaseImporter> importerType) {
DatabaseImporter importer = DatabaseImporter.create(this, importerType);
// obtain the global root shell and close it immediately after we're done
// TODO: find a way to use SuFileInputStream with Shell.newInstance()
try (Shell shell = Shell.getShell()) {
if (!shell.isRoot()) {
Toast.makeText(this, R.string.root_error, Toast.LENGTH_SHORT).show();
finish();
return;
}
DatabaseImporter.State state = importer.readFromApp();
processImporterState(state);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
Toast.makeText(this, R.string.app_lookup_error, Toast.LENGTH_SHORT).show();
finish();
} catch (IOException | DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.reading_file_error, e, (dialog, which) -> finish());
}
}
private void processImporterState(DatabaseImporter.State state) {
try {
if (state.isEncrypted()) {
state.decrypt(this, new DatabaseImporter.DecryptListener() {
@Override
public void onStateDecrypted(DatabaseImporter.State state) {
importDatabase(state);
}
@Override
public void onError(Exception e) {
e.printStackTrace();
Dialogs.showErrorDialog(ImportEntriesActivity.this, R.string.decryption_error, e, (dialog, which) -> finish());
}
@Override
public void onCanceled() {
finish();
}
});
} else {
importDatabase(state);
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.parsing_file_error, e, (dialog, which) -> finish());
}
}
private void importDatabase(DatabaseImporter.State state) {
DatabaseImporter.Result result;
try {
result = state.convert();
} catch (DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.parsing_file_error, e, (dialog, which) -> finish());
return;
}
UUIDMap<VaultEntry> entries = result.getEntries();
for (VaultEntry entry : entries.getValues()) {
_adapter.addEntry(new ImportEntry(entry));
}
List<DatabaseImporterEntryException> errors = result.getErrors();
if (errors.size() > 0) {
showErrorDialog(errors);
}
}
private void showErrorDialog(List<DatabaseImporterEntryException> errors) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.import_error_title)
.setMessage(getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size()))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(getString(R.string.details), (dialog, which) -> showDetailedErrorDialog(errors))
.create());
}
private void showDetailedErrorDialog(List<DatabaseImporterEntryException> errors) {
List<String> messages = new ArrayList<>();
for (DatabaseImporterEntryException e : errors) {
messages.add(e.getMessage());
}
String message = TextUtils.join("\n\n", messages);
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.import_error_title)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(android.R.string.copy, (dialog2, which2) -> {
ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", message);
clipboard.setPrimaryClip(clip);
Toast.makeText(this, R.string.errors_copied, Toast.LENGTH_SHORT).show();
})
.create());
}
private void showWipeEntriesDialog() {
Dialogs.showCheckboxDialog(this, R.string.dialog_wipe_entries_title,
R.string.dialog_wipe_entries_message,
R.string.dialog_wipe_entries_checkbox,
this::saveAndFinish
);
}
private void saveAndFinish(boolean wipeEntries) {
VaultManager vault = getApp().getVaultManager();
if (wipeEntries) {
vault.wipeEntries();
}
List<ImportEntry> selectedEntries = _adapter.getCheckedEntries();
for (ImportEntry selectedEntry : selectedEntries) {
VaultEntry entry = selectedEntry.getEntry();
// temporary: randomize the UUID of duplicate entries and add them anyway
if (vault.isEntryDuplicate(entry)) {
entry.resetUUID();
}
vault.addEntry(entry);
}
if (saveVault(true)) {
String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size());
Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show();
setResult(RESULT_OK, null);
finish();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
getMenuInflater().inflate(R.menu.menu_import_entries, _menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
case R.id.toggle_checkboxes:
_adapter.toggleCheckboxes();
break;
case R.id.toggle_wipe_vault:
item.setChecked(!item.isChecked());
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -1,9 +1,11 @@
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import android.view.inputmethod.InputMethodManager;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.intro.IntroBaseActivity;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
import com.beemdevelopment.aegis.ui.slides.DoneSlide;
@@ -44,6 +46,10 @@ public class IntroActivity extends IntroBaseActivity {
@Override
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
// hide the keyboard before every slide change
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(findViewById(android.R.id.content).getWindowToken(), 0);
if (oldSlide == SecurityPickerSlide.class
&& newSlide == SecuritySetupSlide.class
&& getState().getInt("cryptType", CRYPT_TYPE_INVALID) == CRYPT_TYPE_NONE) {
@@ -7,15 +7,12 @@ import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Toast;
@@ -32,14 +29,19 @@ import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.fragments.BackupsPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.PreferencesFragment;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.ChecksumException;
import com.google.zxing.FormatException;
@@ -55,8 +57,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
public class MainActivity extends AegisActivity implements EntryListView.Listener {
// activity request codes
@@ -75,7 +78,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private AegisApplication _app;
private VaultManager _vault;
private boolean _loaded;
private String _selectedGroup;
private boolean _searchSubmitted;
private boolean _isAuthenticating;
@@ -86,7 +88,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private Menu _menu;
private SearchView _searchView;
private FloatingActionsMenu _fabMenu;
private EntryListView _entryListView;
private LinearLayout _btnBackupError;
@@ -98,6 +99,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setSupportActionBar(findViewById(R.id.toolbar));
_app = (AegisApplication) getApplication();
_vault = _app.getVaultManager();
@@ -108,40 +110,46 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_isDoingIntro = savedInstanceState.getBoolean("isDoingIntro");
}
// set up the entry view
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
_entryListView.setListener(this);
_entryListView.setCodeGroupSize(getPreferences().getCodeGroupSize());
_entryListView.setShowAccountName(getPreferences().isAccountNameVisible());
_entryListView.setSearchAccountName(getPreferences().isSearchAccountNameEnabled());
_entryListView.setHighlightEntry(getPreferences().isEntryHighlightEnabled());
_entryListView.setTapToReveal(getPreferences().isTapToRevealEnabled());
_entryListView.setTapToRevealTime(getPreferences().getTapToRevealTime());
_entryListView.setSortCategory(getPreferences().getCurrentSortCategory(), false);
_entryListView.setViewMode(getPreferences().getCurrentViewMode());
_entryListView.setIsCopyOnTapEnabled(getPreferences().isCopyOnTapEnabled());
_entryListView.setPrefGroupFilter(getPreferences().getGroupFilter());
// set up the floating action button
_fabMenu = findViewById(R.id.fab);
findViewById(R.id.fab_enter).setOnClickListener(view -> {
_fabMenu.collapse();
startEditEntryActivity(CODE_ADD_ENTRY, null, true);
});
findViewById(R.id.fab_scan_image).setOnClickListener(view -> {
_fabMenu.collapse();
startScanImageActivity();
});
findViewById(R.id.fab_scan).setOnClickListener(view -> {
_fabMenu.collapse();
startScanActivity();
});
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> {
View view = getLayoutInflater().inflate(R.layout.dialog_add_entry, null);
BottomSheetDialog dialog = new BottomSheetDialog(this);
dialog.setContentView(view);
view.findViewById(R.id.fab_enter).setOnClickListener(v1 -> {
dialog.dismiss();
startEditEntryActivityForManual(CODE_ADD_ENTRY);
});
view.findViewById(R.id.fab_scan_image).setOnClickListener(v2 -> {
dialog.dismiss();
startScanImageActivity();
});
view.findViewById(R.id.fab_scan).setOnClickListener(v3 -> {
dialog.dismiss();
startScanActivity();
});
Dialogs.showSecureDialog(dialog);
});
_btnBackupError = findViewById(R.id.btn_backup_error);
_btnBackupError.setOnClickListener(view -> {
startPreferencesActivity("pref_backups");
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
});
_fabScrollHelper = new FabScrollHelper(_fabMenu);
_fabScrollHelper = new FabScrollHelper(fab);
_selectedEntries = new ArrayList<>();
}
@@ -159,22 +167,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// collapse the fab menu on touch
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (_fabMenu.isExpanded()) {
Rect rect = new Rect();
_fabMenu.getGlobalVisibleRect(rect);
if (!rect.contains((int) event.getRawX(), (int) event.getRawY())) {
_fabMenu.collapse();
}
}
protected void onPause() {
Map<UUID, Integer> usageMap = _entryListView.getUsageCounts();
if (usageMap != null) {
getPreferences().setUsageCount(usageMap);
}
return super.dispatchTouchEvent(event);
super.onPause();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
_isAuthenticating = false;
@@ -235,7 +237,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = getPreferences().isAccountNameVisible();
int codeGroupSize = getPreferences().getCodeGroupSize();
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
@@ -243,7 +244,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
boolean copyOnTap = getPreferences().isCopyOnTapEnabled();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setCodeGroupSize(codeGroupSize);
_entryListView.setSearchAccountName(searchAccountName);
_entryListView.setHighlightEntry(highlightEntry);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
@@ -254,21 +254,30 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void startEditEntryActivity(int requestCode, VaultEntry entry, boolean isNew) {
private void startEditEntryActivityForNew(int requestCode, VaultEntry entry) {
Intent intent = new Intent(this, EditEntryActivity.class);
if (isNew) {
intent.putExtra("newEntry", entry != null ? entry : VaultEntry.getDefault());
} else {
intent.putExtra("entryUUID", entry.getUUID());
}
intent.putExtra("selectedGroup", _selectedGroup);
intent.putExtra("newEntry", entry);
intent.putExtra("isManual", false);
startActivityForResult(intent, requestCode);
}
private void startEditEntryActivityForManual(int requestCode) {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("newEntry", VaultEntry.getDefault());
intent.putExtra("isManual", true);
startActivityForResult(intent, requestCode);
}
private void startEditEntryActivity(int requestCode, VaultEntry entry) {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("entryUUID", entry.getUUID());
startActivityForResult(intent, requestCode);
}
private void onScanResult(Intent data) {
List<VaultEntry> entries = (ArrayList<VaultEntry>) data.getSerializableExtra("entries");
if (entries.size() == 1) {
startEditEntryActivity(CODE_ADD_ENTRY, entries.get(0), true);
startEditEntryActivityForNew(CODE_ADD_ENTRY, entries.get(0));
} else {
for (VaultEntry entry : entries) {
_vault.addEntry(entry);
@@ -303,7 +312,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void onScanImageResult(Intent intent) {
Uri inputFile = (intent.getData());
decodeQrCodeImage(intent.getData());
}
private void decodeQrCodeImage(Uri inputFile) {
Bitmap bitmap;
try {
@@ -311,7 +323,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
bitmap = BitmapHelper.resize(bitmap, 640, 480);
bitmap = BitmapHelper.resize(bitmap, QrCodeAnalyzer.RESOLUTION.getWidth(), QrCodeAnalyzer.RESOLUTION.getHeight());
}
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
@@ -326,55 +338,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
VaultEntry entry = new VaultEntry(info);
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
}
}
private void updateGroupFilterMenu() {
SubMenu menu = _menu.findItem(R.id.action_filter).getSubMenu();
for (int i = menu.size() - 1; i >= 0; i--) {
MenuItem item = menu.getItem(i);
if (item.getItemId() == R.id.menu_filter_all) {
continue;
}
menu.removeItem(item.getItemId());
}
// if the group no longer exists, switch back to 'All'
TreeSet<String> groups = _vault.getGroups();
if (_selectedGroup != null && !groups.contains(_selectedGroup)) {
menu.findItem(R.id.menu_filter_all).setChecked(true);
setGroupFilter(null);
}
for (String group : groups) {
MenuItem item = menu.add(R.id.action_filter_group, Menu.NONE, Menu.NONE, group);
if (group.equals(_selectedGroup)) {
item.setChecked(true);
}
}
if (groups.size() > 0) {
menu.add(R.id.action_filter_group, Menu.NONE, 10, R.string.filter_ungrouped);
}
menu.setGroupCheckable(R.id.action_filter_group, true, true);
}
private void updateSortCategoryMenu() {
SortCategory category = getPreferences().getCurrentSortCategory();
_menu.findItem(category.getMenuItem()).setChecked(true);
}
private void setGroupFilter(String group) {
getSupportActionBar().setSubtitle(group);
_selectedGroup = group;
_entryListView.setGroupFilter(group, true);
}
private void onDoIntroResult() {
_vault = _app.getVaultManager();
loadEntries();
@@ -415,11 +390,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
startActivityForResult(chooserIntent, CODE_SCAN_IMAGE);
AegisActivity.Helper.startExtActivityForResult(this, chooserIntent, CODE_SCAN_IMAGE);
}
private void startPreferencesActivity(String preference) {
private void startPreferencesActivity() {
startPreferencesActivity(null, null);
}
private void startPreferencesActivity(Class<? extends PreferencesFragment> fragmentType, String preference) {
Intent intent = new Intent(this, PreferencesActivity.class);
intent.putExtra("fragment", fragmentType);
intent.putExtra("pref", preference);
startActivityForResult(intent, CODE_PREFERENCES);
}
@@ -461,11 +441,28 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
if (info != null) {
VaultEntry entry = new VaultEntry(info);
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
}
}
}
private void handleSharedImage() {
if (_app.isVaultLocked()) {
return;
}
Intent intent = getIntent();
String action = intent.getAction();
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (Intent.ACTION_SEND.equals(action) && uri != null) {
intent.setAction(null);
intent.removeExtra(Intent.EXTRA_STREAM);
decodeQrCodeImage(uri);
}
}
@Override
protected void onResume() {
super.onResume();
@@ -499,10 +496,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
if (_app.isVaultLocked()) {
startAuthActivity(false);
} else if (_loaded) {
// update the list of groups in the filter menu
if (_menu != null) {
updateGroupFilterMenu();
}
// update the list of groups in the entry list view so that the chip gets updated
_entryListView.setGroups(_vault.getGroups());
// update the usage counts in case they are edited outside of the entrylistview
_entryListView.setUsageCounts(getPreferences().getUsageCounts());
// refresh all codes to prevent showing old ones
_entryListView.refresh(false);
@@ -512,6 +510,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
handleDeeplink();
handleSharedImage();
updateLockIcon();
doShortcutActions();
updateBackupErrorBar();
@@ -524,8 +523,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.setSearchFilter(null);
collapseSearchView();
setTitle("Aegis");
setGroupFilter(_selectedGroup);
setTitle(R.string.app_name);
return;
}
@@ -552,7 +550,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
getMenuInflater().inflate(R.menu.menu_main, menu);
updateLockIcon();
if (_loaded) {
updateGroupFilterMenu();
_entryListView.setGroups(_vault.getGroups());
updateSortCategoryMenu();
}
@@ -593,7 +591,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings: {
startPreferencesActivity(null);
startPreferencesActivity();
return true;
}
case R.id.action_about: {
@@ -605,16 +603,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_app.lock(true);
return true;
default:
if (item.getGroupId() == R.id.action_filter_group) {
item.setChecked(true);
String group = null;
if (item.getItemId() != R.id.menu_filter_all) {
group = item.getTitle().toString();
}
setGroupFilter(group);
}
if (item.getGroupId() == R.id.action_sort_category) {
item.setChecked(true);
@@ -632,6 +620,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
case R.id.menu_sort_alphabetically_name_reverse:
sortCategory = SortCategory.ACCOUNT_REVERSED;
break;
case R.id.menu_sort_usage_count:
sortCategory = SortCategory.USAGE_COUNT;
break;
case R.id.menu_sort_custom:
default:
sortCategory = SortCategory.CUSTOM;
@@ -652,6 +643,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void loadEntries() {
if (!_loaded) {
_entryListView.setUsageCounts(getPreferences().getUsageCounts());
_entryListView.addEntries(_vault.getEntries());
_entryListView.runEntriesAnimation();
_loaded = true;
@@ -751,15 +743,24 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onListChange() { _fabScrollHelper.setVisible(true); }
@Override
public void onSaveGroupFilter(List<String> groupFilter) {
getPreferences().setGroupFilter(groupFilter);
}
@Override
public void onLocked(boolean userInitiated) {
if (_actionMode != null) {
_actionMode.finish();
}
if (!_searchView.isIconified()) {
collapseSearchView();
}
_entryListView.clearEntries();
_loaded = false;
if (userInitiated) {
startAuthActivity(true);
} else {
@@ -795,7 +796,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return true;
case R.id.action_edit:
startEditEntryActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0), false);
startEditEntryActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0));
mode.finish();
return true;
@@ -814,19 +815,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return true;
case R.id.action_delete:
Dialogs.showDeleteEntriesDialog(MainActivity.this, (d, which) -> {
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
deleteEntries(_selectedEntries);
for (VaultEntry entry : _selectedEntries) {
if (entry.getGroup() != null) {
if (!_vault.getGroups().contains(entry.getGroup())) {
updateGroupFilterMenu();
_entryListView.setGroups(_vault.getGroups());
break;
}
}
}
mode.finish();
}, _selectedEntries.size());
});
return true;
default:
return false;
@@ -1,15 +1,26 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
public class PreferencesActivity extends AegisActivity {
private PreferencesFragment _fragment;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.fragments.MainPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.PreferencesFragment;
public class PreferencesActivity extends AegisActivity implements
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
private Fragment _fragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_preferences);
setSupportActionBar(findViewById(R.id.toolbar));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -17,59 +28,97 @@ public class PreferencesActivity extends AegisActivity {
}
if (savedInstanceState == null) {
_fragment = new PreferencesFragment();
_fragment = new MainPreferencesFragment();
_fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit();
getSupportFragmentManager().beginTransaction()
.replace(R.id.content, _fragment)
.commit();
PreferencesFragment requestedFragment = getRequestedFragment();
if (requestedFragment != null) {
_fragment = requestedFragment;
showFragment(_fragment);
}
} else {
_fragment = (PreferencesFragment) getSupportFragmentManager().findFragmentById(android.R.id.content);
_fragment = getSupportFragmentManager().findFragmentById(R.id.content);
}
}
@Override
public void onResume() {
super.onResume();
Intent intent = getIntent();
String preference = intent.getStringExtra("pref");
if (preference != null) {
_fragment.scrollToPreference(preference);
intent.removeExtra("pref");
}
public void onBackPressed() {
super.onBackPressed();
setTitle(R.string.action_settings);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
// pass permission request results to the fragment
_fragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected void onRestoreInstanceState(final Bundle inState) {
// pass the stored result intent back to the fragment
if (inState.containsKey("result")) {
_fragment.setResult(inState.getParcelable("result"));
if (_fragment instanceof PreferencesFragment) {
// pass the stored result intent back to the fragment
if (inState.containsKey("result")) {
((PreferencesFragment) _fragment).setResult(inState.getParcelable("result"));
}
}
super.onRestoreInstanceState(inState);
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
// save the result intent of the fragment
// this is done so we don't lose anything if the fragment calls recreate on this activity
outState.putParcelable("result", _fragment.getResult());
if (_fragment instanceof PreferencesFragment) {
// save the result intent of the fragment
// this is done so we don't lose anything if the fragment calls recreate on this activity
outState.putParcelable("result", ((PreferencesFragment) _fragment).getResult());
}
super.onSaveInstanceState(outState);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == android.R.id.home) {
onBackPressed();
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
_fragment = getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment());
_fragment.setArguments(pref.getExtras());
_fragment.setTargetFragment(caller, 0);
showFragment(_fragment);
setTitle(pref.getTitle());
return true;
}
private void showFragment(Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right)
.replace(R.id.content, fragment)
.addToBackStack(null)
.commit();
}
@SuppressWarnings("unchecked")
private PreferencesFragment getRequestedFragment() {
Class<? extends PreferencesFragment> fragmentType = (Class<? extends PreferencesFragment>) getIntent().getSerializableExtra("fragment");
if (fragmentType == null) {
return null;
}
try {
return fragmentType.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -21,6 +21,7 @@ import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.zxing.Result;
@@ -28,6 +29,8 @@ import com.google.zxing.Result;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Listener {
private ProcessCameraProvider _cameraProvider;
@@ -37,7 +40,9 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
private int _currentLens;
private Menu _menu;
private ImageAnalysis _analysis;
private PreviewView _previewView;
private ExecutorService _executor;
private int _batchId = 0;
private int _batchIndex = -1;
@@ -47,10 +52,12 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scanner);
setSupportActionBar(findViewById(R.id.toolbar));
_entries = new ArrayList<>();
_lenses = new ArrayList<>();
_previewView = findViewById(R.id.preview_view);
_executor = Executors.newSingleThreadExecutor();
_cameraProviderFuture = ProcessCameraProvider.getInstance(this);
_cameraProviderFuture.addListener(() -> {
@@ -76,6 +83,12 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
}, ContextCompat.getMainExecutor(this));
}
@Override
protected void onDestroy() {
_executor.shutdownNow();
super.onDestroy();
}
@Override
protected void onSetTheme() {
setTheme(ThemeMap.FULLSCREEN);
@@ -91,7 +104,7 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_camera) {
_cameraProvider.unbindAll();
unbindPreview(_cameraProvider);
_currentLens = _currentLens == CameraSelector.LENS_FACING_BACK ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
bindPreview(_cameraProvider);
updateCameraIcon();
@@ -138,29 +151,38 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
.requireLensFacing(_currentLens)
.build();
ImageAnalysis analysis = new ImageAnalysis.Builder()
_analysis = new ImageAnalysis.Builder()
.setTargetResolution(QrCodeAnalyzer.RESOLUTION)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
analysis.setAnalyzer(ContextCompat.getMainExecutor(this), new QrCodeAnalyzer(this));
_analysis.setAnalyzer(_executor, new QrCodeAnalyzer(this));
cameraProvider.bindToLifecycle(this, selector, preview, analysis);
cameraProvider.bindToLifecycle(this, selector, preview, _analysis);
}
private void unbindPreview(@NonNull ProcessCameraProvider cameraProvider) {
_analysis = null;
cameraProvider.unbindAll();
}
@Override
public void onQrCodeDetected(Result result) {
try {
Uri uri = Uri.parse(result.getText().trim());
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
handleExportUri(uri);
} else {
handleUri(uri);
if (_analysis != null) {
try {
Uri uri = Uri.parse(result.getText().trim());
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
handleExportUri(uri);
} else {
handleUri(uri);
}
} catch (GoogleAuthInfoException e) {
e.printStackTrace();
unbindPreview(_cameraProvider);
Dialogs.showErrorDialog(this,
e.isPhoneFactor() ? R.string.read_qr_error_phonefactor : R.string.read_qr_error,
e, ((dialog, which) -> bindPreview(_cameraProvider)));
}
} catch (GoogleAuthInfoException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this,
e.isPhoneFactor() ? R.string.read_qr_error_phonefactor : R.string.read_qr_error,
e, ((dialog, which) -> bindPreview(_cameraProvider)));
_cameraProvider.unbindAll();
}
}
@@ -1,154 +0,0 @@
package com.beemdevelopment.aegis.ui;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.getbase.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class SelectEntriesActivity extends AegisActivity {
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
private boolean _vaultContainsEntries;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_select_entries);
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
_adapter = new ImportEntriesAdapter();
RecyclerView entriesView = findViewById(R.id.list_entries);
entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
onScroll(dx, dy);
}
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
entriesView.setLayoutManager(layoutManager);
entriesView.setAdapter(_adapter);
entriesView.setNestedScrollingEnabled(false);
Intent intent = getIntent();
List<ImportEntry> entries = (ArrayList<ImportEntry>) intent.getSerializableExtra("entries");
List<DatabaseImporterEntryException> errors = (ArrayList<DatabaseImporterEntryException>) intent.getSerializableExtra("errors");
_vaultContainsEntries = intent.getBooleanExtra("vaultContainsEntries", false);
for (ImportEntry entry : entries) {
_adapter.addEntry(entry);
}
if (errors.size() > 0) {
showErrorDialog(errors);
}
FloatingActionButton fabMenu = findViewById(R.id.fab);
fabMenu.setOnClickListener(v -> {
if (_vaultContainsEntries) {
showWipeEntriesDialog();
} else {
returnSelectedEntries(false);
}
});
_fabScrollHelper = new FabScrollHelper(fabMenu);
}
private void showErrorDialog(List<DatabaseImporterEntryException> errors) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.import_error_title)
.setMessage(getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size()))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(getString(R.string.details), (dialog, which) -> showDetailedErrorDialog(errors))
.create());
}
private void showDetailedErrorDialog(List<DatabaseImporterEntryException> errors) {
List<String> messages = new ArrayList<>();
for (DatabaseImporterEntryException e : errors) {
messages.add(e.getMessage());
}
String message = TextUtils.join("\n\n", messages);
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.import_error_title)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(android.R.string.copy, (dialog2, which2) -> {
ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", message);
clipboard.setPrimaryClip(clip);
Toast.makeText(this, R.string.errors_copied, Toast.LENGTH_SHORT).show();
})
.create());
}
private void showWipeEntriesDialog() {
Dialogs.showCheckboxDialog(this, R.string.dialog_wipe_entries_title,
R.string.dialog_wipe_entries_message,
R.string.dialog_wipe_entries_checkbox,
this::returnSelectedEntries
);
}
private void returnSelectedEntries(boolean wipeEntries) {
Intent intent = new Intent();
List<ImportEntry> entries = _adapter.getCheckedEntries();
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
intent.putExtra("wipeEntries", wipeEntries);
setResult(RESULT_OK, intent);
finish();
}
public void onScroll(int dx, int dy) {
_fabScrollHelper.onScroll(dx, dy);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_select_entries, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
case R.id.toggle_checkboxes:
_adapter.toggleCheckboxes();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@@ -39,6 +40,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_slots);
setSupportActionBar(findViewById(R.id.toolbar));
_edited = false;
ActionBar bar = getSupportActionBar();
@@ -4,14 +4,19 @@ import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
@@ -37,6 +42,7 @@ public class TransferEntriesActivity extends AegisActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_share_entry);
setSupportActionBar(findViewById(R.id.toolbar));
_vault = getApp().getVaultManager();
_qrImage = findViewById(R.id.ivQrCode);
@@ -107,7 +113,7 @@ public class TransferEntriesActivity extends AegisActivity {
_entriesCount.setText(getResources().getQuantityString(R.plurals.entries_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
QRCodeWriter writer = new QRCodeWriter();
BitMatrix bitMatrix = null;
BitMatrix bitMatrix;
try {
bitMatrix = writer.encode(selectedEntry.getUri().toString(), BarcodeFormat.QR_CODE, 512, 512);
} catch (WriterException e) {
@@ -115,13 +121,20 @@ public class TransferEntriesActivity extends AegisActivity {
return;
}
@ColorInt int backgroundColor = Color.WHITE;
if (getConfiguredTheme() == Theme.LIGHT) {
TypedValue typedValue = new TypedValue();
getTheme().resolveAttribute(R.attr.background, typedValue, true);
backgroundColor = typedValue.data;
}
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;
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : backgroundColor;
}
}
@@ -0,0 +1,21 @@
package com.beemdevelopment.aegis.ui.dialogs;
import android.content.Context;
import com.beemdevelopment.aegis.R;
public class ChangelogDialog extends SimpleWebViewDialog {
private ChangelogDialog() {
super(R.string.changelog);
}
public static ChangelogDialog create() {
return new ChangelogDialog();
}
@Override
protected String getContent(Context context) {
String content = readAssetAsString(context, "changelog.html");
return String.format(content, getBackgroundColor(), getTextColor());
}
}
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.ui;
package com.beemdevelopment.aegis.ui.dialogs;
import android.app.Activity;
import android.app.Dialog;
@@ -35,15 +35,18 @@ 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.VaultEntry;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.google.android.material.textfield.TextInputEditText;
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 java.util.stream.Collectors;
import javax.crypto.Cipher;
@@ -63,28 +66,31 @@ public class Dialogs {
dialog.show();
}
public static void showDeleteEntryDialog(Activity activity, DialogInterface.OnClickListener onDelete) {
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.delete_entry))
.setMessage(activity.getString(R.string.delete_entry_description))
.setPositiveButton(android.R.string.yes, onDelete)
.setNegativeButton(android.R.string.no, null)
.create());
}
public static void showDeleteEntriesDialog(Activity activity, List<VaultEntry> services, DialogInterface.OnClickListener onDelete) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_delete_entry, null);
TextView textMessage = view.findViewById(R.id.text_message);
TextView textExplanation = view.findViewById(R.id.text_explanation);
String entries = services.stream()
.map(entry -> !entry.getIssuer().isEmpty() ? entry.getIssuer()
: !entry.getName().isEmpty() ? entry.getName()
: activity.getString(R.string.unknown_issuer)
)
.collect(Collectors.joining(", "));
textExplanation.setText(activity.getString(R.string.delete_entry_explanation, entries));
public static void showDeleteEntriesDialog(Activity activity, DialogInterface.OnClickListener onDelete, int totalEntries) {
String title, message;
if (totalEntries > 1) {
if (services.size() > 1) {
title = activity.getString(R.string.delete_entries);
message = activity.getResources().getQuantityString(R.plurals.delete_entries_description, totalEntries, totalEntries);
message = activity.getResources().getQuantityString(R.plurals.delete_entries_description, services.size(), services.size());
} else {
title = activity.getString(R.string.delete_entry);
message = activity.getString(R.string.delete_entry_description);
}
textMessage.setText(message);
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(title)
.setMessage(message)
.setView(view)
.setPositiveButton(android.R.string.yes, onDelete)
.setNegativeButton(android.R.string.no, null)
.create());
@@ -188,13 +194,14 @@ public class Dialogs {
showSecureDialog(dialog);
}
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener, boolean isSecret) {
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener, boolean isSecret) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
EditText input = view.findViewById(R.id.text_input);
TextInputEditText input = view.findViewById(R.id.text_input);
TextInputLayout inputLayout = view.findViewById(R.id.text_input_layout);
if (isSecret) {
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
input.setHint(hintId);
inputLayout.setHint(hintId);
AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(titleId)
@@ -204,8 +211,8 @@ public class Dialogs {
listener.onTextInputResult(text);
});
if (dismissListener != null) {
builder.setOnDismissListener(dismissListener);
if (cancelListener != null) {
builder.setOnCancelListener(cancelListener);
}
if (messageId != 0) {
@@ -213,6 +220,7 @@ public class Dialogs {
}
AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(true);
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
showSecureDialog(dialog);
}
@@ -229,12 +237,20 @@ public class Dialogs {
showTextInputDialog(context, R.string.set_password, R.string.password, listener, true);
}
public static void showPasswordInputDialog(Context context, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
showTextInputDialog(context, R.string.set_password, 0, R.string.password, listener, cancelListener, true);
}
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, null, true);
}
public static void showPasswordInputDialog(Context context, @StringRes int setPasswordMessageId, @StringRes int messageId, TextInputListener listener, DialogInterface.OnDismissListener dismissListener) {
showTextInputDialog(context, setPasswordMessageId, messageId, R.string.password, listener, dismissListener, true);
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, cancelListener, true);
}
public static void showPasswordInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
showTextInputDialog(context, titleId, messageId, R.string.password, listener, cancelListener, true);
}
public static void showCheckboxDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int checkboxMessageId, CheckboxInputListener listener) {
@@ -333,6 +349,7 @@ public class Dialogs {
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.error_occurred)
.setView(view)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
if (listener != null) {
listener.onClick(dialog1, which);
@@ -389,14 +406,18 @@ public class Dialogs {
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);
List<String> names = importers.stream().map(DatabaseImporter.Definition::getName).collect(Collectors.toList());
int i = 0;
if (!isDirect) {
i = names.indexOf(context.getString(R.string.app_name));
}
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);
setImporterHelpText(helpText, importers.get(i), isDirect);
ListView listView = view.findViewById(R.id.list_importers);
listView.setAdapter(new ArrayAdapter<>(context, R.layout.card_importer, names));
listView.setItemChecked(0, true);
listView.setItemChecked(i, true);
listView.setOnItemClickListener((parent, view1, position, id) -> {
setImporterHelpText(helpText, importers.get(position), isDirect);
});
@@ -0,0 +1,162 @@
package com.beemdevelopment.aegis.ui.dialogs;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.PopupMenu;
import androidx.recyclerview.widget.GridLayoutManager;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.ui.views.IconRecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.textfield.TextInputEditText;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class IconPickerDialog {
private IconPickerDialog() {
}
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, IconAdapter.Listener listener) {
View view = LayoutInflater.from(activity).inflate(R.layout.dialog_icon_picker, null);
TextView textIconPack = view.findViewById(R.id.text_icon_pack);
BottomSheetDialog dialog = new BottomSheetDialog(activity);
dialog.setContentView(view);
dialog.create();
FrameLayout rootView = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet);
rootView.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
IconAdapter adapter = new IconAdapter(dialog.getContext(), issuer, new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
dialog.dismiss();
listener.onIconSelected(icon);
}
@Override
public void onCustomSelected() {
dialog.dismiss();
listener.onCustomSelected();
}
});
class IconPreloadProvider implements ListPreloader.PreloadModelProvider<IconPack.Icon> {
@NonNull
@Override
public List<IconPack.Icon> getPreloadItems(int position) {
IconPack.Icon icon = adapter.getIconAt(position);
return Collections.singletonList(icon);
}
@Nullable
@Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
return Glide.with(dialog.getContext())
.asDrawable()
.load(icon.getFile())
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
}
}
TextInputEditText iconSearch = view.findViewById(R.id.text_search_icon);
iconSearch.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(rootView);
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
iconSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String query = s.toString();
adapter.setQuery(query.isEmpty() ? null : query);
}
@Override
public void afterTextChanged(Editable s) {
}
});
ViewPreloadSizeProvider<IconPack.Icon> preloadSizeProvider = new ViewPreloadSizeProvider<>();
IconPreloadProvider modelProvider = new IconPreloadProvider();
RecyclerViewPreloader<IconPack.Icon> preloader = new RecyclerViewPreloader<>(activity, modelProvider, preloadSizeProvider, 10);
IconRecyclerView recyclerView = view.findViewById(R.id.list_icons);
GridLayoutManager layoutManager = recyclerView.getGridLayoutManager();
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (adapter.getItemViewType(position) == R.layout.card_icon) {
return 1;
}
return recyclerView.getSpanCount();
}
});
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
recyclerView.addOnScrollListener(preloader);
adapter.loadIcons(iconPacks.get(0));
textIconPack.setText(iconPacks.get(0).getName());
view.findViewById(R.id.btn_icon_pack).setOnClickListener(v -> {
List<String> iconPackNames = iconPacks.stream()
.map(IconPack::getName)
.collect(Collectors.toList());
PopupMenu popupMenu = new PopupMenu(activity, v);
popupMenu.setOnMenuItemClickListener(item -> {
IconPack pack = iconPacks.get(iconPackNames.indexOf(item.getTitle().toString()));
adapter.loadIcons(pack);
String query = iconSearch.getText().toString();
if (!query.isEmpty()) {
adapter.setQuery(query);
}
textIconPack.setText(pack.getName());
return true;
});
Menu menu = popupMenu.getMenu();
for (String name : iconPackNames) {
menu.add(name);
}
popupMenu.show();
});
return dialog;
}
}
@@ -0,0 +1,22 @@
package com.beemdevelopment.aegis.ui.dialogs;
import android.content.Context;
import com.beemdevelopment.aegis.R;
public class LicenseDialog extends SimpleWebViewDialog {
private LicenseDialog() {
super(R.string.license);
}
public static LicenseDialog create() {
return new LicenseDialog();
}
@Override
protected String getContent(Context context) {
String license = readAssetAsString(context, "LICENSE");
String html = readAssetAsString(context, "license.html");
return String.format(html, license, getBackgroundColor(), getTextColor());
}
}
@@ -0,0 +1,93 @@
package com.beemdevelopment.aegis.ui.dialogs;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public abstract class SimpleWebViewDialog extends DialogFragment {
private Theme _theme;
private final @StringRes int _title;
protected SimpleWebViewDialog(@StringRes int title) {
_title = title;
}
protected abstract String getContent(Context context);
@SuppressLint("InflateParams")
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view;
try {
view = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_web_view, null);
} catch (InflateException e) {
e.printStackTrace();
return new AlertDialog.Builder(getActivity())
.setTitle(android.R.string.dialog_alert_title)
.setMessage(getString(R.string.webview_error))
.setPositiveButton(android.R.string.ok, null)
.show();
}
AlertDialog dialog = new AlertDialog.Builder(getActivity())
.setTitle(_title)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.show();
String content = getContent(getContext());
final WebView webView = view.findViewById(R.id.web_view);
webView.loadData(content, "text/html", "UTF-8");
return dialog;
}
public SimpleWebViewDialog setTheme(Theme theme) {
_theme = theme;
return this;
}
protected String getBackgroundColor() {
int backgroundColorResource = _theme == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
return colorToCSS(ThemeHelper.getThemeColor(backgroundColorResource, getContext().getTheme()));
}
protected String getTextColor() {
return colorToCSS(0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getContext().getTheme()));
}
@SuppressLint("DefaultLocale")
private static String colorToCSS(int color) {
return String.format("rgb(%d, %d, %d)", Color.red(color), Color.green(color), Color.blue(color));
}
protected static String readAssetAsString(Context context, String name) {
try (InputStream inStream = context.getAssets().open(name);
InputStreamReader reader = new InputStreamReader(inStream, StandardCharsets.UTF_8)) {
return CharStreams.toString(reader);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,144 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.GroupManagerActivity;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.ArrayList;
import java.util.HashSet;
public class AppearancePreferencesFragment extends PreferencesFragment {
private Preference _groupsPreference;
private Preference _resetUsageCountPreference;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_appearance);
Preferences prefs = getPreferences();
_groupsPreference = findPreference("pref_groups");
_groupsPreference.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(getActivity(), GroupManagerActivity.class);
intent.putExtra("groups", new ArrayList<>(getVault().getGroups()));
startActivityForResult(intent, CODE_GROUPS);
return true;
});
_resetUsageCountPreference = findPreference("pref_reset_usage_count");
_resetUsageCountPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.preference_reset_usage_count)
.setMessage(R.string.preference_reset_usage_count_dialog)
.setPositiveButton(android.R.string.yes, (dialog, which) -> getPreferences().clearUsageCount())
.setNegativeButton(android.R.string.no, null)
.create());
return true;
});
int currentTheme = prefs.getCurrentTheme().ordinal();
Preference darkModePreference = findPreference("pref_dark_mode");
darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme]));
darkModePreference.setOnPreferenceClickListener(preference -> {
int currentTheme1 = prefs.getCurrentTheme().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_theme)
.setSingleChoiceItems(R.array.theme_titles, currentTheme1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
prefs.setCurrentTheme(Theme.fromInteger(i));
dialog.dismiss();
getResult().putExtra("needsRecreate", true);
getActivity().recreate();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
});
Preference langPreference = findPreference("pref_lang");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRecreate", true);
getActivity().recreate();
return true;
});
} else {
// Setting locale doesn't work on Marshmallow or below
langPreference.setVisible(false);
}
int currentViewMode = prefs.getCurrentViewMode().ordinal();
Preference viewModePreference = findPreference("pref_view_mode");
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode]));
viewModePreference.setOnPreferenceClickListener(preference -> {
int currentViewMode1 = prefs.getCurrentViewMode().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_view_mode)
.setSingleChoiceItems(R.array.view_mode_titles, currentViewMode1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
prefs.setCurrentViewMode(ViewMode.fromInteger(i));
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i]));
getResult().putExtra("needsRefresh", true);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
});
Preference codeDigitGroupingPreference = findPreference("pref_code_group_size");
codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
Preference issuerPreference = findPreference("pref_account_name");
issuerPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data != null && requestCode == CODE_GROUPS) {
onGroupManagerResult(resultCode, data);
}
}
private void onGroupManagerResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
HashSet<String> groups = new HashSet<>(data.getStringArrayListExtra("groups"));
for (VaultEntry entry : getVault().getEntries()) {
if (!groups.contains(entry.getGroup())) {
entry.setGroup(null);
}
}
saveVault();
}
}
@@ -0,0 +1,139 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.AegisActivity;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultManagerException;
public class BackupsPreferencesFragment extends PreferencesFragment {
private SwitchPreferenceCompat _androidBackupsPreference;
private SwitchPreferenceCompat _backupsPreference;
private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference;
@Override
public void onResume() {
super.onResume();
updateBackupPreference();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_backups);
Preferences prefs = getPreferences();
_backupsPreference = findPreference("pref_backups");
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if ((boolean) newValue) {
selectBackupsLocation();
} else {
prefs.setIsBackupsEnabled(false);
updateBackupPreference();
}
return false;
});
_androidBackupsPreference = findPreference("pref_android_backups");
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
prefs.setIsAndroidBackupsEnabled((boolean) newValue);
updateBackupPreference();
getVault().androidBackupDataChanged();
return false;
});
Uri backupLocation = prefs.getBackupsLocation();
_backupsLocationPreference = findPreference("pref_backups_location");
if (backupLocation != null) {
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
}
_backupsLocationPreference.setOnPreferenceClickListener(preference -> {
selectBackupsLocation();
return false;
});
_backupsTriggerPreference = findPreference("pref_backups_trigger");
_backupsTriggerPreference.setOnPreferenceClickListener(preference -> {
if (prefs.isBackupsEnabled()) {
try {
getVault().backup();
Toast.makeText(getActivity(), R.string.backup_successful, Toast.LENGTH_LONG).show();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.backup_error, e);
}
}
return true;
});
_backupsVersionsPreference = findPreference("pref_backups_versions");
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, prefs.getBackupsVersionCount(), prefs.getBackupsVersionCount()));
_backupsVersionsPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> {
number = number * 5 + 5;
prefs.setBackupsVersionCount(number);
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, prefs.getBackupsVersionCount(), prefs.getBackupsVersionCount()));
});
return false;
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data != null && requestCode == CODE_BACKUPS) {
onSelectBackupsLocationResult(resultCode, data);
}
}
private void onSelectBackupsLocationResult(int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContext().getContentResolver().takePersistableUriPermission(data.getData(), flags);
Preferences prefs = getPreferences();
prefs.setBackupsLocation(uri);
prefs.setIsBackupsEnabled(true);
prefs.setBackupsError(null);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference();
}
private void updateBackupPreference() {
boolean encrypted = getVault().isEncryptionEnabled();
boolean androidBackupEnabled = getPreferences().isAndroidBackupsEnabled() && encrypted;
boolean backupEnabled = getPreferences().isBackupsEnabled() && encrypted;
_androidBackupsPreference.setChecked(androidBackupEnabled);
_androidBackupsPreference.setEnabled(encrypted);
_backupsPreference.setChecked(backupEnabled);
_backupsPreference.setEnabled(encrypted);
_backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled);
}
private void selectBackupsLocation() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
AegisActivity.Helper.startExtActivityForResult(this, intent, CODE_BACKUPS);
}
}
@@ -0,0 +1,26 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.os.Bundle;
import androidx.preference.Preference;
import com.beemdevelopment.aegis.R;
public class BehaviorPreferencesFragment extends PreferencesFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_behavior);
Preference copyOnTapPreference = findPreference("pref_copy_on_tap");
copyOnTapPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
}
}
@@ -0,0 +1,160 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconPackException;
import com.beemdevelopment.aegis.icons.IconPackExistsException;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.ui.AegisActivity;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ImportIconPackTask;
import com.beemdevelopment.aegis.ui.views.IconPackAdapter;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
public class IconPacksManagerFragment extends Fragment implements IconPackAdapter.Listener {
private static final int CODE_IMPORT = 0;
private IconPackAdapter _adapter;
private IconPackManager _iconPackManager;
private View _iconPacksView;
private RecyclerView _iconPacksRecyclerView;
private LinearLayout _noIconPacksView;
private FabScrollHelper _fabScrollHelper;
public IconPacksManagerFragment() {
super(R.layout.fragment_icon_packs);
}
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
_iconPackManager = ((AegisApplication) getContext().getApplicationContext()).getIconPackManager();
FloatingActionButton fab = view.findViewById(R.id.fab);
fab.setOnClickListener(v -> startImportIconPack());
_fabScrollHelper = new FabScrollHelper(fab);
_noIconPacksView = view.findViewById(R.id.vEmptyList);
((TextView) view.findViewById(R.id.txt_no_icon_packs)).setMovementMethod(LinkMovementMethod.getInstance());
_iconPacksView = view.findViewById(R.id.view_icon_packs);
_adapter = new IconPackAdapter(this);
_iconPacksRecyclerView = view.findViewById(R.id.list_icon_packs);
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
_iconPacksRecyclerView.setLayoutManager(layoutManager);
_iconPacksRecyclerView.setAdapter(_adapter);
_iconPacksRecyclerView.setNestedScrollingEnabled(false);
_iconPacksRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
_fabScrollHelper.onScroll(dx, dy);
}
});
for (IconPack pack : _iconPackManager.getIconPacks()) {
_adapter.addIconPack(pack);
}
updateEmptyState();
}
@Override
public void onRemoveIconPack(IconPack pack) {
Dialogs.showSecureDialog(new AlertDialog.Builder(getContext())
.setTitle(R.string.remove_icon_pack)
.setMessage(R.string.remove_icon_pack_description)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
try {
_iconPackManager.removeIconPack(pack);
} catch (IconPackException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_delete_error, e);
return;
}
_adapter.removeIconPack(pack);
updateEmptyState();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CODE_IMPORT && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
importIconPack(data.getData());
}
}
private void importIconPack(Uri uri) {
ImportIconPackTask task = new ImportIconPackTask(getContext(), result -> {
Exception e = result.getException();
if (e instanceof IconPackExistsException) {
Dialogs.showSecureDialog(new AlertDialog.Builder(getContext())
.setTitle(R.string.error_occurred)
.setMessage(R.string.icon_pack_import_exists_error)
.setPositiveButton(R.string.yes, (dialog, which) -> {
if (removeIconPack(((IconPackExistsException) e).getIconPack())) {
importIconPack(uri);
}
})
.setNegativeButton(R.string.no, null)
.create());
} else if (e != null) {
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_import_error, e);
} else {
_adapter.addIconPack(result.getIconPack());
updateEmptyState();
}
});
task.execute(getLifecycle(), new ImportIconPackTask.Params(_iconPackManager, uri));
}
private boolean removeIconPack(IconPack pack) {
try {
_iconPackManager.removeIconPack(pack);
} catch (IconPackException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_delete_error, e);
return false;
}
_adapter.removeIconPack(pack);
updateEmptyState();
return true;
}
private void startImportIconPack() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("application/zip");
AegisActivity.Helper.startExtActivityForResult(this, intent, CODE_IMPORT);
}
private void updateEmptyState() {
if (_adapter.getItemCount() > 0) {
_iconPacksView.setVisibility(View.VISIBLE);
_noIconPacksView.setVisibility(View.GONE);
} else {
_iconPacksView.setVisibility(View.GONE);
_noIconPacksView.setVisibility(View.VISIBLE);
}
}
}
@@ -0,0 +1,359 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
import androidx.preference.Preference;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.ui.AegisActivity;
import com.beemdevelopment.aegis.ui.ImportEntriesActivity;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ExportTask;
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import javax.crypto.Cipher;
public class ImportExportPreferencesFragment extends PreferencesFragment {
// keep a reference to the type of database converter that was selected
private Class<? extends DatabaseImporter> _importerType;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_import_export);
if (savedInstanceState != null) {
_importerType = (Class<? extends DatabaseImporter>) savedInstanceState.getSerializable("importerType");
}
Preference importPreference = findPreference("pref_import");
importPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showImportersDialog(getContext(), false, definition -> {
_importerType = definition.getType();
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
AegisActivity.Helper.startExtActivityForResult(this, intent, CODE_IMPORT_SELECT);
});
return true;
});
Preference importAppPreference = findPreference("pref_import_app");
importAppPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showImportersDialog(getContext(), true, definition -> {
startImportEntriesActivity(definition.getType(), null);
});
return true;
});
Preference exportPreference = findPreference("pref_export");
exportPreference.setOnPreferenceClickListener(preference -> {
startExport();
return true;
});
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("importerType", _importerType);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CODE_IMPORT) {
getResult().putExtra("needsRecreate", true);
} else if (data != null) {
switch (requestCode) {
case CODE_IMPORT_SELECT:
onImportSelectResult(resultCode, data);
break;
case CODE_EXPORT:
// intentional fallthrough
case CODE_EXPORT_PLAIN:
// intentional fallthrough
case CODE_EXPORT_GOOGLE_URI:
onExportResult(requestCode, resultCode, data);
break;
}
}
}
private void onImportSelectResult(int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
ImportFileTask.Params params = new ImportFileTask.Params(uri, "import", null);
ImportFileTask task = new ImportFileTask(getContext(), result -> {
if (result.getException() == null) {
startImportEntriesActivity(_importerType, result.getFile());
} else {
Dialogs.showErrorDialog(getContext(), R.string.reading_file_error, result.getException());
}
});
task.execute(getLifecycle(), params);
}
private void startImportEntriesActivity(Class<? extends DatabaseImporter> importerType, File file) {
Intent intent = new Intent(getActivity(), ImportEntriesActivity.class);
intent.putExtra("importerType", importerType);
intent.putExtra("file", file);
startActivityForResult(intent, CODE_IMPORT);
}
private void startExport() {
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_export, null);
TextView warningText = view.findViewById(R.id.text_export_warning);
CheckBox checkBoxEncrypt = view.findViewById(R.id.checkbox_export_encrypt);
CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept);
AutoCompleteTextView dropdown = view.findViewById(R.id.dropdown_export_format);
DropdownHelper.fillDropdown(getContext(), dropdown, R.array.export_formats);
dropdown.setText(getString(R.string.export_format_aegis), false);
dropdown.setOnItemClickListener((parent, view1, position, id) -> {
checkBoxEncrypt.setChecked(position == 0);
checkBoxEncrypt.setEnabled(position == 0);
warningText.setVisibility(checkBoxEncrypt.isChecked() ? View.GONE : View.VISIBLE);
});
AlertDialog dialog = new AlertDialog.Builder(getContext())
.setTitle(R.string.pref_export_summary)
.setView(view)
.setNeutralButton(R.string.share, null)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setOnShowListener(d -> {
Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
Button btnNeutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
checkBoxEncrypt.setOnCheckedChangeListener((buttonView, isChecked) -> {
warningText.setVisibility(isChecked ? View.GONE : View.VISIBLE);
checkBoxAccept.setVisibility(isChecked ? View.GONE : View.VISIBLE);
checkBoxAccept.setChecked(false);
btnPos.setEnabled(isChecked);
btnNeutral.setEnabled(isChecked);
});
checkBoxAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
btnPos.setEnabled(isChecked);
btnNeutral.setEnabled(isChecked);
});
btnPos.setOnClickListener(v -> {
dialog.dismiss();
if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) {
return;
}
int pos = getStringResourceIndex(R.array.export_formats, dropdown.getText().toString());
int requestCode = getExportRequestCode(pos, checkBoxEncrypt.isChecked());
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked());
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(getExportMimeType(requestCode))
.putExtra(Intent.EXTRA_TITLE, fileInfo.toString());
AegisActivity.Helper.startExtActivityForResult(this, intent, requestCode);
});
btnNeutral.setOnClickListener(v -> {
dialog.dismiss();
int pos = getStringResourceIndex(R.array.export_formats, dropdown.getText().toString());
if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) {
return;
}
File file;
try {
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked());
file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir());
} catch (IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
return;
}
int requestCode = getExportRequestCode(pos, checkBoxEncrypt.isChecked());
startExportVault(requestCode, cb -> {
try (OutputStream stream = new FileOutputStream(file)) {
cb.exportVault(stream);
} catch (IOException | VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
return;
}
Uri uri = FileProvider.getUriForFile(getContext(), BuildConfig.FILE_PROVIDER_AUTHORITY, file);
Intent intent = new Intent(Intent.ACTION_SEND)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(getExportMimeType(requestCode))
.putExtra(Intent.EXTRA_STREAM, uri);
Intent chooser = Intent.createChooser(intent, getString(R.string.pref_export_summary));
AegisActivity.Helper.startExtActivity(this, chooser);
});
});
});
Dialogs.showSecureDialog(dialog);
}
private static int getExportRequestCode(int spinnerPos, boolean encrypt) {
if (spinnerPos == 0) {
return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN;
}
return CODE_EXPORT_GOOGLE_URI;
}
private static VaultBackupManager.FileInfo getExportFileInfo(int spinnerPos, boolean encrypt) {
if (spinnerPos == 0) {
String filename = encrypt ? VaultManager.FILENAME_PREFIX_EXPORT : VaultManager.FILENAME_PREFIX_EXPORT_PLAIN;
return new VaultBackupManager.FileInfo(filename);
}
return new VaultBackupManager.FileInfo(VaultManager.FILENAME_PREFIX_EXPORT_URI, "txt");
}
private static String getExportMimeType(int requestCode) {
return requestCode == CODE_EXPORT_GOOGLE_URI ? "text/plain" : "application/json";
}
private File getExportCacheDir() throws IOException {
File dir = new File(getContext().getCacheDir(), "export");
if (!dir.exists() && !dir.mkdir()) {
throw new IOException(String.format("Unable to create directory %s", dir));
}
return dir;
}
private void startExportVault(int requestCode, StartExportCallback cb) {
switch (requestCode) {
case CODE_EXPORT:
if (getVault().isEncryptionEnabled()) {
cb.exportVault(stream -> getVault().export(stream));
} else {
Dialogs.showSetPasswordDialog(getActivity(), new Dialogs.SlotListener() {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
VaultFileCredentials creds = new VaultFileCredentials();
try {
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
} catch (SlotException e) {
onException(e);
return;
}
cb.exportVault(stream -> getVault().export(stream, creds));
}
@Override
public void onException(Exception e) {
}
});
}
break;
case CODE_EXPORT_PLAIN:
cb.exportVault((stream) -> getVault().export(stream, null));
break;
case CODE_EXPORT_GOOGLE_URI:
cb.exportVault((stream) -> getVault().exportGoogleUris(stream));
break;
}
}
private void onExportResult(int requestCode, int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
startExportVault(requestCode, cb -> {
File file;
OutputStream outStream = null;
try {
file = File.createTempFile(VaultManager.FILENAME_PREFIX_EXPORT + "-", ".json", getExportCacheDir());
outStream = new FileOutputStream(file);
cb.exportVault(outStream);
new ExportTask(getContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri));
} catch (VaultManagerException | IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
} finally {
try {
if (outStream != null) {
outStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
private int getStringResourceIndex(@ArrayRes int id, String string) {
String[] res = getResources().getStringArray(id);
for (int i = 0; i < res.length; i++) {
if (res[i].equalsIgnoreCase(string)) {
return i;
}
}
return -1;
}
private class ExportResultListener implements ExportTask.Callback {
@Override
public void onTaskFinished(Exception e) {
if (e != null) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
} else {
Toast.makeText(getContext(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
}
}
}
private interface FinishExportCallback {
void exportVault(OutputStream stream) throws IOException, VaultManagerException;
}
private interface StartExportCallback {
void exportVault(FinishExportCallback exportCb);
}
}
@@ -0,0 +1,13 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.os.Bundle;
import com.beemdevelopment.aegis.R;
public class MainPreferencesFragment extends PreferencesFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences);
}
}
@@ -0,0 +1,88 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.CallSuper;
import androidx.preference.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
public abstract class PreferencesFragment extends PreferenceFragmentCompat {
// activity request codes
public static final int CODE_IMPORT_SELECT = 0;
public static final int CODE_SLOTS = 2;
public static final int CODE_GROUPS = 3;
public static final int CODE_IMPORT = 4;
public static final int CODE_EXPORT = 5;
public static final int CODE_EXPORT_PLAIN = 6;
public static final int CODE_EXPORT_GOOGLE_URI = 7;
public static final int CODE_BACKUPS = 8;
private AegisApplication _app;
private Intent _result;
private Preferences _prefs;
private VaultManager _vault;
@Override
@CallSuper
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
_app = (AegisApplication) getActivity().getApplication();
_prefs = _app.getPreferences();
_vault = _app.getVaultManager();
setResult(new Intent());
}
@Override
@CallSuper
public void onResume() {
super.onResume();
Intent intent = getActivity().getIntent();
String preference = intent.getStringExtra("pref");
if (preference != null) {
scrollToPreference(preference);
intent.removeExtra("pref");
}
}
public Intent getResult() {
return _result;
}
public void setResult(Intent result) {
_result = result;
getActivity().setResult(Activity.RESULT_OK, _result);
}
protected AegisApplication getApp() {
return _app;
}
protected Preferences getPreferences() {
return _prefs;
}
protected VaultManager getVault() {
return _vault;
}
protected boolean saveVault() {
try {
_vault.save(true);
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.saving_error, e);
return false;
}
return true;
}
}
@@ -0,0 +1,412 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.SlotManagerActivity;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import java.util.List;
import javax.crypto.Cipher;
import static android.text.TextUtils.isDigitsOnly;
public class SecurityPreferencesFragment extends PreferencesFragment {
private SwitchPreference _encryptionPreference;
private SwitchPreference _biometricsPreference;
private Preference _autoLockPreference;
private Preference _setPasswordPreference;
private Preference _slotsPreference;
private Preference _passwordReminderPreference;
private SwitchPreferenceCompat _pinKeyboardPreference;
@Override
public void onResume() {
super.onResume();
updateEncryptionPreferences();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_security);
Preference tapToRevealPreference = findPreference("pref_tap_to_reveal");
tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
Preference screenPreference = findPreference("pref_secure_screen");
screenPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRecreate", true);
Window window = getActivity().getWindow();
if ((boolean) newValue) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
return true;
});
Preference tapToRevealTimePreference = findPreference("pref_tap_to_reveal_time");
tapToRevealTimePreference.setSummary(getPreferences().getTapToRevealTime() + " seconds");
tapToRevealTimePreference.setOnPreferenceClickListener(preference -> {
Dialogs.showNumberPickerDialog(getActivity(), number -> {
getPreferences().setTapToRevealTime(number);
tapToRevealTimePreference.setSummary(number + " seconds");
getResult().putExtra("needsRefresh", true);
});
return false;
});
_encryptionPreference = findPreference("pref_encryption");
_encryptionPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if (!getVault().isEncryptionEnabled()) {
Dialogs.showSetPasswordDialog(getActivity(), new EnableEncryptionListener());
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.disable_encryption)
.setMessage(getText(R.string.disable_encryption_description))
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
try {
getVault().disableEncryption();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.disable_encryption_error, e);
return;
}
// clear the KeyStore
try {
KeyStoreHandle handle = new KeyStoreHandle();
handle.clear();
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
getActivity().stopService(new Intent(getActivity(), NotificationService.class));
getPreferences().setIsBackupsEnabled(false);
getPreferences().setIsAndroidBackupsEnabled(false);
updateEncryptionPreferences();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
return false;
});
_biometricsPreference = findPreference("pref_biometrics");
_biometricsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
VaultFileCredentials creds = getVault().getCredentials();
SlotList slots = creds.getSlots();
if (!slots.has(BiometricSlot.class)) {
if (BiometricsHelper.isAvailable(getContext())) {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(SecurityPreferencesFragment.this, new RegisterBiometricsListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.set_up_biometric))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
initializer.authenticate(info);
}
} else {
// remove the biometric slot
BiometricSlot slot = slots.find(BiometricSlot.class);
slots.remove(slot);
getVault().setCredentials(creds);
// remove the KeyStore key
try {
KeyStoreHandle handle = new KeyStoreHandle();
handle.deleteKey(slot.getUUID().toString());
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
saveVault();
updateEncryptionPreferences();
}
return false;
});
_setPasswordPreference = findPreference("pref_password");
_setPasswordPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showSetPasswordDialog(getActivity(), new SetPasswordListener());
return false;
});
_slotsPreference = findPreference("pref_slots");
_slotsPreference.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(getActivity(), SlotManagerActivity.class);
intent.putExtra("creds", getVault().getCredentials());
startActivityForResult(intent, CODE_SLOTS);
return true;
});
_pinKeyboardPreference = findPreference("pref_pin_keyboard");
_pinKeyboardPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if (!(boolean) newValue) {
return true;
}
Dialogs.showPasswordInputDialog(getActivity(), R.string.set_password_confirm, R.string.pin_keyboard_description, password -> {
if (isDigitsOnly(new String(password))) {
List<PasswordSlot> slots = getVault().getCredentials().getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(getActivity(), new PasswordConfirmationListener());
task.execute(getLifecycle(), params);
} else {
_pinKeyboardPreference.setChecked(false);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.pin_keyboard_error)
.setMessage(R.string.pin_keyboard_error_description)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.create());
}
}, dialog -> {
_pinKeyboardPreference.setChecked(false);
});
return false;
});
_autoLockPreference = findPreference("pref_auto_lock");
_autoLockPreference.setSummary(getAutoLockSummary());
_autoLockPreference.setOnPreferenceClickListener((preference) -> {
final int[] items = Preferences.AUTO_LOCK_SETTINGS;
final String[] textItems = getResources().getStringArray(R.array.pref_auto_lock_types);
final boolean[] checkedItems = new boolean[items.length];
for (int i = 0; i < items.length; i++) {
checkedItems[i] = getPreferences().isAutoLockTypeEnabled(items[i]);
}
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.pref_auto_lock_prompt)
.setMultiChoiceItems(textItems, checkedItems, (dialog, index, isChecked) -> checkedItems[index] = isChecked)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int autoLock = Preferences.AUTO_LOCK_OFF;
for (int i = 0; i < checkedItems.length; i++) {
if (checkedItems[i]) {
autoLock |= items[i];
}
}
getPreferences().setAutoLockMask(autoLock);
_autoLockPreference.setSummary(getAutoLockSummary());
})
.setNegativeButton(android.R.string.cancel, null);
Dialogs.showSecureDialog(builder.create());
return false;
});
_passwordReminderPreference = findPreference("pref_password_reminder");
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data != null && requestCode == CODE_SLOTS) {
onSlotManagerResult(resultCode, data);
}
}
private void onSlotManagerResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
getVault().setCredentials(creds);
saveVault();
updateEncryptionPreferences();
}
private void updateEncryptionPreferences() {
boolean encrypted = getVault().isEncryptionEnabled();
_encryptionPreference.setChecked(encrypted, true);
_setPasswordPreference.setVisible(encrypted);
_biometricsPreference.setVisible(encrypted);
_slotsPreference.setEnabled(encrypted);
_autoLockPreference.setVisible(encrypted);
_pinKeyboardPreference.setVisible(encrypted);
if (encrypted) {
SlotList slots = getVault().getCredentials().getSlots();
boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1;
boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1;
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiBio;
boolean canUseBio = BiometricsHelper.isAvailable(getContext());
_setPasswordPreference.setEnabled(!multiPassword);
_biometricsPreference.setEnabled(canUseBio && !multiBio);
_biometricsPreference.setChecked(slots.has(BiometricSlot.class), true);
_slotsPreference.setVisible(showSlots);
_passwordReminderPreference.setVisible(slots.has(BiometricSlot.class));
} else {
_setPasswordPreference.setEnabled(false);
_biometricsPreference.setEnabled(false);
_biometricsPreference.setChecked(false, true);
_slotsPreference.setVisible(false);
_passwordReminderPreference.setVisible(false);
}
}
private String getAutoLockSummary() {
final int[] settings = Preferences.AUTO_LOCK_SETTINGS;
final String[] descriptions = getResources().getStringArray(R.array.pref_auto_lock_types);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < settings.length; i++) {
if (getPreferences().isAutoLockTypeEnabled(settings[i])) {
if (builder.length() != 0) {
builder.append(", ");
}
builder.append(descriptions[i].toLowerCase());
}
}
if (builder.length() == 0) {
return getString(R.string.pref_auto_lock_summary_disabled);
}
return getString(R.string.pref_auto_lock_summary, builder.toString());
}
private class SetPasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
VaultFileCredentials creds = getVault().getCredentials();
SlotList slots = creds.getSlots();
try {
// encrypt the master key for this slot
slot.setKey(creds.getKey(), cipher);
// remove the old master password slot
PasswordSlot oldSlot = creds.getSlots().find(PasswordSlot.class);
if (oldSlot != null) {
slots.remove(oldSlot);
}
// add the new master password slot
slots.add(slot);
} catch (SlotException e) {
onException(e);
return;
}
getVault().setCredentials(creds);
saveVault();
if (getPreferences().isPinKeyboardEnabled()) {
_pinKeyboardPreference.setChecked(false);
Toast.makeText(getContext(), R.string.pin_keyboard_disabled, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onException(Exception e) {
e.printStackTrace();
updateEncryptionPreferences();
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
}
}
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
VaultFileCredentials creds = getVault().getCredentials();
try {
slot.setKey(creds.getKey(), cipher);
} catch (SlotException e) {
e.printStackTrace();
onSlotInitializationFailed(0, e.toString());
return;
}
creds.getSlots().add(slot);
getVault().setCredentials(creds);
saveVault();
updateEncryptionPreferences();
}
@Override
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString);
}
}
}
private class EnableEncryptionListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
VaultFileCredentials creds = new VaultFileCredentials();
try {
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
getVault().enableEncryption(creds);
} catch (VaultManagerException | SlotException e) {
onException(e);
return;
}
getActivity().startService(new Intent(getActivity(), NotificationService.class));
updateEncryptionPreferences();
}
@Override
public void onException(Exception e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
}
}
private class PasswordConfirmationListener implements PasswordSlotDecryptTask.Callback {
@Override
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
if (result != null) {
_pinKeyboardPreference.setChecked(true);
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.pin_keyboard_error)
.setMessage(R.string.invalid_password)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.create());
_pinKeyboardPreference.setChecked(false);
}
}
}
}
@@ -1,6 +1,7 @@
package com.beemdevelopment.aegis.ui.glide;
import android.content.Context;
import android.graphics.drawable.PictureDrawable;
import androidx.annotation.NonNull;
@@ -9,7 +10,9 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import com.caverock.androidsvg.SVG;
import java.io.InputStream;
import java.nio.ByteBuffer;
@GlideModule
@@ -17,5 +20,8 @@ public class AegisGlideModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.prepend(VaultEntry.class, ByteBuffer.class, new IconLoader.Factory());
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
.append(InputStream.class, SVG.class, new SvgDecoder())
.append(ByteBuffer.class, SVG.class, new SvgBytesDecoder());
}
}
@@ -2,9 +2,11 @@ package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Option;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.ModelLoader;
@@ -14,6 +16,8 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import java.nio.ByteBuffer;
public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
public static final Option<IconType> ICON_TYPE = Option.memory("ICON_TYPE", IconType.INVALID);
@Override
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntry model, int width, int height, @NonNull Options options) {
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model));
@@ -25,7 +29,7 @@ public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
}
public static class Fetcher implements DataFetcher<ByteBuffer> {
private VaultEntry _model;
private final VaultEntry _model;
private Fetcher(VaultEntry model) {
_model = model;
@@ -0,0 +1,29 @@
package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.caverock.androidsvg.SVG;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class SvgBytesDecoder implements ResourceDecoder<ByteBuffer, SVG> {
private SvgDecoder _decoder = new SvgDecoder();
@Override
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(source.array())) {
return _decoder.handles(inStream, options);
}
}
public Resource<SVG> decode(@NonNull ByteBuffer source, int width, int height, @NonNull Options options) throws IOException {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(source.array())) {
return _decoder.decode(inStream, width, height, options);
}
}
}
@@ -0,0 +1,44 @@
package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.icons.IconType;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.IOException;
import java.io.InputStream;
import static com.bumptech.glide.request.target.Target.SIZE_ORIGINAL;
// source: https://github.com/bumptech/glide/tree/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg
/** Decodes an SVG internal representation from an {@link InputStream}. */
public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
@Override
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
return options.get(IconLoader.ICON_TYPE) == IconType.SVG;
}
public Resource<SVG> decode(
@NonNull InputStream source, int width, int height, @NonNull Options options)
throws IOException {
try {
SVG svg = SVG.getFromInputStream(source);
if (width != SIZE_ORIGINAL) {
svg.setDocumentWidth(width);
}
if (height != SIZE_ORIGINAL) {
svg.setDocumentHeight(height);
}
return new SimpleResource<>(svg);
} catch (SVGParseException ex) {
throw new IOException("Cannot load SVG from stream", ex);
}
}
}
@@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.ui.glide;
import android.graphics.Picture;
import android.graphics.drawable.PictureDrawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
import com.caverock.androidsvg.SVG;
// source: https://github.com/bumptech/glide/tree/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg
/**
* Convert the {@link SVG}'s internal representation to an Android-compatible one ({@link Picture}).
*/
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
@Nullable
@Override
public Resource<PictureDrawable> transcode(
@NonNull Resource<SVG> toTranscode, @NonNull Options options) {
SVG svg = toTranscode.get();
Picture picture = svg.renderToPicture();
PictureDrawable drawable = new PictureDrawable(picture);
return new SimpleResource<>(drawable);
}
}
@@ -3,32 +3,19 @@ package com.beemdevelopment.aegis.ui.models;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.io.Serializable;
import java.util.UUID;
public class ImportEntry implements Serializable {
private UUID _uuid;
private String _name;
private String _issuer;
private final VaultEntry _entry;
private transient Listener _listener;
private boolean _isChecked = true;
public ImportEntry(VaultEntry entry) {
_uuid = entry.getUUID();
_name = entry.getName();
_issuer = entry.getIssuer();
_entry = entry;
}
public UUID getUUID() {
return _uuid;
}
public String getName() {
return _name;
}
public String getIssuer() {
return _issuer;
public VaultEntry getEntry() {
return _entry;
}
public void setOnCheckedChangedListener(Listener listener) {
@@ -23,7 +23,7 @@ import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
@@ -0,0 +1,98 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import android.net.Uri;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.util.IOUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* ImportFileTask reads an SAF file from a background thread and
* writes it to a temporary file in the cache directory.
*/
public class ImportFileTask extends ProgressDialogTask<ImportFileTask.Params, ImportFileTask.Result> {
private final Callback _cb;
public ImportFileTask(Context context, Callback cb) {
super(context, context.getString(R.string.reading_file));
_cb = cb;
}
@Override
protected Result doInBackground(Params... params) {
Context context = getDialog().getContext();
Params p = params[0];
try (InputStream inStream = context.getContentResolver().openInputStream(p.getUri())) {
String prefix = p.getNamePrefix() != null ? p.getNamePrefix() + "-" : "";
String suffix = p.getNameSuffix() != null ? "-" + p.getNameSuffix() : "";
File tempFile = File.createTempFile(prefix, suffix, context.getCacheDir());
try (FileOutputStream outStream = new FileOutputStream(tempFile)) {
IOUtils.copy(inStream, outStream);
}
return new Result(tempFile, null);
} catch (IOException e) {
e.printStackTrace();
return new Result(null, e);
}
}
@Override
protected void onPostExecute(Result result) {
super.onPostExecute(result);
_cb.onTaskFinished(result);
}
public interface Callback {
void onTaskFinished(Result result);
}
public static class Params {
private final Uri _uri;
private final String _namePrefix;
private final String _nameSuffix;
public Params(Uri uri, String namePrefix, String nameSuffix) {
_uri = uri;
_namePrefix = namePrefix;
_nameSuffix = nameSuffix;
}
public Uri getUri() {
return _uri;
}
public String getNamePrefix() {
return _namePrefix;
}
public String getNameSuffix() {
return _nameSuffix;
}
}
public static class Result {
private final File _file;
private final Exception _e;
public Result(File file, Exception e) {
_file = file;
_e = e;
}
public File getFile() {
return _file;
}
public Exception getException() {
return _e;
}
}
}
@@ -0,0 +1,95 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import android.net.Uri;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconPackException;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.util.IOUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class ImportIconPackTask extends ProgressDialogTask<ImportIconPackTask.Params, ImportIconPackTask.Result> {
private final ImportIconPackTask.Callback _cb;
public ImportIconPackTask(Context context, ImportIconPackTask.Callback cb) {
super(context, context.getString(R.string.importing_icon_pack));
_cb = cb;
}
@Override
protected ImportIconPackTask.Result doInBackground(ImportIconPackTask.Params... params) {
Context context = getDialog().getContext();
ImportIconPackTask.Params param = params[0];
File tempFile = null;
try {
tempFile = File.createTempFile("icon-pack-", "", context.getCacheDir());
try (InputStream inStream = context.getContentResolver().openInputStream(param.getUri());
FileOutputStream outStream = new FileOutputStream(tempFile)) {
IOUtils.copy(inStream, outStream);
}
IconPack pack = param.getManager().importPack(tempFile);
return new Result(pack, null);
} catch (IOException | IconPackException e) {
e.printStackTrace();
return new ImportIconPackTask.Result(null, e);
} finally {
if (tempFile != null) {
tempFile.delete();
}
}
}
@Override
protected void onPostExecute(ImportIconPackTask.Result result) {
super.onPostExecute(result);
_cb.onTaskFinished(result);
}
public interface Callback {
void onTaskFinished(ImportIconPackTask.Result result);
}
public static class Params {
private final IconPackManager _manager;
private final Uri _uri;
public Params(IconPackManager manager, Uri uri) {
_manager = manager;
_uri = uri;
}
public IconPackManager getManager() {
return _manager;
}
public Uri getUri() {
return _uri;
}
}
public static class Result {
private final IconPack _pack;
private final Exception _e;
public Result(IconPack pack, Exception e) {
_pack = pack;
_e = e;
}
public IconPack getIconPack() {
return _pack;
}
public Exception getException() {
return _e;
}
}
}
@@ -28,9 +28,13 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
setPriority();
Params params = args[0];
for (PasswordSlot slot : params.getSlots()) {
return decrypt(params.getSlots(), params.getPassword());
}
public static Result decrypt(List<PasswordSlot> slots, char[] password) {
for (PasswordSlot slot : slots) {
try {
return decryptPasswordSlot(slot, params.getPassword());
return decryptPasswordSlot(slot, password);
} catch (SlotException e) {
throw new RuntimeException(e);
} catch (SlotIntegrityException ignored) {
@@ -41,7 +45,7 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
return null;
}
private Result decryptPasswordSlot(PasswordSlot slot, char[] password)
public static Result decryptPasswordSlot(PasswordSlot slot, char[] password)
throws SlotIntegrityException, SlotException {
MasterKey masterKey;
SecretKey key = slot.deriveKey(password);
@@ -56,8 +60,6 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
throw e;
}
publishProgress(getDialog().getContext().getString(R.string.unlocking_vault_repair));
// try to decrypt the password slot with the old key
SecretKey oldKey = slot.deriveKey(oldPasswordBytes);
masterKey = decryptPasswordSlot(slot, oldKey);
@@ -75,7 +77,7 @@ public class PasswordSlotDecryptTask extends ProgressDialogTask<PasswordSlotDecr
return new Result(masterKey, slot, repaired);
}
private MasterKey decryptPasswordSlot(PasswordSlot slot, SecretKey key)
public static MasterKey decryptPasswordSlot(PasswordSlot slot, SecretKey key)
throws SlotException, SlotIntegrityException {
Cipher cipher = slot.createDecryptCipher(key);
return slot.getKey(cipher);
@@ -12,7 +12,7 @@ import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, String, Result> {
private ProgressDialog _dialog;
@@ -6,9 +6,9 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
@@ -25,6 +25,7 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.UUID;
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
@@ -32,16 +33,16 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
private List<VaultEntry> _entries;
private List<VaultEntry> _shownEntries;
private List<VaultEntry> _selectedEntries;
private Map<UUID, Integer> _usageCounts;
private VaultEntry _focusedEntry;
private int _codeGroupSize;
private boolean _showAccountName;
private boolean _searchAccountName;
private boolean _highlightEntry;
private boolean _tempHighlightEntry;
private boolean _tapToReveal;
private int _tapToRevealTime;
private boolean _copyOnTap;
private String _groupFilter;
private List<String> _groupFilter;
private SortCategory _sortCategory;
private ViewMode _viewMode;
private String _searchFilter;
@@ -57,6 +58,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_entries = new ArrayList<>();
_shownEntries = new ArrayList<>();
_selectedEntries = new ArrayList<>();
_groupFilter = new ArrayList<>();
_holders = new ArrayList<>();
_dimHandler = new Handler();
_view = view;
@@ -85,10 +87,6 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_tapToRevealTime = number;
}
public void setSearchAccountName(boolean searchAccountName) {
_searchAccountName = searchAccountName;
}
public void setHighlightEntry(boolean highlightEntry) {
_highlightEntry = highlightEntry;
}
@@ -143,6 +141,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
public void addEntries(Collection<VaultEntry> entries) {
for (VaultEntry entry: entries) {
entry.setUsageCount(_usageCounts.containsKey(entry.getUUID()) ? _usageCounts.get(entry.getUUID()) : 0);
}
_entries.addAll(entries);
updateShownEntries();
checkPeriodUniformity(true);
@@ -212,12 +214,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
String issuer = entry.getIssuer().toLowerCase();
String name = entry.getName().toLowerCase();
if (_groupFilter != null) {
if (group == null && _groupFilter.equals(_view.getContext().getString(R.string.filter_ungrouped))) {
return false;
}
if (group == null || !group.equals(_groupFilter)) {
if (!_groupFilter.isEmpty()) {
if (group == null || !_groupFilter.contains(group)) {
return true;
}
}
@@ -226,7 +224,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
return false;
}
return !issuer.contains(_searchFilter) && !(_searchAccountName && name.contains(_searchFilter));
return !issuer.contains(_searchFilter) && !name.contains(_searchFilter);
}
public void refresh(boolean hard) {
@@ -239,15 +237,14 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
}
public void setGroupFilter(String group, boolean apply) {
if (_groupFilter != null && _groupFilter.equals(group)) {
public void setGroupFilter(@NonNull List<String> groups) {
if (_groupFilter.equals(groups)) {
return;
}
_groupFilter = group;
if (apply) {
updateShownEntries();
checkPeriodUniformity();
}
_groupFilter = groups;
updateShownEntries();
checkPeriodUniformity();
}
public void setSortCategory(SortCategory category, boolean apply) {
@@ -262,7 +259,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
public void setSearchFilter(String search) {
_searchFilter = search != null ? search.toLowerCase() : null;
_searchFilter = (search != null && !search.isEmpty()) ? search.toLowerCase() : null;
updateShownEntries();
}
@@ -291,6 +288,14 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_viewMode = viewMode;
}
public void setUsageCounts(Map<UUID, Integer> usageCounts) { _usageCounts = usageCounts; }
public Map<UUID, Integer> getUsageCounts() { return _usageCounts; }
public void setGroups(TreeSet<String> groups) {
_view.setGroups(groups);
}
@Override
public void onItemDismiss(int position) {
@@ -299,7 +304,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public void onItemDrop(int position) {
// moving entries is not allowed when a filter is applied
if (_groupFilter != null) {
if (!_groupFilter.isEmpty()) {
return;
}
@@ -309,7 +314,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public void onItemMove(int firstPosition, int secondPosition) {
// moving entries is not allowed when a filter is applied
if (_groupFilter != null) {
if (!_groupFilter.isEmpty()) {
return;
}
@@ -372,6 +377,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
focusEntry(entry, _tapToRevealTime);
}
}
incrementUsageCount(entry);
} else {
if (_selectedEntries.contains(entry)) {
_view.onDeselect(entry);
@@ -595,8 +602,17 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
updateDraggableStatus();
}
private void incrementUsageCount(VaultEntry entry) {
if (!_usageCounts.containsKey(entry.getUUID())) {
_usageCounts.put(entry.getUUID(), 1);
} else {
int usageCount = _usageCounts.get(entry.getUUID());
_usageCounts.put(entry.getUUID(), ++usageCount);
}
}
public boolean isDragAndDropAllowed() {
return _sortCategory == SortCategory.CUSTOM && _groupFilter == null && _searchFilter == null;
return _sortCategory == SortCategory.CUSTOM && _groupFilter.isEmpty() && _searchFilter == null;
}
public boolean isPeriodUniform() {
@@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.helpers.UiRefresher;
@@ -21,10 +22,10 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.zxing.common.StringUtils;
public class EntryHolder extends RecyclerView.ViewHolder {
private static final float DEFAULT_ALPHA = 1.0f;
@@ -141,9 +142,11 @@ public class EntryHolder extends RecyclerView.ViewHolder {
public void loadIcon(Fragment fragment) {
if (_entry.hasIcon()) {
IconViewHelper.setLayerType(_profileDrawable, _entry.getIconType());
Glide.with(fragment)
.asDrawable()
.load(_entry)
.set(IconLoader.ICON_TYPE, _entry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_profileDrawable);
@@ -2,7 +2,9 @@ package com.beemdevelopment.aegis.ui.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -10,6 +12,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
@@ -28,6 +31,8 @@ import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader;
@@ -35,11 +40,17 @@ import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.UUID;
import java.util.stream.Collectors;
public class EntryListView extends Fragment implements EntryAdapter.Listener {
private EntryAdapter _adapter;
@@ -53,7 +64,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private TotpProgressBar _progressBar;
private boolean _showProgress;
private ViewMode _viewMode;
private TreeSet<String> _groups;
private LinearLayout _emptyStateView;
private Chip _groupChip;
private List<String> _groupFilter;
private List<String> _prefGroupFilter;
private UiRefresher _refresher;
@@ -74,6 +89,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
_progressBar = view.findViewById(R.id.progressBar);
_groupChip = view.findViewById(R.id.chip_group);
initializeGroupChip();
// set up the recycler view
_recyclerView = view.findViewById(R.id.rvKeyProfiles);
@@ -115,7 +132,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
});
_emptyStateView = view.findViewById(R.id.vEmptyList);
return view;
}
@@ -129,11 +145,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
super.onDestroyView();
}
public void setGroupFilter(String group, boolean apply) {
_adapter.setGroupFilter(group, apply);
public void setGroupFilter(List<String> groups, boolean animate) {
_groupFilter = groups;
_adapter.setGroupFilter(groups);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
updateEmptyState();
updateGroupChip();
if (apply) {
if (animate) {
runEntriesAnimation();
}
}
@@ -166,6 +185,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
}
public void setUsageCounts(Map<UUID, Integer> usageCounts) {
_adapter.setUsageCounts(usageCounts);
}
public Map<UUID, Integer> getUsageCounts() {
return _adapter.getUsageCounts();
}
public void setSearchFilter(String search) {
_adapter.setSearchFilter(search);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
@@ -251,6 +278,10 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
@Override
public void onListChange() { _listener.onListChange(); }
public void setPrefGroupFilter(List<String> groupFilter) {
_prefGroupFilter = groupFilter;
}
public void setCodeGroupSize(int codeGrouping) {
_adapter.setCodeGroupSize(codeGrouping);
}
@@ -259,10 +290,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_adapter.setShowAccountName(showAccountName);
}
public void setSearchAccountName(boolean searchAccountName) {
_adapter.setSearchAccountName(searchAccountName);
}
public void setHighlightEntry(boolean highlightEntry) {
_adapter.setHighlightEntry(highlightEntry);
}
@@ -284,33 +311,44 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
int position = _adapter.addEntry(entry);
updateEmptyState();
LinearLayoutManager layoutManager = (LinearLayoutManager) _recyclerView.getLayoutManager();
if (focusEntry && position >= 0) {
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
_recyclerView.removeOnScrollListener(this);
_adapter.setTempHighlightEntry(true);
final int secondsToFocus = 3;
_adapter.focusEntry(entry, secondsToFocus);
if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition())
|| (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) {
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
_recyclerView.removeOnScrollListener(this);
_recyclerView.setOnTouchListener(null);
tempHighlightEntry(entry);
}
}
};
_recyclerView.addOnScrollListener(scrollListener);
_recyclerView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
_recyclerView.removeOnScrollListener(scrollListener);
_recyclerView.stopScroll();
_recyclerView.setOnTouchListener(null);
}
}
};
_recyclerView.addOnScrollListener(scrollListener);
_recyclerView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
_recyclerView.removeOnScrollListener(scrollListener);
_recyclerView.stopScroll();
_recyclerView.setOnTouchListener(null);
}
return false;
});
_recyclerView.smoothScrollToPosition(position);
return false;
});
_recyclerView.smoothScrollToPosition(position);
} else {
tempHighlightEntry(entry);
}
}
}
public void tempHighlightEntry(VaultEntry entry) {
_adapter.setTempHighlightEntry(true);
final int secondsToFocus = 3;
_adapter.focusEntry(entry, secondsToFocus);
}
public void addEntries(Collection<VaultEntry> entries) {
_adapter.addEntries(entries);
updateEmptyState();
@@ -343,11 +381,97 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_recyclerView.scheduleLayoutAnimation();
}
private void initializeGroupChip() {
View view = getLayoutInflater().inflate(R.layout.dialog_select_groups, null);
BottomSheetDialog dialog = new BottomSheetDialog(getContext());
dialog.setContentView(view);
ChipGroup chipGroup = view.findViewById(R.id.groupChipGroup);
Button clearButton = view.findViewById(R.id.btnClear);
Button saveButton = view.findViewById(R.id.btnSave);
clearButton.setOnClickListener(v -> {
chipGroup.clearCheck();
List<String> groupFilter = Collections.emptyList();
_listener.onSaveGroupFilter(groupFilter);
setGroupFilter(groupFilter, true);
dialog.dismiss();
});
saveButton.setOnClickListener(v -> {
List<String> groupFilter = getGroupFilter(chipGroup);
_listener.onSaveGroupFilter(groupFilter);
setGroupFilter(groupFilter, true);
dialog.dismiss();
});
_groupChip.setOnClickListener(v -> {
ColorStateList colorStateList = ContextCompat.getColorStateList(getContext(), R.color.bg_chip_text_color);
chipGroup.removeAllViews();
for (String group : _groups) {
Chip chip = new Chip(getContext());
chip.setText(group);
chip.setCheckable(true);
chip.setChecked(_groupFilter != null && _groupFilter.contains(group));
chip.setCheckedIconVisible(false);
chip.setChipBackgroundColorResource(R.color.bg_chip_color);
chip.setTextColor(colorStateList);
chip.setOnCheckedChangeListener((group1, checkedId) -> {
List<String> groupFilter = getGroupFilter(chipGroup);
setGroupFilter(groupFilter, true);
});
chipGroup.addView(chip);
}
Dialogs.showSecureDialog(dialog);
});
}
private static List<String> getGroupFilter(ChipGroup chipGroup) {
return chipGroup.getCheckedChipIds().stream()
.map(i -> ((Chip) chipGroup.findViewById(i)).getText().toString())
.collect(Collectors.toList());
}
private void updateGroupChip() {
if (_groupFilter.isEmpty()) {
_groupChip.setText(R.string.groups);
} else {
_groupChip.setText(String.format("%s (%d)", getString(R.string.groups), _groupFilter.size()));
}
}
private void setShowProgress(boolean showProgress) {
_showProgress = showProgress;
updateDividerDecoration();
}
public void setGroups(TreeSet<String> groups) {
_groups = groups;
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
updateDividerDecoration();
if (_prefGroupFilter != null) {
List<String> groupFilter = cleanGroupFilter(_prefGroupFilter);
_prefGroupFilter = null;
if (!groupFilter.isEmpty()) {
setGroupFilter(groupFilter, false);
}
} else if (_groupFilter != null) {
List<String> groupFilter = cleanGroupFilter(_groupFilter);
if (!_groupFilter.equals(groupFilter)) {
setGroupFilter(groupFilter, true);
}
}
}
private List<String> cleanGroupFilter(List<String> groupFilter) {
return groupFilter.stream()
.filter(g -> _groups.contains(g))
.collect(Collectors.toList());
}
private void updateDividerDecoration() {
if (_dividerDecoration != null) {
_recyclerView.removeItemDecoration(_dividerDecoration);
@@ -386,6 +510,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
void onSelect(VaultEntry entry);
void onDeselect(VaultEntry entry);
void onListChange();
void onSaveGroupFilter(List<String> groupFilter);
}
private class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {
@@ -398,7 +523,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
if (parent.getChildAdapterPosition(view) == 0) {
if (parent.getChildAdapterPosition(view) == 0 && (_groups == null || _groups.isEmpty())) {
// the first item should also have a top margin
outRect.top = _height;
}
@@ -419,10 +544,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
@Nullable
@Override
public RequestBuilder getPreloadRequestBuilder(@NonNull VaultEntry entry) {
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) {
return Glide.with(EntryListView.this)
.asDrawable()
.load(entry)
.set(IconLoader.ICON_TYPE, entry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
}
@@ -0,0 +1,288 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class IconAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final Context _context;
private final String _issuer;
private final Listener _listener;
private IconPack _pack;
private List<IconPack.Icon> _icons;
private final List<CategoryHeader> _categories;
private String _query;
public IconAdapter(@NonNull Context context, String issuer, @NonNull Listener listener) {
_context = context;
_issuer = issuer;
_listener = listener;
_icons = new ArrayList<>();
_categories = new ArrayList<>();
}
/**
* Loads all icons from the given icon pack into this adapter. Any icons added before this call will be overwritten.
*/
public void loadIcons(IconPack pack) {
_pack = pack;
_query = null;
_icons = new ArrayList<>(_pack.getIcons());
_categories.clear();
Comparator<IconPack.Icon> iconCategoryComparator = (i1, i2) -> {
String c1 = getCategoryString(i1.getCategory());
String c2 = getCategoryString(i2.getCategory());
return c1.compareTo(c2);
};
Collections.sort(_icons, iconCategoryComparator.thenComparing(IconPack.Icon::getName));
long categoryCount = _icons.stream()
.map(IconPack.Icon::getCategory)
.filter(Objects::nonNull)
.distinct()
.count();
List<IconPack.Icon> suggested = pack.getSuggestedIcons(_issuer);
suggested.add(0, new DummyIcon(_context.getString(R.string.icon_custom)));
if (suggested.size() > 0) {
CategoryHeader category = new CategoryHeader(_context.getString(R.string.suggested));
category.setIsCollapsed(false);
category.getIcons().addAll(suggested);
_categories.add(category);
}
CategoryHeader category = null;
for (IconPack.Icon icon : _icons) {
String iconCategory = getCategoryString(icon.getCategory());
if (category == null || !getCategoryString(category.getCategory()).equals(iconCategory)) {
boolean collapsed = !(categoryCount == 0 && category == null);
category = new CategoryHeader(iconCategory);
category.setIsCollapsed(collapsed);
_categories.add(category);
}
category.getIcons().add(icon);
}
_icons.addAll(0, suggested);
updateCategoryPositions();
notifyDataSetChanged();
}
public void setQuery(@Nullable String query) {
_query = query;
if (_query == null) {
loadIcons(_pack);
} else {
_icons = _pack.getIcons().stream()
.filter(i -> i.isSuggestedFor(query))
.collect(Collectors.toList());
Collections.sort(_icons, Comparator.comparing(IconPack.Icon::getName));
notifyDataSetChanged();
}
}
public IconPack.Icon getIconAt(int position) {
if (isQueryActive()) {
return _icons.get(position);
}
position = translateIconPosition(position);
return _icons.get(position);
}
public CategoryHeader getCategoryAt(int position) {
return _categories.stream()
.filter(c -> c.getPosition() == position)
.findFirst()
.orElse(null);
}
private String getCategoryString(String category) {
return category == null ? _context.getString(R.string.uncategorized) : category;
}
private boolean isCategoryPosition(int position) {
if (isQueryActive()) {
return false;
}
return getCategoryAt(position) != null;
}
private int translateIconPosition(int position) {
int offset = 0;
for (CategoryHeader category : _categories) {
if (category.getPosition() < position) {
offset++;
if (category.isCollapsed()) {
offset -= category.getIcons().size();
}
}
}
return position - offset;
}
private void updateCategoryPositions() {
int i = 0;
for (CategoryHeader category : _categories) {
category.setPosition(i);
int icons = 0;
if (!category.isCollapsed()) {
icons = category.getIcons().size();
}
i += 1 + icons;
}
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
return viewType == R.layout.card_icon ? new IconHolder(view) : new IconCategoryHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (!isCategoryPosition(position)) {
IconHolder iconHolder = (IconHolder) holder;
IconPack.Icon icon = getIconAt(position);
iconHolder.setData(icon);
iconHolder.loadIcon(_context);
iconHolder.itemView.setOnClickListener(v -> {
if (icon instanceof DummyIcon) {
_listener.onCustomSelected();
} else {
_listener.onIconSelected(icon);
}
});
} else {
IconCategoryHolder categoryHolder = (IconCategoryHolder) holder;
CategoryHeader category = getCategoryAt(position);
categoryHolder.setData(category);
categoryHolder.itemView.setOnClickListener(v -> {
boolean collapsed = !category.isCollapsed();
categoryHolder.setIsCollapsed(collapsed);
category.setIsCollapsed(collapsed);
int startPosition = category.getPosition() + 1;
if (category.isCollapsed()) {
notifyItemRangeRemoved(startPosition, category.getIcons().size());
} else {
notifyItemRangeInserted(startPosition, category.getIcons().size());
}
updateCategoryPositions();
});
}
}
@Override
public int getItemCount() {
if (isQueryActive()) {
return _icons.size();
}
int items = _categories.stream()
.filter(c -> !c.isCollapsed())
.mapToInt(c -> c.getIcons().size())
.sum();
return items + _categories.size();
}
@Override
public int getItemViewType(int position) {
if (isCategoryPosition(position)) {
return R.layout.card_icon_category;
}
return R.layout.card_icon;
}
private boolean isQueryActive() {
return _query != null;
}
public interface Listener {
void onIconSelected(IconPack.Icon icon);
void onCustomSelected();
}
public static class DummyIcon extends IconPack.Icon {
private final String _name;
protected DummyIcon(String name) {
super(null, null, null);
_name = name;
}
@Override
public String getName() {
return _name;
}
@Override
public IconType getIconType() {
return null;
}
}
public static class CategoryHeader {
private final String _category;
private int _position = -1;
private final List<IconPack.Icon> _icons;
private boolean _collapsed = true;
public CategoryHeader(String category) {
_category = category;
_icons = new ArrayList<>();
}
public String getCategory() {
return _category;
}
public int getPosition() {
return _position;
}
public void setPosition(int position) {
_position = position;
}
public List<IconPack.Icon> getIcons() {
return _icons;
}
public boolean isCollapsed() {
return _collapsed;
}
public void setIsCollapsed(boolean collapsed) {
_collapsed = collapsed;
}
}
}
@@ -0,0 +1,36 @@
package com.beemdevelopment.aegis.ui.views;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
public class IconCategoryHolder extends RecyclerView.ViewHolder {
private final TextView _textView;
private final ImageView _imgView;
public IconCategoryHolder(final View view) {
super(view);
_textView = view.findViewById(R.id.icon_category);
_imgView = view.findViewById(R.id.icon_category_indicator);
}
public void setData(IconAdapter.CategoryHeader header) {
_textView.setText(header.getCategory());
_imgView.setRotation(getRotation(header.isCollapsed()));
}
public void setIsCollapsed(boolean collapsed) {
_imgView.animate()
.setDuration(200)
.rotation(getRotation(collapsed))
.start();
}
private static int getRotation(boolean collapsed) {
return collapsed ? 90 : 0;
}
}
@@ -0,0 +1,59 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.io.File;
public class IconHolder extends RecyclerView.ViewHolder {
private File _iconFile;
private IconType _iconType;
private boolean _isCustom;
private final ImageView _imageView;
private final TextView _textView;
public IconHolder(final View view) {
super(view);
_imageView = view.findViewById(R.id.icon);
_textView = view.findViewById(R.id.icon_name);
}
public void setData(IconPack.Icon icon) {
_iconFile = icon.getFile();
_iconType = icon.getIconType();
_isCustom = icon instanceof IconAdapter.DummyIcon;
_textView.setText(icon.getName());
}
public void loadIcon(Context context) {
if (_isCustom) {
int tint = ThemeHelper.getThemeColor(R.attr.iconColorPrimary, context.getTheme());
_imageView.setColorFilter(tint);
_imageView.setImageResource(R.drawable.ic_plus_black_24dp);
} else {
_imageView.setImageTintList(null);
IconViewHelper.setLayerType(_imageView, _iconType);
Glide.with(context)
.asDrawable()
.load(_iconFile)
.set(IconLoader.ICON_TYPE, _iconType)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_imageView);
}
}
}
@@ -0,0 +1,66 @@
package com.beemdevelopment.aegis.ui.views;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack;
import java.util.ArrayList;
import java.util.List;
public class IconPackAdapter extends RecyclerView.Adapter<IconPackHolder> {
private IconPackAdapter.Listener _listener;
private List<IconPack> _iconPacks;
public IconPackAdapter(IconPackAdapter.Listener listener) {
_listener = listener;
_iconPacks = new ArrayList<>();
}
public void addIconPack(IconPack pack) {
_iconPacks.add(pack);
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
public void removeIconPack(IconPack pack) {
int position = _iconPacks.indexOf(pack);
_iconPacks.remove(position);
notifyItemRemoved(position);
}
@NonNull
@Override
public IconPackHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_icon_pack, parent, false);
return new IconPackHolder(view);
}
@Override
public void onBindViewHolder(@NonNull IconPackHolder holder, int position) {
holder.setData(_iconPacks.get(position));
holder.setOnDeleteClickListener(v -> {
int position12 = holder.getAdapterPosition();
_listener.onRemoveIconPack(_iconPacks.get(position12));
});
}
@Override
public int getItemCount() {
return _iconPacks.size();
}
public interface Listener {
void onRemoveIconPack(IconPack pack);
}
}
@@ -0,0 +1,32 @@
package com.beemdevelopment.aegis.ui.views;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack;
public class IconPackHolder extends RecyclerView.ViewHolder {
private final TextView _iconPackName;
private final TextView _iconPackInfo;
private final ImageView _buttonDelete;
public IconPackHolder(final View view) {
super(view);
_iconPackName = view.findViewById(R.id.text_icon_pack_name);
_iconPackInfo = view.findViewById(R.id.text_icon_pack_info);
_buttonDelete = view.findViewById(R.id.button_delete);
}
public void setData(IconPack pack) {
_iconPackName.setText(String.format("%s (v%d)", pack.getName(), pack.getVersion()));
_iconPackInfo.setText(itemView.getResources().getQuantityString(R.plurals.icon_pack_info, pack.getIcons().size(), pack.getIcons().size()));
}
public void setOnDeleteClickListener(View.OnClickListener listener) {
_buttonDelete.setOnClickListener(listener);
}
}
@@ -0,0 +1,63 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
// source (slightly modified for Aegis): https://github.com/chiuki/android-recyclerview/blob/745dc88/app/src/main/java/com/sqisland/android/recyclerview/AutofitRecyclerView.java
public class IconRecyclerView extends RecyclerView {
private GridLayoutManager _manager;
private int _columnWidth = -1;
private int _spanCount;
public IconRecyclerView(@NonNull Context context) {
super(context);
init(context, null);
}
public IconRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public IconRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
if (attrs != null) {
int[] attrsArray = {
android.R.attr.columnWidth
};
TypedArray array = context.obtainStyledAttributes(attrs, attrsArray);
_columnWidth = array.getDimensionPixelSize(0, -1);
array.recycle();
}
_manager = new GridLayoutManager(getContext(), 1);
setLayoutManager(_manager);
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
super.onMeasure(widthSpec, heightSpec);
if (_columnWidth > 0) {
_spanCount = Math.max(1, getMeasuredWidth() / _columnWidth);
_manager.setSpanCount(_spanCount);
}
}
public GridLayoutManager getGridLayoutManager() {
return _manager;
}
public int getSpanCount() {
return _spanCount;
}
}
@@ -4,15 +4,15 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class ImportEntriesAdapter extends RecyclerView.Adapter<ImportEntryHolder> {
private List<ImportEntry> _entries;
@@ -47,7 +47,7 @@ public class ImportEntriesAdapter extends RecyclerView.Adapter<ImportEntryHolder
@Override
public void onViewRecycled(@NonNull ImportEntryHolder holder) {
holder.getEntry().setOnCheckedChangedListener(null);
holder.getData().setOnCheckedChangedListener(null);
}
@Override
@@ -15,7 +15,7 @@ public class ImportEntryHolder extends RecyclerView.ViewHolder implements Import
private TextView _accountName;
private CheckBox _checkbox;
private ImportEntry _entry;
private ImportEntry _data;
public ImportEntryHolder(final View view) {
super(view);
@@ -23,20 +23,20 @@ public class ImportEntryHolder extends RecyclerView.ViewHolder implements Import
_issuer = view.findViewById(R.id.profile_issuer);
_accountName = view.findViewById(R.id.profile_account_name);
_checkbox = view.findViewById(R.id.checkbox_import_entry);
view.setOnClickListener(v -> _entry.setIsChecked(!_entry.isChecked()));
view.setOnClickListener(v -> _data.setIsChecked(!_data.isChecked()));
}
public void setData(ImportEntry entry) {
_entry = entry;
public void setData(ImportEntry data) {
_data = data;
Context context = itemView.getContext();
_issuer.setText(!entry.getIssuer().isEmpty() ? entry.getIssuer() : context.getString(R.string.unknown_issuer));
_accountName.setText(!entry.getName().isEmpty() ? entry.getName() : context.getString(R.string.unknown_account_name));
_checkbox.setChecked(entry.isChecked());
_issuer.setText(!_data.getEntry().getIssuer().isEmpty() ? _data.getEntry().getIssuer() : context.getString(R.string.unknown_issuer));
_accountName.setText(!_data.getEntry().getName().isEmpty() ? _data.getEntry().getName() : context.getString(R.string.unknown_account_name));
_checkbox.setChecked(_data.isChecked());
}
public ImportEntry getEntry() {
return _entry;
public ImportEntry getData() {
return _data;
}
@Override
@@ -14,7 +14,7 @@ import androidx.annotation.RequiresApi;
import com.beemdevelopment.aegis.otp.TotpInfo;
public class TotpProgressBar extends ProgressBar {
private int _period = 30;
private int _period = TotpInfo.DEFAULT_PERIOD;
private Handler _handler;
private float _animDurationScale;
@@ -141,6 +141,7 @@ public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializa
this(UUID.randomUUID());
}
@NonNull
public final UUID getUUID() {
return _uuid;
}

Some files were not shown because too many files have changed in this diff Show More