Compare commits

..
238 Commits
Author SHA1 Message Date
Alexander Bakker 0046e8827e Release v3.2
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-09-08 18:41:30 +02:00
Alexander Bakker 5640b8be83 Update translations from Crowdin 2024-09-08 18:28:07 +02:00
Alexander BakkerandGitHub 3bc3448b5c Merge pull request #1468 from michaelschattgen/feature/add-search-behavior
Add preference to change search behavior
2024-09-06 19:36:54 +02:00
Michael SchättgenandGitHub 7472e32b25 Merge pull request #1466 from alexbakker/fix-audit-log-crash
Account for audit log entries that reference deleted entries
2024-09-04 11:31:12 +02:00
Michael Schättgen 3425256c29 Add preference to switch search behavior 2024-08-28 16:32:23 +02:00
Alexander Bakker b92956dece Account for audit log entries that reference deleted entries
This fixes the following crash I noticed in the developer console:

```
Exception java.lang.AssertionError:
  at com.beemdevelopment.aegis.util.UUIDMap.getByUUID (UUIDMap.java:127)
  at com.beemdevelopment.aegis.vault.VaultRepository.getEntryByUUID (VaultRepository.java:229)
  at com.beemdevelopment.aegis.ui.fragments.preferences.AuditLogPreferencesFragment.lambda$onViewCreated$0 (AuditLogPreferencesFragment.java:70)
  at androidx.lifecycle.LiveData.considerNotify (LiveData.java:133)
  at androidx.lifecycle.LiveData.dispatchingValue (LiveData.java:151)
  at androidx.lifecycle.LiveData.setValue (LiveData.java:309)
  at androidx.lifecycle.LiveData$1.run (LiveData.java:93)
  at android.os.Handler.handleCallback (Handler.java:959)
  at android.os.Handler.dispatchMessage (Handler.java:100)
  at android.os.Looper.loopOnce (Looper.java:232)
  at android.os.Looper.loop (Looper.java:317)
  at android.app.ActivityThread.main (ActivityThread.java:8592)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:580)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:878)
```
2024-08-27 23:01:33 +02:00
Michael SchättgenandGitHub 20c5236cfa Merge pull request #1467 from alexbakker/trim-search
Trim spaces from the search filter
2024-08-27 22:18:46 +02:00
Alexander Bakker 7e1daf731f Trim spaces from the search filter 2024-08-27 22:05:25 +02:00
Michael SchättgenandGitHub 2add8aab14 Merge pull request #1464 from alexbakker/shapeableimageview
Replace CircleImageView with ShapeableImageView
2024-08-27 20:49:38 +02:00
Alexander Bakker 99e633d61a Replace CircleImageView with ShapeableImageView 2024-08-27 20:02:06 +02:00
Michael SchättgenandGitHub 7ce72e046a Merge pull request #1465 from alexbakker/vendor-jcenter-libs
Vendor TextDrawable and TrustedIntents
2024-08-26 23:55:33 +02:00
Alexander Bakker 991da65af0 Vendor TextDrawable and TrustedIntents
These were the only two libraries we were still getting from JCenter,
which was permanently shut down recently: https://jfrog.com/blog/jcenter-sunset/
2024-08-26 23:06:09 +02:00
Alexander BakkerandGitHub 9eae773efb Merge pull request #1458 from michaelschattgen/fix/hidden-dots-size
Fix sizing inconsistency of the dots in hidden view
2024-08-23 23:08:35 +02:00
Michael Schättgen 4ddc42ea51 Fix sizing inconsistency of the dots in hidden view 2024-08-22 01:05:35 +02:00
Alexander BakkerandGitHub a46c816167 Merge pull request #1447 from michaelschattgen/feature/hide-account-name-tiles
Add ability to hide account name in tiles mode
2024-08-12 22:30:44 +02:00
Michael Schättgen 71c0ad2a08 Add ability to hide account name in tiles mode 2024-08-12 21:53:55 +02:00
Michael SchättgenandGitHub bc5cb488f5 Merge pull request #1454 from alexbakker/pass-popup-check
Add an extra check before showing the password reminder popup
2024-08-11 18:07:22 +02:00
Alexander Bakker 010e2628e8 Add an extra check before showing the password reminder popup
This is another attempt to fix a rare crash we're seeing in the
developer console:

```
Exception android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
  at android.view.ViewRootImpl.setView (ViewRootImpl.java:1423)
  at android.view.WindowManagerGlobal.addView (WindowManagerGlobal.java:408)
  at android.view.WindowManagerImpl.addView (WindowManagerImpl.java:148)
  at android.widget.PopupWindow.invokePopup (PopupWindow.java:1583)
  at android.widget.PopupWindow.showAsDropDown (PopupWindow.java:1430)
  at android.widget.PopupWindow.showAsDropDown (PopupWindow.java:1386)
  at com.beemdevelopment.aegis.ui.AuthActivity.lambda$showPasswordReminder$5 (AuthActivity.java:253)
  at android.os.Handler.handleCallback (Handler.java:942)
  at android.os.Handler.dispatchMessage (Handler.java:99)
  at android.os.Looper.loopOnce (Looper.java:211)
  at android.os.Looper.loop (Looper.java:300)
  at android.app.ActivityThread.main (ActivityThread.java:8294)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:580)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1028)
```
2024-08-10 13:13:03 +02:00
Alexander Bakker db4c738c8f Update dependencies 2024-08-09 19:49:08 +02:00
Michael SchättgenandGitHub e79c2c174b Merge pull request #1444 from alexbakker/scroll-export-dialog
Make the export dialog scrollable
2024-08-02 22:56:27 +02:00
Alexander Bakker 655881e852 Make the export dialog scrollable
Reported by @valentinb102 on Matrix.
2024-08-02 19:43:47 +02:00
Alexander BakkerandGitHub 29eccaf9cf Merge pull request #1429 from r3dh3ck/fix/preferences_result_location
Remove preferences result
2024-07-23 21:08:27 +02:00
r3dh3ckandAlexander Bakker f796e4542a Remove preferences result 2024-07-23 20:52:59 +02:00
Alexander Bakker a10693e79e Expand the number of cases covered under the slot exclusion tests 2024-07-23 20:30:36 +02:00
Alexander BakkerandGitHub b76e7a369c Merge pull request #1424 from r3dh3ck/fix/biometric_slot_stripping
Strip a biometric slot when a backup is made
2024-07-23 20:26:16 +02:00
Alexander Bakker 4ea19a2b7a Don't enforce backup versioning if versionsToKeep <= 0 2024-07-23 20:12:07 +02:00
Alexander BakkerandGitHub 6a67ca43e4 Merge pull request #1433 from r3dh3ck/feature/infinite_backups
Infinite backups
2024-07-23 20:11:41 +02:00
r3dh3ck fc8cdc6502 Implement infinite backups 2024-07-23 06:08:52 +00:00
Alexander BakkerandGitHub 27a723205e Merge pull request #1437 from michaelschattgen/fix/duplicate-dialog
Fix showing duplicate time sync dialogs
2024-07-22 20:59:55 +02:00
Michael Schättgen 7d76be577d Fix showing duplicate time sync dialogs 2024-07-22 20:08:48 +02:00
r3dh3ck 62f25d9ae9 Strip a biometric slot when a backup is made 2024-07-22 17:53:04 +00:00
Alexander BakkerandGitHub 9d374a2098 Merge pull request #1435 from michaelschattgen/fix/i18n-auditlog
Fix datetime parsing in Audit Log
2024-07-22 19:48:54 +02:00
Michael Schättgen e53688d30d Fix datetime parsing in Audit Log 2024-07-22 19:04:34 +02:00
Michael SchättgenandGitHub ec237ecb4e Merge pull request #1349 from InfiniteCoder06/feature-multi-group
Support for Adding Multiple Groups
2024-07-22 00:36:54 +02:00
Michael Schättgen 8960ffffb4 Release v3.1.1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-07-22 00:17:19 +02:00
Praveen Kumar 2e9efa0617 Support for Adding Multiple Groups 2024-07-21 19:35:36 +05:30
Alexander BakkerandGitHub 2050d29236 Merge pull request #1393 from michaelschattgen/feature/hide-keyboard-on-scroll
Hide keyboard on scroll when search field is focused
2024-07-20 10:09:12 +02:00
Michael Schättgen c1ffe4a23e Hide keyboard on scroll when search field is focused 2024-07-19 23:57:54 +02:00
Michael SchättgenandGitHub 52f8c709b5 Merge pull request #1432 from alexbakker/fix-1417
Simplify approach for animating advanced entry settings
2024-07-19 22:59:26 +02:00
Michael SchättgenandGitHub 676a7f603b Merge pull request #1431 from alexbakker/fix-group-chip
Initialize the group chip properly after unlocking Aegis
2024-07-19 22:49:01 +02:00
Alexander Bakker 94d1cc6608 Simplify approach for animating advanced entry settings
This slightly simplifies the approach we use to animate the advanced
entry settings into view, by defaulting its alpha to 0 and setting it to
VISIBLE before the animation starts. That way, we're not dependent on
"animation ended" callbacks that apparently don't fire in all cases.

The XML diff looks a bit scary, but it basically just removes a
wrapping ``RelativeLayout`` that appears to not be necessary.
2024-07-19 21:00:22 +02:00
Alexander Bakker f7862dcdf0 Initialize the group chip properly after unlocking Aegis
This fixes an issue introduced by
46e1421c28 where the group chip would not
show after unlocking Aegis. This happened because the activity result is
received *after* ``onStart``. When we were using ``onResume``, it was
the other way around.
2024-07-19 20:28:48 +02:00
Michael Schättgen d1695aa712 Bump versioncode
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
because play store
2024-07-17 22:29:45 +02:00
Michael Schättgen f1ff402db4 Release v3.1.1-beta1 2024-07-17 22:17:35 +02:00
Michael SchättgenandGitHub fd2ae9423e Merge pull request #1430 from alexbakker/workaround-1342
Start auth/intro activities from onStart to work around an Android bug
2024-07-17 21:10:02 +02:00
Alexander Bakker 46e1421c28 Start auth/intro activities from onStart to work around an Android bug 2024-07-17 19:18:09 +02:00
Alexander BakkerandGitHub 359621612a Merge pull request #1420 from juleskers/freeotpplus-json
Clarify FreeOTP+ import needs JSON, not URI-format
2024-07-16 17:04:26 +02:00
Jules Kerssemakers edf2201fb3 Clarify FreeOTP+ import needs JSON, not URI-format 2024-07-15 10:34:34 +02:00
Michael SchättgenandGitHub 1201b505f7 Merge pull request #1408 from juleskers/patch-1
DatabaseImporter: add overlooked FreeOTP "1.x" hint
2024-07-03 19:31:17 +02:00
Jules KerssemakersandGitHub 327f97c51d DatabaseImporter: add overlooked FreeOTP "1.x"-only
Include the '(1.x)' qualifier directly in the import-source selection dropdown to avoid raising false expectations.

See also:
- #1204, where the 1.x-hint was introduced
- #1084: tracking issue for 2.x support
- https://github.com/freeotp/freeotp-android/issues/381
  FreeOTP-issue to reconsider the brittle serialised java format used by 2.x
2024-07-02 11:52:12 +02:00
Michael Schättgen 29ebe31f8a Release v3.1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-06-29 22:34:06 +02:00
Michael Schättgen 372bbaa3fb Release v3.1-beta
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-06-14 12:50:37 +02:00
Michael Schättgen 2165ac4b2b Update translations from Crowdin 2024-06-14 12:01:11 +02:00
Michael SchättgenandGitHub 46ceeeafb9 Merge pull request #1364 from michaelschattgen/fix/edit-entry-scroll
Make EditEntryActivity scrollable again
2024-06-13 21:34:22 +02:00
Michael SchättgenandAlexander Bakker 2b2c4fc0ce Make EditEntryActivity scrollable again
Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2024-06-03 20:21:23 +02:00
Michael SchättgenandGitHub 2864f9b30c Merge pull request #1391 from alexbakker/fix-1329
Save the vault when saving group filter right after a vault version bump
2024-06-02 14:38:01 +02:00
Alexander BakkerandGitHub c17f30b89e Merge pull request #1354 from InfiniteCoder06/bug-scrolling
Fix Scrolling in BottomSheet
2024-06-01 14:43:56 +02:00
Praveen KumarandAlexander Bakker 9c07b981d4 Fix scrolling in bottom sheet for groups 2024-06-01 14:41:21 +02:00
Alexander Bakker 2b69dc3a84 Save the vault when saving group filter right after a vault version bump 2024-05-31 17:02:15 +02:00
Alexander Bakker 892116fcd7 Remove metadata translations 2024-05-31 16:18:03 +02:00
Alexander BakkerandGitHub ee1dd322b8 Merge pull request #1357 from michaelschattgen/feature/audit-log
Add audit log
2024-05-28 20:54:51 +02:00
Alexander BakkerandGitHub 56285eb468 Merge pull request #1352 from InfiniteCoder06/bug-spacing
Fix Spacing Issue With Name
2024-05-28 20:45:37 +02:00
Praveen KumarandAlexander Bakker 7e8b86ecf5 Fix spacing issue with entry name 2024-05-28 20:43:45 +02:00
Alexander BakkerandGitHub ea09c67027 Merge pull request #1383 from NWuensche/master
Update change of Code Digit Grouping
2024-05-28 20:33:12 +02:00
Alexander BakkerandGitHub d6468420ee Merge pull request #1382 from codeall9/bug/entry-list-group-selection
Remember group selection after screen rotate
2024-05-28 20:32:47 +02:00
codeall9andAlexander Bakker 65a57f2b9c Remember group selection after screen rotate
Solution:
Restore _groupFilter as _prefGroupFilter in order to reapply correct filter after screen rotate
2024-05-28 20:31:54 +02:00
nwuensche 40f630170c Update change of Code Digit Grouping 2024-05-18 14:54:49 +02:00
Alexander BakkerandGitHub 2b04ae0622 Merge pull request #1370 from michaelschattgen/fix/icon-packs-assign
Fix icon pack selector in AssignIconsActivity
2024-05-17 14:46:53 +02:00
Alexander BakkerandGitHub c28548debb Merge pull request #1378 from codeall9/bug/setting-appearance-title
Fix Language of Appearance Title
2024-05-17 12:21:29 +02:00
codeall9 802d449bfb Fix AppearancePreferencesFragment title after locale changed
root cause:
the `_prefTitle` is saved in `CharSequence`

solution:
we only able to store `CharSequence` since `Preference.getTitleRes` is removed in AndroidX. As a workaround, we update the title again on Fragment.onStart()
2024-05-12 17:01:11 +08:00
Michael Schättgen 69126242bd Fix icon pack selector in AssignIconsActivity 2024-05-06 17:43:42 +02:00
Michael SchättgenandGitHub dee881bc05 Merge pull request #1347 from InfiniteCoder06/refractor-groups
Renaming of Groups
2024-04-20 11:33:12 +02:00
Michael Schättgen 171da34b13 Start working on audit logs 2024-04-20 01:46:00 +02:00
Praveen Kumar a582c2053c Renaming of Groups 2024-04-19 09:03:03 +05:30
Alexander Bakker 9b96bbde54 Adjust R8 settings for easier retracing of stacktraces in release builds 2024-04-12 19:16:37 +02:00
Michael SchättgenandGitHub 3c124deae1 Merge pull request #1336 from alexbakker/limit-strength-analysis
Stop analyzing password strength if it becomes longer than 64 chars
2024-03-27 23:17:02 +01:00
Alexander Bakker 559e68e0d2 Stop analyzing password strength if it becomes longer than 64 chars
This should help reduce the chance that zxcvbn4j explodes on a password
input.

I also took the opportunity to deduplicate related code a bit.
2024-03-27 15:22:35 +01:00
Alexander Bakker 06437132b5 Update FUNDING.yml 2024-03-25 20:33:49 +01:00
Michael SchättgenandGitHub 8e9a1bda92 Merge pull request #1332 from alexbakker/fix-1330
Use DayNight as the default theme
2024-03-25 20:33:16 +01:00
Alexander Bakker 0d34f0749d Use DayNight as the default theme
This reduces the chance that we flashbang the user when they launch the
app. The issue remains on older Android versions that don't natively
support dark mode, but I don't think that's fixable.

Activities override the theme based on the user's settings, so this
change only has effect while the app is launching.
2024-03-25 20:21:49 +01:00
Michael Schättgen f44fe389d7 Release v3.0.1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-03-25 19:26:55 +01:00
Michael Schättgen 1644b35e87 Fix typos in our featured screenshots 2024-03-25 19:11:17 +01:00
Michael SchättgenandGitHub 49a2b5d34d Merge pull request #1323 from alexbakker/glide-no-res
Don't use Glide to load drawable resources
2024-03-24 20:51:28 +01:00
Michael SchättgenandGitHub 006815d36b Merge pull request #1322 from alexbakker/new-battle-net
Add support for importing from the new Battle.net app
2024-03-24 20:06:22 +01:00
Michael SchättgenandGitHub ec617e0c94 Merge pull request #1326 from alexbakker/fix-1325
Use Android color reference for android:colorBackground in AMOLED theme
2024-03-24 20:00:05 +01:00
Alexander Bakker 3962d50fa6 Use Android color reference for android:colorBackground in AMOLED theme
My best guess is that API 28 and below somehow interpret ``#000000`` to mean
either ``@null`` or transparent for ``android:colorBackground``.
2024-03-24 19:27:36 +01:00
Alexander Bakker 9815e510df Don't use Glide to load drawable resources
Loading drawables using Glide while the size of the ImageView
is not known yet appears to result in a blurry mess.
2024-03-24 17:58:55 +01:00
Alexander Bakker f9f37d30b2 Release v3.0
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-03-24 16:47:26 +01:00
Alexander Bakker 4c4acf0cd0 Update translations from Crowdin 2024-03-24 16:42:26 +01:00
Alexander Bakker 3a66851df5 Only fetch 2 specific dependencies from JCenter 2024-03-24 14:26:59 +01:00
Alexander Bakker 4311bd9bd8 Add support for importing from the new Battle.net app 2024-03-23 18:43:02 +01:00
Michael SchättgenandGitHub 09c789b250 Merge pull request #1321 from alexbakker/about-libraries
Switch to AboutLibraries for the third-party license list
2024-03-23 16:03:25 +01:00
Alexander Bakker 60c72d48ee Switch to AboutLibraries for the third-party license list
The previous library we were using is unmaintained and can't be
customized to match the Material 3 theme.
2024-03-23 13:41:02 +01:00
Alexander BakkerandGitHub 8001ecb482 Merge pull request #1320 from michaelschattgen/fix/padding-unlock-button
Fix padding unlock button
2024-03-20 21:34:59 +01:00
Michael Schättgen 8912d75870 Fix padding unlock button 2024-03-20 21:33:15 +01:00
Michael SchättgenandGitHub e70499bcff Merge pull request #1319 from alexbakker/amoled-code-color
Make the code color white for AMOLED
2024-03-20 21:32:09 +01:00
Alexander BakkerandMichael Schättgen 4c28bf2a12 Make the code color white for AMOLED
Co-authored-by: Michael Schättgen <michael@schattgen.me>
2024-03-20 21:21:25 +01:00
Michael SchättgenandGitHub 584b0acb2b Merge pull request #1318 from alexbakker/no-compact-divider
Fix various minor inconsistencies in entry list item offsets
2024-03-19 22:50:13 +01:00
Alexander Bakker 45ced0de60 Fix various minor inconsistencies in entry list item offsets
This patch addresses the following:
- More consistent offsets between entries in the list, especially in
  relation to the action bar and the error card.
- Consistent correct application of card shapes when switching between
  favoriting and unfavoriting entries.
- Removal of CompactDividerDecoration. We no longer uses dividers, so
  this is no longer needed.
2024-03-19 22:40:47 +01:00
Michael Schättgen 7ec786231e Update featured screenshots with material 3 refresh 2024-03-17 23:23:44 +01:00
Alexander BakkerandGitHub 9737c85f86 Merge pull request #1312 from michaelschattgen/fix/favorites-ui
Fix shape of favorited entries
2024-03-16 17:40:29 +01:00
Michael Schättgen bf7c60d620 Fix shape of favorited entries 2024-03-16 17:38:56 +01:00
Alexander BakkerandGitHub 6fc9cd5a71 Merge pull request #1310 from michaelschattgen/feature/last-used
Add ability to sort based on last used timestamp
2024-03-16 16:15:26 +01:00
Michael Schättgen 9bae4d6bbc Add ability to sort based on last used timestamp 2024-03-16 16:14:47 +01:00
Alexander Bakker 7ce43a0afd Update build pipeline to resolve deprecation warnings 2024-03-16 13:08:21 +01:00
Alexander BakkerandGitHub 2b2cac1ada Merge pull request #1308 from michaelschattgen/fix/ui-inconsistencies
Fix a couple UI inconsistencies
2024-03-16 11:28:02 +01:00
Alexander BakkerandGitHub 2f18907ce1 Merge pull request #1307 from michaelschattgen/fix/copied-text
Fix copied text visibility
2024-03-16 11:24:22 +01:00
Michael SchättgenandGitHub ec01a4a96d Merge pull request #1240 from GitGitro/master
Add delta-aegis-icons to the Readme
2024-03-15 22:46:55 +01:00
GitGitro 76f6ebd216 Add delta-aegis-icons to the Readme
fix: remove preview
2024-03-15 22:35:11 +01:00
Michael Schättgen f1e14e6645 Fix a couple UI inconsistencies 2024-03-15 22:12:55 +01:00
Michael Schättgen 52ecf12576 Fix copied text visibility 2024-03-15 21:34:59 +01:00
Michael SchättgenandGitHub dfd720b406 Merge pull request #1303 from alexbakker/amoled-dynamic
Apply dark background colors when combining AMOLED and dynamic colors
2024-03-14 20:38:31 +01:00
Alexander Bakker 8995626d16 Apply dark background colors when combining AMOLED and dynamic colors
Previously, the dark background colors would not be applied for this
combination of settings.

Unfortunately, I couldn't find a way to avoid some duplication in
themes.xml.
2024-03-14 20:27:43 +01:00
Michael SchättgenandGitHub cffe15735c Merge pull request #1302 from alexbakker/fix-cam-btn-color
Set the correct color for the camera switch button
2024-03-14 20:16:18 +01:00
Michael SchättgenandGitHub 8512986a6b Merge pull request #1301 from alexbakker/fix-1300
Use MaterialColors.getColor instead of our own helper
2024-03-14 20:01:53 +01:00
Alexander Bakker c5b8ee9215 Set the correct color for the camera switch button 2024-03-13 20:50:07 +01:00
Alexander Bakker 2e44a81c69 Use MaterialColors.getColor instead of our own helper
I set CompactDividerDecoration to transparant, because that was already
effectively the case. I think we can remove this class entirely, but
I'll do that in a separate PR.
2024-03-13 20:33:09 +01:00
Alexander Bakker f76d84ef87 Release v3.0-beta1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-03-13 17:21:14 +01:00
Alexander Bakker 3d59114230 Update translations from Crowdin 2024-03-13 16:56:54 +01:00
Michael SchättgenandGitHub 22c9ab7c03 Merge pull request #1295 from alexbakker/intro-init-crash
Don't initialize VaultManager after the intro unless saving succeeds
2024-03-13 16:51:39 +01:00
Michael SchättgenandGitHub f8ad3d16fc Merge pull request #1297 from alexbakker/entry-move-anim
Restore entry list item animations
2024-03-13 16:40:41 +01:00
Michael SchättgenandGitHub fbd3bf3ff5 Merge pull request #1296 from alexbakker/shown-entries-bold
Only bold number of shown entries if found in the translated string
2024-03-13 16:39:30 +01:00
Alexander Bakker 8bbbe3611a Don't initialize VaultManager after the intro unless saving succeeds
In rare cases where writing to disk fails after the intro, a crash could
occur if the user presses "Done" again. VaultManager would have been
initialized, and trying to initialize it again would result in a crash.
2024-03-13 16:36:09 +01:00
Michael SchättgenandGitHub 2d0e201060 Merge pull request #1294 from alexbakker/load-vaultfile
Load vault file on demand instead of juggling it around in-memory
2024-03-13 16:29:01 +01:00
Michael SchättgenandGitHub b59350337f Merge pull request #1293 from alexbakker/disable-unlock-button
Disable the unlock button until the slot decryption task is done
2024-03-13 16:27:44 +01:00
Michael SchättgenandGitHub 6d73e5101c Merge pull request #1278 from alexbakker/fix-1077
Pass down the root shell to every SuFile for the Authy importer
2024-03-13 16:24:22 +01:00
Michael SchättgenandGitHub d16d56c4b0 Merge pull request #1263 from alexbakker/icon-suggestion-prio
Prioritize normal icon issuer matches over inverse matches
2024-03-13 16:23:55 +01:00
Alexander BakkerandGitHub 59bae27556 Merge pull request #1227 from alexbakker/material3
Material 3
2024-03-13 16:07:00 +01:00
Alexander BakkerandMichael Schättgen fcde086ae3 Material 3
Co-authored-by: Michael Schättgen <michael@schattgen.me>
2024-03-13 16:03:56 +01:00
Alexander Bakker 0e2fa929e6 Restore entry list item animations
This fixes an issue where the entry list items no longer animated upon
move, insert, delete, etc.

RecyclerView's DefaultItemAnimator automatically scales the animations
according to the user's settings.

Introduced in 9ff8efab69
2024-03-10 22:18:15 +01:00
Alexander Bakker 8951c19581 Only bold number of shown entries if found in the translated string
This should fix the following crash:

```
Exception java.lang.IndexOutOfBoundsException: setSpan (-1 ... 0) starts before 0
  at android.text.SpannableStringInternal.checkRange (SpannableStringInternal.java:499)
  at android.text.SpannableStringInternal.setSpan (SpannableStringInternal.java:199)
  at android.text.SpannableStringInternal.setSpan (SpannableStringInternal.java:186)
  at android.text.SpannableString.setSpan (SpannableString.java:60)
  at com.beemdevelopment.aegis.ui.views.EntryAdapter$FooterView.refresh (EntryAdapter.java:596)
```
2024-03-10 20:43:59 +01:00
Alexander Bakker 32e462bdce Load vault file on demand instead of juggling it around in-memory
This trades performance for making VaultManager a bit easier to reason
about.

This also fixes a rare crash that could occur if the user retries to unlock
the app after the previous attempt resulted in an error related to
parsing the vault. The vault file would no longer be present in memory
after the first attempt, causing the second attempt to crash the app.
2024-03-10 19:43:40 +01:00
Alexander Bakker 6bd8521661 Disable the unlock button until the slot decryption task is done
This prevents a crash that could occur when double tapping the Unlock
button.
2024-03-10 18:29:49 +01:00
Alexander Bakker f7bac4331e Run the instrumented tests on Ubuntu since KVM is now available
See: https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
2024-03-02 14:43:15 +01:00
Alexander BakkerandGitHub 243a52ebed Merge pull request #1286 from Granddave/feature/update-vault-docs
Update vault documentation
2024-03-01 13:27:07 +01:00
David IsakssonandAlexander Bakker f91b6f0466 Update vault documentation
Here some changes to the vault documentation are made. The documentation
is updated to reflect the latest versions of both the vault and the
database, i.e. vault version 1 and database version 3.

Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2024-03-01 13:22:50 +01:00
Alexander Bakker 57ec695718 Pass down the root shell to every SuFile for the Authy importer
The issue was introduced in: 69f0bb4fbc
2024-02-18 20:03:37 +01:00
Michael SchättgenandGitHub 224ec2553c Merge pull request #1262 from alexbakker/glide-caching
Use the hash of entry icons as keys for Glide caching
2024-02-01 22:47:39 +01:00
Michael SchättgenandGitHub 5acacf63e1 Merge pull request #1249 from alexbakker/2fas-schema4
Add support for importing 2FAS schema v4 backups
2024-02-01 22:06:23 +01:00
Alexander Bakker bfbb3ef2c4 Prioritize normal icon issuer matches over inverse matches
Icon packs may have very generic issuers for their icons (like [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons)).
For example, this causes the icon assigning view to suggest the "C" icon for every
entry that contains a "c".

This patch addresses that by giving inverse matches (where the entry
issuer contains the icon issuer) a lower position in the suggested icons
list.
2024-01-20 14:25:17 +01:00
Alexander Bakker f1c9c6c5fc Use the hash of entry icons as keys for Glide caching
This is mostly a cleanup of the way we do Glide in-memory caching. It
also fixes a few minor issues along the way:

- Entry icon cache keys were based on entry UUID's. This could cause
  problems when changing an entry's icon.
- A TextDrawable could get replaced by the icon of a different entry
  when scrolling through the entry list quickly.
2024-01-18 23:55:16 +01:00
Michael SchättgenandGitHub 566bcac3e0 Merge pull request #1236 from alexbakker/steam-xposed
Add support for importing decrypted Steam JSON blob
2024-01-09 23:03:05 +01:00
Alexander BakkerandGitHub 4d729d1bef Merge pull request #1204 from ranjeetchouhan/master
feat: Update references to FreeOTP and add version hint "1.x"
2023-12-27 18:03:57 +01:00
RanjeetandAlexander Bakker 1acb9db489 feat: Update references to FreeOTP and add version hint "1.x"
Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2023-12-27 18:00:21 +01:00
Alexander Bakker 98bcdc7615 Update Gradle and dependencies 2023-12-27 17:51:56 +01:00
Alexander Bakker 7c1a954e4d Stop using deprecated startActivityAndCollapse(Intent) 2023-12-27 17:51:53 +01:00
Alexander BakkerandGitHub a1d00b47fe Merge pull request #1238 from cyb3rko/startactivityforesult-deprecation
Replace deprecated startActivityForResult
2023-12-22 22:39:21 +01:00
Niko Diamadis ca530f229b Replace startActivityForResult with result launchers 2023-12-21 22:57:39 +01:00
Alexander Bakker b86bb286e8 Add support for importing 2FAS schema v4 backups 2023-12-18 22:55:30 +01:00
Alexander Bakker 52abb08201 Update dependencies 2023-12-17 17:42:16 +01:00
Alexander Bakker ff233090f8 Add support for importing decrypted Steam JSON blob
Some people have managed to snatch the OTP details from Steam using
Xposed while it is being decrypted by the app. Aegis still won't be
able to do the decryption part, but we can add support for importing
the decrypted JSON blob, which only differs slightly from the old
format.
2023-11-30 21:01:27 +01:00
Alexander BakkerandGitHub adaae9e6d6 Merge pull request #1234 from michaelschattgen/feature/issuer-sort-account-fallback
Improve issuer and account sorting
2023-11-29 23:24:19 +01:00
Michael SchättgenandGitHub 3dd70de5df Merge pull request #1233 from alexbakker/explain-uri-perms
Explain vault backup permission error
2023-11-29 21:26:18 +01:00
Michael Schättgen da2244f511 Improve issuer and account sorting 2023-11-29 20:57:48 +01:00
Alexander Bakker 08c73922cc Explain vault backup permission error
Users understandably get confused by the "No persisted URI permissions"
error. This patch adds some text to the dialog explaining why this
happened and how the user can fix the issue.

This permission issue can happen for one of two reasons:
- The user made a change to the backup destination (renamed, moved,
  deleted, etc)
- Aegis was restored from an Android backup
2023-11-29 20:09:37 +01:00
Michael SchättgenandGitHub 88caafd61c Merge pull request #1232 from jsoberg/jsoberg/1231/fixing-configuration-change-licensedialog-crash
#1231 - Fix crash in License and Changelog dialogs on configuration change
2023-11-29 14:44:23 +01:00
Joshua Soberg 45220241aa #1231 - Use public constructors for License/Changelog dialog fragments so that they can be recreated on configuration change 2023-11-28 19:04:18 -05:00
Alexander Bakker 60e93559c3 Bump target SDK version and update dependencies 2023-11-07 20:29:45 +01:00
Alexander BakkerandGitHub e1f4696115 Merge pull request #1200 from michaelschattgen/feature/select-all
Add ability to select all tokens
2023-09-24 18:08:30 +02:00
Michael Schättgen 1c86c5fd51 Add ability to select all tokens 2023-09-24 17:12:37 +02:00
Michael SchättgenandGitHub 92e9e047a7 Merge pull request #1192 from alexbakker/agp-migration
Transition to non-final resource IDs and non-transitive R classes
2023-09-20 22:32:48 +02:00
Alexander Bakker c13d4e7f8d Transition to non-final resource IDs and non-transitive R classes
Future versions of AGP will force us to do this, so we might as well get
it over with now.
2023-09-19 23:34:08 +02:00
Michael SchättgenandGitHub d09e81232a Merge pull request #1190 from alexbakker/fix-assign-icons-menu
Introduce a separate menu for AssignIconsActivity
2023-09-19 21:01:24 +02:00
Alexander Bakker 03f1a0e8ab Introduce a separate menu for AssignIconsActivity
Apparently this was using ``menu_groups``, probably a copy-paste error.

This also moves ``AssignIconsActivity`` to the right package.
2023-09-18 22:31:38 +02:00
Alexander BakkerandGitHub 305e157fc5 Merge pull request #1078 from orange-elephant/entries-in-multiple-groups
Refer to groups by UUID
2023-09-11 22:34:14 +02:00
elenaandAlexander Bakker 5c86e5c099 Refer to groups by UUID
- Also lays the foundations for adding entries to multiple groups and changing group names

Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2023-09-11 22:28:53 +02:00
Michael SchättgenandGitHub 0760bfc618 Merge pull request #1188 from alexbakker/fix-anim-issues
Fix two issues related to animation duration scale
2023-09-11 21:34:33 +02:00
Alexander BakkerandGitHub 9414b5c420 Merge pull request #1172 from michaelschattgen/feature/assign-icons
Add ability to automatically assign icons to (imported) entries
2023-09-11 21:07:23 +02:00
Alexander Bakker e7a1058618 Fix two issues related to animation duration scale
This patch addresses two issues:
- The entry selection icon would flicker when a non-1x animator
  duration scale was set.
- The advanced entry field animation was not shown if the animator
  duration scale was set to .5x, due to a rounding error.

Introduced in: 9ff8efab69
2023-09-11 21:05:20 +02:00
Michael Schättgen 1a6f85ccb6 Add ability to assign icons
More progress

Open IconPicker dialog on click

Add ability to reset

Fix changing icons

Cleanup

Add ability to assign icons after import

PR fixes
2023-09-10 12:14:57 +02:00
Michael SchättgenandGitHub b84ecf15da Merge pull request #1184 from alexbakker/no-nested-recyclerview
Never wrap RecyclerView with a NestedScrollView
2023-09-10 00:32:41 +02:00
Alexander Bakker 31b8162ab4 Use 'comment' instead of 'context' to add context to strings 2023-09-09 22:09:03 +02:00
Alexander Bakker 7def7eb4f7 Remove unused strings and add context to a couple of strings
Most of these were related to slots. Also removed the card_slot layout.
2023-09-09 21:59:31 +02:00
Alexander Bakker 8ca45d2322 Fix singular form of the import_error_dialog string 2023-09-09 21:37:34 +02:00
Michael SchättgenandGitHub 1c1dee560c Merge pull request #1182 from alexbakker/clarify-intro-import
Clarify that only Aegis vaults can be imported during the intro
2023-09-09 21:05:53 +02:00
Alexander Bakker ca4a3e2f74 Never wrap RecyclerView with a NestedScrollView
Wrapping a ``RecyclerView`` with a ``NestedScrollView`` breaks its recycling
functionality because the view height is stretched to fit the full list
of entries.

We never noticed performance issues in these two cases because these
lists never get very long. Let's fix these cases anyway so that we
don't accidentally base a new use of a ``RecyclerView`` on this broken
pattern.

Also renamed ``list_slots`` to ``list_groups``. Must have been
a copy-paste error.
2023-09-09 18:37:07 +02:00
Alexander Bakker 37964da4a5 Clarify that only Aegis vaults can be imported during the intro
Some users understandably get confused when they try to import a backup
file from a different 2FA app during the intro and then get greeted
with an error dialog.

This changes the button text to "Import Aegis vault" and adds a small
hint text in the hope that this makes the limitations of the intro more
clear to the user.

<img width="200" src="https://alexbakker.me/u/jzhh3bk30w.png" />
2023-09-09 12:51:04 +02:00
Alexander Bakker dd9c307dea Release v2.2.2
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2023-09-09 12:15:32 +02:00
Alexander Bakker c65ecd9c54 Update translations from Crowdin 2023-09-09 12:08:06 +02:00
Michael SchättgenandGitHub 72511fc02b Merge pull request #1180 from alexbakker/fix-tile-crash
Check for null returned by getQsTile()
2023-09-08 00:26:47 +02:00
Michael SchättgenandGitHub 79ade74c0c Merge pull request #1179 from alexbakker/icon-name
Introduce optional 'name' field for iconpack icons
2023-09-08 00:26:15 +02:00
Michael SchättgenandGitHub 8164e91dd0 Merge pull request #1178 from alexbakker/fix-auth-pro
Add support for new Authenticator Pro backup format
2023-09-07 23:59:28 +02:00
Alexander Bakker 1ccbe88ce6 Check for null returned by getQsTile()
Apparently ``getQsTile()`` can return null, which resulted in a crash.
Reported through the Google Play Console:

```
Exception java.lang.NullPointerException: Attempt to invoke virtual method 'void android.service.quicksettings.Tile.setState(int)' on a null object reference
  at com.beemdevelopment.aegis.services.LaunchAppTileService.onStartListening
  at android.service.quicksettings.TileService$H.handleMessage (TileService.java:488)
  at android.os.Handler.dispatchMessage (Handler.java:106)
  at android.os.Looper.loopOnce (Looper.java:205)
  at android.os.Looper.loop (Looper.java:294)
  at android.app.ActivityThread.main (ActivityThread.java:8177)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:552)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:971)
```
2023-09-07 23:51:01 +02:00
Alexander Bakker 9b3e7136bd Introduce optional 'name' field for iconpack icons
This introduces a new (optional) 'name' field for iconpack icons. It
will be used to describe the icon in the icon selection dialog. If it is
not present, the name of the icon will be derived from the filename,
like before. Using this new field allows usage of more exotic characters
in the icon name that are not allowed in a filename.
2023-09-07 22:49:52 +02:00
Alexander Bakker 9cabd9f309 Add support for new Authenticator Pro backup format
This adds support for Authenticator Pro's latest backup format changes.
The format of the content itself has not changed as far as I can tell, but
they do use a different cipher and KDF now: AES GCM and Argon2id,
respectively.

The memory cost is statically set at 64MiB. I suspect that this may
cause OOM situations on some lower-end devices, but we'll see, not much
we can do about that right now without making more changes.
2023-09-07 22:30:22 +02:00
Michael Schättgen 27e56d60b5 Release v2.2.1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2023-09-06 23:49:51 +02:00
Michael SchättgenandGitHub aac77442bf Merge pull request #1175 from michaelschattgen/hotfix/biometrics-unlock
Fix biometrics unlock button on AuthActivity
2023-09-06 23:43:50 +02:00
Michael Schättgen c9cf6729e0 Fix biometrics unlock button on AuthActivity 2023-09-06 23:39:18 +02:00
Alexander BakkerandGitHub b916697391 Merge pull request #1171 from michaelschattgen/feature/import-duplicates
Add ability to skip duplicates during import
2023-09-06 12:49:23 +02:00
Michael Schättgen b205438982 Add ability to skip duplicates during import 2023-09-06 12:40:01 +02:00
Michael Schättgen 4f83706f73 Release v2.2
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2023-09-05 23:12:55 +02:00
Michael Schättgen 44bb11e2d8 Update translations from Crowdin 2023-09-05 22:54:29 +02:00
Michael SchättgenandGitHub 92b31c3eda Reference to new icon pack in readme 2023-09-03 18:58:15 +02:00
Alexander Bakker 2793b65786 Fix versions of two build dependencies 2023-08-31 23:46:03 +02:00
Michael Schättgen 5e35cc203f Fix bug in Japanese translation 2023-08-30 22:58:32 +02:00
Michael Schättgen 67d7f7a75d Release v2.2-beta1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2023-08-30 21:37:56 +02:00
Michael Schättgen 18e6fd2561 Update translations from Crowdin 2023-08-30 21:09:24 +02:00
Alexander BakkerandGitHub c19692b416 Merge pull request #1165 from michaelschattgen/feature/disable-animations
Respect system animation setting
2023-08-30 15:17:26 +02:00
Michael SchättgenandAlexander Bakker 9ff8efab69 Respect system animation setting
Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2023-08-30 15:07:09 +02:00
Alexander Bakker f38b6ec586 Fix the build by updating to JDK 17 2023-08-30 14:45:21 +02:00
Alexander Bakker 3278d6544b Update dependencies 2023-08-30 14:26:12 +02:00
Alexander Bakker 366c039aff Add example Crowdin CLI config file 2023-08-30 14:00:54 +02:00
Michael Schättgen 8917bb3b94 Fix the spacing between issuer and account name 2023-08-23 18:45:39 +02:00
Alexander BakkerandGitHub 3e352156c4 Merge pull request #1096 from beemdevelopment/feature/view-mode-tiles
Add tiles view mode
2023-08-23 17:31:41 +02:00
Michael SchättgenandAlexander Bakkker d90303cf0e Add tiles view mode
Minor UI improvements
Fix animations
Fix typo
Improvements made after PR review
PR improvements

Co-authored-by: Alexander Bakkker <ab@alexbakker.me>
2023-08-23 17:29:18 +02:00
Alexander BakkerandGitHub 94a38e82e4 Merge pull request #1167 from michaelschattgen/feature/hide-account-name
Add ability to only show names when necessary
2023-08-22 23:26:20 +02:00
Michael Schättgen edb1d8d76f Add ability to only show names when necessary 2023-08-21 00:24:10 +02:00
Alexander BakkerandGitHub 6009c09607 Merge pull request #1161 from beemdevelopment/feature/copy-behavior
Add ability to change copy behavior
2023-08-20 16:51:30 +02:00
Michael Schättgen 3ff242e6b6 Add ability to change copy behavior 2023-08-20 15:51:32 +02:00
Michael Schättgen 51897a4ab7 Update featured screenshots in README.md
this was well needed
2023-08-20 13:05:26 +02:00
Alexander BakkerandGitHub 84abffb2ad Merge pull request #1163 from michaelschattgen/feature/password-reminder-dialog
Add dialog when password reminder is active
2023-08-20 10:43:36 +02:00
Michael Schättgen ef069e49af Add dialog when password reminder is activated 2023-08-18 10:46:08 +02:00
Michael SchättgenandGitHub d3c48848be Merge pull request #1162 from michaelschattgen/feature/account-name-position
Add ability to change account name position
2023-08-17 02:20:17 +02:00
Michael Schättgen 813fd62dc7 Add ability to change account name position 2023-08-16 23:40:19 +02:00
Michael SchättgenandGitHub 8c81ec0739 Merge pull request #1140 from nerodevo/master
Remove unwanted character
2023-08-16 01:47:58 +02:00
Michael SchättgenandGitHub b08a2f5f93 Fix minor typo in FAQ
Closes #1158
2023-08-09 00:31:46 +02:00
nerodevo 3026a3eab1 Remove unwanted character 2023-06-04 06:38:04 -05:00
Alexander Bakker efd8e2d9ff Run key derivation for Authenticator Pro importer on background thread 2023-03-03 23:04:33 +01:00
Alexander Bakker 58b8edf318 Various minor touchups for the Authenticator Pro importer 2023-03-03 21:44:21 +01:00
Alexander BakkerandGitHub abe7af1549 Merge pull request #1080 from NepNep21/authpro
Add Authenticator Pro encrypted import support, fixes #1035
2023-03-03 20:53:17 +01:00
NepNep21 289d5409a5 Add Authenticator Pro encrypted import support 2023-03-02 17:46:45 -03:00
Alexander Bakker e906131f1b Disable Glide notification permission lint check
Fixes the build
2023-03-01 14:38:29 +01:00
Alexander Bakker b6bfc5b15f Update Gradle and dependencies 2023-03-01 13:49:01 +01:00
Alexander BakkerandGitHub 90f2ea79d9 Merge pull request #1100 from beemdevelopment/bugfix/empty-state
Fix empty state while using search filter
2023-03-01 12:11:23 +01:00
Michael Schättgen 3c9e5a9fdb Fix empty state while using search filter 2023-02-27 23:49:35 +01:00
Alexander Bakker f0f9653ace Release v2.1.3
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2023-02-27 23:19:04 +01:00
Alexander Bakker e2df97514c Update translations from Crowdin 2023-02-27 23:11:07 +01:00
Alexander BakkerandGitHub ed5ac4730f Merge pull request #1093 from beemdevelopment/feature/backup-reminder-disable
Add ability to disable backup reminder
2023-02-27 22:39:01 +01:00
Michael Schättgen 61d41a26fa Add ability to disable backup reminder 2023-02-05 18:31:31 +01:00
Alexander Bakker fd42c5c893 Update build pipeline to resolve deprecation warnings 2022-12-27 22:19:56 +01:00
Alexander BakkerandGitHub 0eccc875b0 Merge pull request #1067 from orange-elephant/export-dialog-ux
Only enable export buttons if options are in a valid state
2022-12-24 14:47:16 +01:00
elena d3e5472ef2 Only enable export buttons if options are in a valid state 2022-12-20 17:27:46 +00:00
Alexander Bakker bf825df221 Fix crash caused by incorrect Toast creation 2022-12-18 18:39:46 +01:00
Alexander Bakker 6de007e3b1 Fix the build status badge 2022-12-18 18:32:34 +01:00
Alexander BakkerandGitHub 121c1dada9 Merge pull request #1054 from orange-elephant/checkboxes-dropdown
Create checkboxes dropdown component
2022-12-18 18:20:56 +01:00
elena 1c9931b1c8 Display export groups selection as dropdown 2022-12-13 15:22:00 +00:00
elena 51698947aa Create checkboxes dropdown component 2022-12-13 15:20:00 +00:00
Alexander Bakker ceaf52e238 Update dependencies 2022-12-09 22:37:45 +01:00
391 changed files with 20733 additions and 5956 deletions
+10
View File
@@ -0,0 +1,10 @@
project_id: "372633"
api_token: "<api-token-here>"
base_path: "../app/src/main"
base_url: "https://api.crowdin.com"
preserve_hierarchy: true
files:
- source: "res/values/strings.xml"
dest: "strings.xml"
translation: "res/values-%android_code%/%original_file_name%"
+1 -1
View File
@@ -1,4 +1,4 @@
buy_me_a_coffee: beemdevelopment
custom:
- "https://www.buymeacoffee.com/beemdevelopment"
- "https://www.blockchain.com/btc/address/bc1q26kyxqjkc6tu477pzy0whagwhs4ypv93qls22n"
- "https://nanocrawler.cc/explorer/account/nano_1aegisc559b1x4p3839egnu579jkd4htpidy14eo9e31gzqmwuafypnj4q94"
+20 -14
View File
@@ -5,31 +5,37 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b
uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Build the app
run: ./gradlew build
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: apk
path: app/build/outputs/apk/debug/app-debug.apk
test:
runs-on: macos-latest
# This is probably pretty expensive for GitHub, so restrict the repositories that this job runs on
if: github.repository == 'beemdevelopment/Aegis' || github.repository == 'alexbakker/Aegis'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-java@v3
uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '11'
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Install HAXM
run: brew install --cask intel-haxm
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Tests
uses: reactivecircus/android-emulator-runner@e790971012b979513b4e2fe70d4079bc0ca8a1ae
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2
with:
api-level: 31
arch: x86_64
@@ -47,8 +53,8 @@ jobs:
adb logcat -d > artifacts/logcat.txt
cp -r app/build/reports/androidTests/connected/* artifacts/report/
if adb shell '[ -e /sdcard/Pictures/screenshots ]'; then adb pull /sdcard/Pictures/screenshots artifacts/; fi
# test ! -f tests_failing
- uses: actions/upload-artifact@v3
test ! -f tests_failing
- uses: actions/upload-artifact@v4
if: always()
with:
name: instrumented-test-report
+8 -3
View File
@@ -16,7 +16,7 @@ jobs:
security-events: write
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Exclude paths
# The importers are excluded from analysis, because some of the apps Aegis
# can import from don't have such great crypto, which will cause false
@@ -24,13 +24,18 @@ jobs:
run: |
find app/src/main/java/com/beemdevelopment/aegis/importers ! \( -name AegisImporter.java -o -name "DatabaseImporter*" \) -type f -exec rm -f {} +
sed -i '/Importer.class/d' app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: java
- name: Build
run: ./gradlew assembleDebug
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'beemdevelopment/Aegis'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install crowdin-cli
run: |
wget https://github.com/crowdin/crowdin-cli/releases/download/3.7.2/crowdin-cli.zip
+1
View File
@@ -40,3 +40,4 @@ captures/
# Keystore files
*.jks
crowdin.properties
.crowdin/config.yml
+1 -1
View File
@@ -73,7 +73,7 @@ apps probably perform a weaker form of biometric authentication.
### How can I back up my Aegis vault to the cloud automatically?
Aegis can only automatically back up to the cloud of the app of your cloud
Aegis can only automatically back up to the cloud if the app of your cloud
provider is installed on your device and fully participates in the Android
Storage Access Framework. Aegis doesn't have access to the internet and we don't
have plans to change this, so adding support for specific cloud providers in the
+25 -9
View File
@@ -5,9 +5,9 @@ alt="App icon">
<br>
[![CI](https://img.shields.io/github/workflow/status/beemdevelopment/Aegis/build/master)](https://github.com/beemdevelopment/Aegis/actions/workflows/build-app-workflow.yaml?query=branch%3Amaster) [![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/matrix/aegis:matrix.org?color=blue)](https://matrix.to/#/#aegis:matrix.org)
[![Build](https://github.com/beemdevelopment/Aegis/actions/workflows/build-app-workflow.yaml/badge.svg)](https://github.com/beemdevelopment/Aegis/actions/workflows/build-app-workflow.yaml?query=branch%3Amaster) [![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/matrix/aegis:matrix.org?color=blue)](https://matrix.to/#/#aegis:matrix.org)
__Aegis Authenticator__ is a free, secure and open source 2FA app for Android.
**Aegis Authenticator** is a free, secure and open source 2FA app for Android.
It aims to provide a secure authenticator for your online services, while also
including some features missing in existing authenticator apps, like proper
encryption and backups. Aegis supports HOTP and TOTP, making it compatible with
@@ -53,19 +53,20 @@ The security design of the app and the vault format is described in detail in
[<img width=200 alt="Screenshot 1"
src="metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true)
[<img width=200 alt="Screenshot 2"
src="metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true)
src="metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true)
[<img width=200 alt="Screenshot 3"
src="metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true)
src="metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true)
[<img width=200 alt="Screenshot 4"
src="metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true)
[<img width=200 alt="Screenshot 5"
src="metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true)
[<img width=200 alt="Screenshot 6"
src="metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true)
[<img width=200 alt="Screenshot 7"
src="metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true)
[<img width=200 alt="Screenshot 8"
src="metadata/en-US/images/phoneScreenshots/screenshot8.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot8.png?raw=true)
## Downloads
@@ -77,7 +78,7 @@ src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
[<img height="80" alt="Get it on F-Droid"
src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
/>](https://f-droid.org/app/com.beemdevelopment.aegis)
### Verification
APK releases on Google Play and GitHub are signed using the same key. They can
@@ -124,10 +125,20 @@ documentation](docs/iconpacks.md).
[<img width=500 alt="aegis-icons preview"
src="https://raw.githubusercontent.com/aegis-icons/aegis-icons/master/showcase.png">](https://github.com/aegis-icons/aegis-icons)
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons)
- [delta-aegis-icons](https://github.com/Delta-Icons/aegis-icons)
Delta version of the unofficial monochrome-styled 2FA icon pack aegis-icons.
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons) *
This project periodically generates an icon pack for Aegis based on [Simple
Icons](https://simpleicons.org/). The icons are automatically generated, so
Icons](https://simpleicons.org/).
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) *
This is a variant on the aegis-simple-icons pack where the icons contain no solid background and just the outlines are being used.
\* The icons are automatically generated, so
not all of them are as high quality as the ones you'll find in
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
@@ -144,3 +155,8 @@ Swing by our Matrix room to interact with other contributors:
This project is licensed under the GNU General Public License v3.0. See the
[LICENSE](LICENSE) file for details.
A couple of libraries vendored in Aegis' repository are licensed under a
different license:
- [TextDrawable](app/src/main/java/com/amulyakhare/textdrawable)
- [TrustedIntents](app/src/main/java/info/guardianproject/trustedintents)
+68 -51
View File
@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
def getCmdOutput = { cmd ->
def stdout = new ByteArrayOutputStream()
@@ -19,21 +20,27 @@ def fileProviderAuthority = "${packageName}.fileprovider"
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
android {
compileSdkVersion 33
compileSdk 34
namespace packageName
defaultConfig {
applicationId "${packageName}"
minSdkVersion 21
targetSdkVersion 33
versionCode 57
versionName "2.1.2"
targetSdkVersion 34
versionCode 71
versionName "3.2"
multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
buildConfigField "java.util.concurrent.atomic.AtomicBoolean", "TEST", "new java.util.concurrent.atomic.AtomicBoolean(false)"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas"]
}
}
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
@@ -103,11 +110,14 @@ android {
checkDependencies true
disable 'MissingQuantity', 'MissingTranslation'
}
buildFeatures {
buildConfig true
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.8.0'
artifact = 'com.google.protobuf:protoc:3.25.1'
}
generateProtoTasks {
all().each { task ->
@@ -120,38 +130,46 @@ protobuf {
}
}
dependencies {
def cameraxVersion = '1.1.0'
def glideVersion = '4.14.2'
def guavaVersion = '31.1'
def hiltVersion = '2.44'
def junitVersion = '4.13.2'
def libsuVersion = '5.0.3'
aboutLibraries {
// Tasks for aboutLibraries are not run automatically to keep the build reproducible
// To update manually: ./gradlew app:exportLibraryDefinitions -PaboutLibraries.exportPath=src/main/res/raw
prettyPrint = true
configPath = "app/config"
fetchRemoteFunding = false
registerAndroidTasks = false
exclusionPatterns = [~"javax.annotation.*"]
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
}
annotationProcessor 'androidx.annotation:annotation:1.5.0'
dependencies {
def cameraxVersion = '1.3.4'
def glideVersion = '4.16.0'
def guavaVersion = '33.2.1'
def hiltVersion = '2.52'
def junitVersion = '4.13.2'
def libsuVersion = '6.0.0'
def roomVersion = "2.6.1"
annotationProcessor 'androidx.annotation:annotation:1.8.2'
annotationProcessor "androidx.room:room-compiler:$roomVersion"
annotationProcessor "com.google.dagger:hilt-compiler:$hiltVersion"
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.activity:activity:1.6.1'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.activity:activity:1.9.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.camera:camera-camera2:$cameraxVersion"
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
implementation "androidx.camera:camera-view:1.1.0"
implementation 'androidx.cardview:cardview:1.0.0'
implementation "androidx.core:core:1.9.0"
implementation "androidx.camera:camera-view:$cameraxVersion"
implementation 'androidx.core:core:1.13.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation "androidx.lifecycle:lifecycle-process:2.5.1"
implementation ('androidx.preference:preference:1.2.0') {
// See: https://issuetracker.google.com/issues/238425626
exclude group: 'androidx.lifecycle', module:'lifecycle-viewmodel'
exclude group: 'androidx.lifecycle', module:'lifecycle-viewmodel-ktx'
}
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'androidx.lifecycle:lifecycle-process:2.8.4'
implementation "androidx.preference:preference:1.2.1"
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation "androidx.room:room-runtime:$roomVersion"
implementation 'androidx.viewpager2:viewpager2:1.1.0'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation "com.google.dagger:hilt-android:$hiltVersion"
implementation 'com.github.avito-tech:krop:0.52'
@@ -163,36 +181,35 @@ 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.7.0'
implementation 'com.google.protobuf:protobuf-javalite:3.21.4'
implementation 'com.google.zxing:core:3.5.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.7.0'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation 'de.psdev.licensesdialog:licensesdialog:2.2.0'
implementation 'net.lingala.zip4j:zip4j:2.11.2'
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
implementation 'org.bouncycastle:bcprov-jdk18on:1.72'
implementation "org.simpleflatmapper:sfm-csv:8.2.3"
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.google.protobuf:protobuf-javalite:4.27.3'
implementation 'com.google.zxing:core:3.5.3'
implementation('com.mikepenz:aboutlibraries:11.2.2') {
exclude group: 'com.mikepenz', module: 'aboutlibraries-core'
}
implementation 'com.mikepenz:aboutlibraries-core-android:11.2.2'
implementation 'com.nulab-inc:zxcvbn:1.9.0'
implementation 'net.lingala.zip4j:zip4j:2.11.5'
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
implementation 'org.simpleflatmapper:sfm-csv:8.2.3'
androidTestAnnotationProcessor "com.google.dagger:hilt-android-compiler:$hiltVersion"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hiltVersion"
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.0'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.0'
androidTestImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test:runner:1.6.1'
androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
androidTestImplementation "junit:junit:${junitVersion}"
androidTestUtil 'androidx.test:orchestrator:1.4.2'
androidTestUtil 'androidx.test:orchestrator:1.5.0'
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'androidx.test:core:1.6.1'
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
testImplementation "junit:junit:${junitVersion}"
testImplementation 'org.json:json:20220924'
testImplementation 'org.robolectric:robolectric:4.9'
testImplementation 'org.json:json:20240303'
testImplementation 'org.robolectric:robolectric:4.13'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
}
+6
View File
@@ -0,0 +1,6 @@
{
"uniqueId": "com.github.avito-tech:krop",
"licenses": [
"MIT"
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"uniqueId": "com.github.topjohnwu.libsu:.*::regex",
"licenses": [
"Apache-2.0"
]
}
+15
View File
@@ -0,0 +1,15 @@
{
"uniqueId": "com.amulyakhare:com.amulyakhare.textdrawable",
"funding": [
],
"developers": [
],
"artifactVersion": "1.0.1",
"description": "This light-weight library provides images with letter/text like the Gmail app. It extends the Drawable class thus can be used with existing/custom/network ImageView classes. Also included is a fluent interface for creating drawables and a customizable ColorGenerator.",
"name": "textdrawable",
"licenses": [
"MIT"
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"uniqueId": "info.guardianproject.trustedintents:trustedintents",
"funding": [
],
"developers": [
{
"name": "Guardian Project"
}
],
"artifactVersion": "0.2",
"description": "TrustedIntents is a library for flexible trusted interactions between Android apps. It is modeled after Android's `signature` protection level for permissions. The key difference is that the framework allows the trusted signature to be set, rather than requiring to match the current app's signature.",
"scm": {
"connection": "scm:https://github.com/guardianproject/TrustedIntents.git",
"url": "scm:https://github.com/guardianproject/TrustedIntents",
"developerConnection": "scm:git@github.com:guardianproject/TrustedIntents.git"
},
"name": "TrustedIntents",
"website": "https://guardianproject.info/code/trustedintents",
"licenses": [
"3ca920d1875f7ad7ab04a2a331958577"
]
}
@@ -0,0 +1,5 @@
{
"hash": "3ca920d1875f7ad7ab04a2a331958577",
"url": "https://github.com/guardianproject/TrustedIntents/blob/master/LICENSE.txt",
"name": "LGPLv2.1"
}
+3
View File
@@ -3,5 +3,8 @@
<issue id="InvalidPackage">
<ignore regexp="X509LDAPCertStoreSpi" />
</issue>
<issue id="NotificationPermission">
<ignore regexp="com.bumptech.glide.request.target.NotificationTarget" />
</issue>
<issue id="ResourceType" severity="Warning" />
</lint>
+4 -1
View File
@@ -1,7 +1,10 @@
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
-dontobfuscate
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
-keep class com.beemdevelopment.aegis.ui.fragments.preferences.*
-keep class com.beemdevelopment.aegis.importers.** { *; }
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
-dontobfuscate
-dontwarn javax.naming.**
@@ -0,0 +1,52 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "392278bdb797d013cb2ada67a3b1cc60",
"entities": [
{
"tableName": "audit_logs",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `event_type` TEXT NOT NULL, `reference` TEXT, `timestamp` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "_eventType",
"columnName": "event_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "_reference",
"columnName": "reference",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "_timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '392278bdb797d013cb2ada67a3b1cc60')"
]
}
}
@@ -2,15 +2,19 @@ package com.beemdevelopment.aegis;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.matcher.BoundedMatcher;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.ui.views.EntryHolder;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultManager;
@@ -20,6 +24,7 @@ import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vectors.VaultEntries;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Rule;
@@ -178,4 +183,21 @@ public abstract class AegisTest {
}
};
}
@NonNull
protected static Matcher<RecyclerView.ViewHolder> withOtpType(Class<? extends OtpInfo> otpClass) {
return new BoundedMatcher<RecyclerView.ViewHolder, EntryHolder>(EntryHolder.class) {
@Override
public boolean matchesSafely(EntryHolder holder) {
return holder != null
&& holder.getEntry() != null
&& holder.getEntry().getInfo().getClass().equals(otpClass);
}
@Override
public void describeTo(Description description) {
description.appendText(String.format("with otp type '%s'", otpClass.getSimpleName()));
}
};
}
}
@@ -1,13 +1,13 @@
package com.beemdevelopment.aegis;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
import static androidx.test.espresso.action.ViewActions.clearText;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.pressBack;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
@@ -18,19 +18,17 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import androidx.annotation.IdRes;
import androidx.test.core.app.ApplicationProvider;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Hex;
@@ -41,6 +39,7 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.otp.YandexInfo;
import com.beemdevelopment.aegis.rules.ScreenshotTestRule;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@@ -55,6 +54,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import dagger.hilt.android.testing.HiltAndroidTest;
@@ -104,19 +104,26 @@ public class OverallTest extends AegisTest {
}
for (int i = 0; i < 10; i++) {
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, clickChildViewWithId(R.id.buttonRefresh)));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnHolderItem(withOtpType(HotpInfo.class), clickChildViewWithId(R.id.buttonRefresh)));
}
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
AtomicBoolean isErrorCardShown = new AtomicBoolean(false);
_activityRule.getScenario().onActivity(activity -> {
isErrorCardShown.set(((EntryAdapter)((RecyclerView) activity.findViewById(R.id.rvKeyProfiles)).getAdapter()).isErrorCardShown());
});
int entryPosOffset = isErrorCardShown.get() ? 1 : 0;
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
onView(withId(R.id.action_copy)).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 1, longClick()));
onView(withId(R.id.action_edit)).perform(click());
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
onView(withId(R.id.dropdown_group)).perform(click());
onView(withText(R.string.new_group)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(R.id.text_group)).perform(click());
onView(withId(R.id.addGroup)).inRoot(RootMatchers.isDialog()).perform(click());
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(withText(R.string.save)).perform(click());
onView(isRoot()).perform(pressBack());
onView(withId(android.R.id.button1)).perform(click());
@@ -129,13 +136,13 @@ public class OverallTest extends AegisTest {
changeGroupFilter(_groupName);
changeGroupFilter(null);
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(4, click()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 2, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 3, click()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 4, click()));
onView(withId(R.id.action_share_qr)).perform(click());
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(entryPosOffset + 0, longClick()));
onView(allOf(isDescendantOfA(withClassName(containsString("ActionBarContextView"))), withClassName(containsString("OverflowMenuButton")))).perform(click());
onView(withText(R.string.action_delete)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
@@ -183,7 +190,7 @@ public class OverallTest extends AegisTest {
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.accordian_header)).perform(scrollTo(), click());
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
@@ -203,7 +210,7 @@ public class OverallTest extends AegisTest {
throw new RuntimeException(String.format("Unexpected entry type: %s", entry.getInfo().getClass().getSimpleName()));
}
onView(withId(R.id.dropdown_type)).perform(click());
onView(withId(R.id.dropdown_type)).perform(scrollTo(), click());
onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
}
@@ -1,7 +1,6 @@
package com.beemdevelopment.aegis;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -32,11 +31,10 @@ public class PanicTriggerTest extends AegisTest {
@Test
public void testPanicTriggerDisabled() {
assertFalse(_prefs.isPanicTriggerEnabled());
assertTrue(_vaultManager.isVaultLoaded());
launchPanic();
assertTrue(_vaultManager.isVaultLoaded());
_vaultManager.getVault();
assertFalse(_vaultManager.isVaultFileLoaded());
assertNull(_vaultManager.getVaultFileError());
assertTrue(VaultRepository.fileExists(getApp()));
}
@@ -44,11 +42,10 @@ public class PanicTriggerTest extends AegisTest {
public void testPanicTriggerEnabled() {
_prefs.setIsPanicTriggerEnabled(true);
assertTrue(_prefs.isPanicTriggerEnabled());
assertTrue(_vaultManager.isVaultLoaded());
launchPanic();
assertFalse(_vaultManager.isVaultLoaded());
assertThrows(IllegalStateException.class, () -> _vaultManager.getVault());
assertFalse(_vaultManager.isVaultFileLoaded());
assertNull(_vaultManager.getVaultFileError());
assertFalse(VaultRepository.fileExists(getApp()));
}
+6 -2
View File
@@ -28,7 +28,6 @@
android:label="Aegis"
android:supportsRtl="true"
android:theme="@style/Theme.Aegis.Launch"
tools:replace="android:theme"
tools:targetApi="tiramisu">
<activity android:name=".ui.TransferEntriesActivity"
android:label="@string/title_activity_transfer" />
@@ -82,6 +81,10 @@
<activity
android:name=".ui.GroupManagerActivity"
android:label="@string/title_activity_manage_groups" />
<activity android:name=".ui.AssignIconsActivity"
android:label="@string/title_activity_assign_icons"/>
<activity android:name=".ui.LicensesActivity"
android:label="@string/title_activity_licenses"/>
<activity
android:name=".ui.PanicResponderActivity"
android:exported="true"
@@ -146,6 +149,7 @@
</application>
<queries>
<package android:name="me.jmh.authenticatorpro" />
<package android:name="com.authy.authy" />
<package android:name="org.fedorahosted.freeotp" />
<package android:name="org.liberty.android.freeotpplus" />
@@ -154,7 +158,7 @@
<package android:name="com.valvesoftware.android.steam.community" />
<package android:name="com.authenticator.authservice2" />
<package android:name="com.duosecurity.duomobile" />
<package android:name="com.blizzard.bma" />
<package android:name="com.blizzard.messenger" />
</queries>
</manifest>
+116
View File
@@ -31,6 +31,122 @@
</head>
<body>
<div></div>
<h3>Version 3.2</h3>
<h4>New</h4>
<ul>
<li>The ability to add a single entry to multiple groups</li>
<li>Option to keep an infinite number of backups</li>
<li>Option to customize which fields to search for in entries</li>
<li>Allow hiding entry names in the tiled view mode</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>With "Tap to reveal" enabled, the size of the shown dots would not be consistent with the size of the code digits, on some devices</li>
<li>After importing a backup, the UI would in some cases incorrectly claim that biometric unlock is enabled</li>
<li>The export dialog was not fully visible on some devices</li>
<li>Various other minor UI, stability and performance improvements</li>
</ul>
<h3>Version 3.1.1</h3>
<h4>Fixes</h4>
<p>
A recent Android Pixel update introduced a bug causing Aegis to sometimes show a black screen after unlocking the vault.
We have reported this issue to the Google Issue Tracker (<a href="https://issuetracker.google.com/issues/352963108">link</a>) and
are awaiting a response from Google. In the meantime, we have implemented a workaround that eliminates this bug.
</p>
<ul>
<li>Group filter now gets applied properly upon unlocking the vault</li>
<li>Advanced entry settings now gets shown correctly</li>
<li>Keyboard when searching for entries now gets hidden when the user starts scrolling through the list</li>
</ul>
<h3>Version 3.1</h3>
<h4>New</h4>
<ul>
<li>A new audit log has been added to check all important events that occurred in your vault</li>
<li>Added the ability to rename groups</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Group selection will now be remembered again upon launch</li>
<li>Various UI improvements</li>
<li>Stability fixes</li>
</ul>
<h3>Version 3.0.1</h3>
<h4>New</h4>
<ul>
<li>Support for importing from the new Battle.net app</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Visual glitches when AMOLED theme was used on old Android versions</li>
<li>Minor UI improvements</li>
</ul>
<h3>Version 3.0</h3>
<h4>New</h4>
<ul>
<li>Material 3 (and Material You)</li>
<li>Automatic assignment of icons to entries</li>
<li>Ability to select all entries in one go</li>
<li>Support for importing 2FAS schema v4 backups</li>
<li>Sort entries based on the last time they were used</li>
<li>Some clarifications related to importing and backup permission errors</li>
<li>Preparations for the ability to assign a single entry to multiple groups</li>
<li>Performance improvements when scrolling through an entry list with lots of icons</li>
<li>A new look for the third-party licenses list</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Directly importing from Authy using root would fail</li>
<li>Minor glitches related to animation duration scale settings</li>
<li>Various stability improvements</li>
</ul>
<h3>Version 2.2.2</h3>
<h4>New</h4>
<ul>
<li>An optional name field for icon packs to bypass filename character restrictions</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The Authenticator Pro importer only supported the legacy backup format</li>
<li>A crash could occur in the tile service</li>
</ul>
<h3>Version 2.2.1</h3>
<h4>New</h4>
<ul>
<li>Ability to automatically skip potential duplicates when importing entries</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Biometrics button on the unlock screen was unresponsive</li>
</ul>
<h3>Version 2.2</h3>
<h4>New</h4>
<ul>
<li>Authenticator Pro encrypted import support</li>
<li>Ability to change account name position</li>
<li>A new dialog explaining how our password reminder works</li>
<li>Ability to change copy behavior</li>
<li>Ability to only show account names when necessary</li>
<li>New view mode: Tiles/Grid</li>
<li>Added translation: Dutch (Frysian)</li>
<li>Updated translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Deleting an entry while a search filter is active now shows the correct state</li>
<li>Aegis now fully respects system animation settings</li>
</ul>
<h3>Version 2.1.3</h3>
<h4>New</h4>
<ul>
<li>Option to disable the backup reminder</li>
<li>Improved group selection dropdown during vault export</li>
<li>New translation: Hebrew</li>
<li>Updated translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>A crash could occur because a Toast was incorrectly created</li>
</ul>
<h3>Version 2.1.2</h3>
<h4>Fixes</h4>
<ul>
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 Amulya Khare
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,316 @@
package com.amulyakhare.textdrawable;
import android.graphics.*;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.graphics.drawable.shapes.RectShape;
import android.graphics.drawable.shapes.RoundRectShape;
/**
* @author amulya
* @datetime 14 Oct 2014, 3:53 PM
*/
public class TextDrawable extends ShapeDrawable {
private final Paint textPaint;
private final Paint borderPaint;
private static final float SHADE_FACTOR = 0.9f;
private final String text;
private final int color;
private final RectShape shape;
private final int height;
private final int width;
private final int fontSize;
private final float radius;
private final int borderThickness;
private TextDrawable(Builder builder) {
super(builder.shape);
// shape properties
shape = builder.shape;
height = builder.height;
width = builder.width;
radius = builder.radius;
// text and color
text = builder.toUpperCase ? builder.text.toUpperCase() : builder.text;
color = builder.color;
// text paint settings
fontSize = builder.fontSize;
textPaint = new Paint();
textPaint.setColor(builder.textColor);
textPaint.setAntiAlias(true);
textPaint.setFakeBoldText(builder.isBold);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTypeface(builder.font);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setStrokeWidth(builder.borderThickness);
// border paint settings
borderThickness = builder.borderThickness;
borderPaint = new Paint();
borderPaint.setColor(getDarkerShade(color));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(borderThickness);
// drawable paint color
Paint paint = getPaint();
paint.setColor(color);
}
private int getDarkerShade(int color) {
return Color.rgb((int)(SHADE_FACTOR * Color.red(color)),
(int)(SHADE_FACTOR * Color.green(color)),
(int)(SHADE_FACTOR * Color.blue(color)));
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
Rect r = getBounds();
// draw border
if (borderThickness > 0) {
drawBorder(canvas);
}
int count = canvas.save();
canvas.translate(r.left, r.top);
// draw text
int width = this.width < 0 ? r.width() : this.width;
int height = this.height < 0 ? r.height() : this.height;
int fontSize = this.fontSize < 0 ? (Math.min(width, height) / 2) : this.fontSize;
textPaint.setTextSize(fontSize);
canvas.drawText(text, width / 2, height / 2 - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint);
canvas.restoreToCount(count);
}
private void drawBorder(Canvas canvas) {
RectF rect = new RectF(getBounds());
rect.inset(borderThickness/2, borderThickness/2);
if (shape instanceof OvalShape) {
canvas.drawOval(rect, borderPaint);
}
else if (shape instanceof RoundRectShape) {
canvas.drawRoundRect(rect, radius, radius, borderPaint);
}
else {
canvas.drawRect(rect, borderPaint);
}
}
@Override
public void setAlpha(int alpha) {
textPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
textPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public int getIntrinsicWidth() {
return width;
}
@Override
public int getIntrinsicHeight() {
return height;
}
public static IShapeBuilder builder() {
return new Builder();
}
public static class Builder implements IConfigBuilder, IShapeBuilder, IBuilder {
private String text;
private int color;
private int borderThickness;
private int width;
private int height;
private Typeface font;
private RectShape shape;
public int textColor;
private int fontSize;
private boolean isBold;
private boolean toUpperCase;
public float radius;
private Builder() {
text = "";
color = Color.GRAY;
textColor = Color.WHITE;
borderThickness = 0;
width = -1;
height = -1;
shape = new RectShape();
font = Typeface.create("sans-serif-light", Typeface.NORMAL);
fontSize = -1;
isBold = false;
toUpperCase = false;
}
public IConfigBuilder width(int width) {
this.width = width;
return this;
}
public IConfigBuilder height(int height) {
this.height = height;
return this;
}
public IConfigBuilder textColor(int color) {
this.textColor = color;
return this;
}
public IConfigBuilder withBorder(int thickness) {
this.borderThickness = thickness;
return this;
}
public IConfigBuilder useFont(Typeface font) {
this.font = font;
return this;
}
public IConfigBuilder fontSize(int size) {
this.fontSize = size;
return this;
}
public IConfigBuilder bold() {
this.isBold = true;
return this;
}
public IConfigBuilder toUpperCase() {
this.toUpperCase = true;
return this;
}
@Override
public IConfigBuilder beginConfig() {
return this;
}
@Override
public IShapeBuilder endConfig() {
return this;
}
@Override
public IBuilder rect() {
this.shape = new RectShape();
return this;
}
@Override
public IBuilder round() {
this.shape = new OvalShape();
return this;
}
@Override
public IBuilder roundRect(int radius) {
this.radius = radius;
float[] radii = {radius, radius, radius, radius, radius, radius, radius, radius};
this.shape = new RoundRectShape(radii, null, null);
return this;
}
@Override
public TextDrawable buildRect(String text, int color) {
rect();
return build(text, color);
}
@Override
public TextDrawable buildRoundRect(String text, int color, int radius) {
roundRect(radius);
return build(text, color);
}
@Override
public TextDrawable buildRound(String text, int color) {
round();
return build(text, color);
}
@Override
public TextDrawable build(String text, int color) {
this.color = color;
this.text = text;
return new TextDrawable(this);
}
}
public interface IConfigBuilder {
public IConfigBuilder width(int width);
public IConfigBuilder height(int height);
public IConfigBuilder textColor(int color);
public IConfigBuilder withBorder(int thickness);
public IConfigBuilder useFont(Typeface font);
public IConfigBuilder fontSize(int size);
public IConfigBuilder bold();
public IConfigBuilder toUpperCase();
public IShapeBuilder endConfig();
}
public static interface IBuilder {
public TextDrawable build(String text, int color);
}
public static interface IShapeBuilder {
public IConfigBuilder beginConfig();
public IBuilder rect();
public IBuilder round();
public IBuilder roundRect(int radius);
public TextDrawable buildRect(String text, int color);
public TextDrawable buildRoundRect(String text, int color, int radius);
public TextDrawable buildRound(String text, int color);
}
}
@@ -0,0 +1,69 @@
package com.amulyakhare.textdrawable.util;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
* @author amulya
* @datetime 14 Oct 2014, 5:20 PM
*/
public class ColorGenerator {
public static ColorGenerator DEFAULT;
public static ColorGenerator MATERIAL;
static {
DEFAULT = create(Arrays.asList(
0xfff16364,
0xfff58559,
0xfff9a43e,
0xffe4c62e,
0xff67bf74,
0xff59a2be,
0xff2093cd,
0xffad62a7,
0xff805781
));
MATERIAL = create(Arrays.asList(
0xffe57373,
0xfff06292,
0xffba68c8,
0xff9575cd,
0xff7986cb,
0xff64b5f6,
0xff4fc3f7,
0xff4dd0e1,
0xff4db6ac,
0xff81c784,
0xffaed581,
0xffff8a65,
0xffd4e157,
0xffffd54f,
0xffffb74d,
0xffa1887f,
0xff90a4ae
));
}
private final List<Integer> mColors;
private final Random mRandom;
public static ColorGenerator create(List<Integer> colorList) {
return new ColorGenerator(colorList);
}
private ColorGenerator(List<Integer> colorList) {
mColors = colorList;
mRandom = new Random(System.currentTimeMillis());
}
public int getRandomColor() {
return mColors.get(mRandom.nextInt(mColors.size()));
}
public int getColor(Object key) {
return mColors.get(Math.abs(key.hashCode()) % mColors.size());
}
}
@@ -0,0 +1,17 @@
package com.beemdevelopment.aegis;
public enum AccountNamePosition {
HIDDEN,
END,
BELOW;
private static AccountNamePosition[] _values;
static {
_values = values();
}
public static AccountNamePosition fromInteger(int x) {
return _values[x];
}
}
@@ -17,16 +17,23 @@ import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.room.Room;
import com.beemdevelopment.aegis.database.AppDatabase;
import com.beemdevelopment.aegis.database.AuditLogEntry;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.receivers.VaultLockReceiver;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.mikepenz.iconics.Iconics;
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
import com.topjohnwu.superuser.Shell;
import org.checkerframework.checker.units.qual.A;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import dagger.hilt.InstallIn;
import dagger.hilt.android.EarlyEntryPoint;
@@ -48,9 +55,6 @@ public abstract class AegisApplicationBase extends Application {
super.onCreate();
_vaultManager = EarlyEntryPoints.get(this, EntryPoint.class).getVaultManager();
Iconics.init(this);
Iconics.registerFont(new MaterialDesignIconic());
VaultLockReceiver lockReceiver = new VaultLockReceiver();
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
ContextCompat.registerReceiver(this, lockReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
@@ -8,6 +8,8 @@ import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import com.beemdevelopment.aegis.database.AppDatabase;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultRepository;
@@ -25,12 +27,16 @@ public class AegisBackupAgent extends BackupAgent {
private Preferences _prefs;
private AuditLogRepository _auditLogRepository;
@Override
public void onCreate() {
super.onCreate();
// Cannot use injection with Dagger Hilt here, because the app is launched in a restricted mode on restore
_prefs = new Preferences(this);
AppDatabase appDatabase = AegisModule.provideAppDatabase(this);
_auditLogRepository = AegisModule.provideAuditLogRepository(appDatabase);
}
@Override
@@ -53,6 +59,7 @@ public class AegisBackupAgent extends BackupAgent {
// report any runtime exceptions, in addition to the expected IOExceptions.
try {
fullBackup(data);
_auditLogRepository.addAndroidBackupCreatedEvent();
_prefs.setAndroidBackupResult(new Preferences.BackupResult(null));
} catch (Exception e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
@@ -2,6 +2,11 @@ package com.beemdevelopment.aegis;
import android.content.Context;
import androidx.room.Room;
import com.beemdevelopment.aegis.database.AppDatabase;
import com.beemdevelopment.aegis.database.AuditLogDao;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.vault.VaultManager;
@@ -24,12 +29,27 @@ public class AegisModule {
@Provides
@Singleton
public static VaultManager provideVaultManager(@ApplicationContext Context context) {
return new VaultManager(context);
public static AuditLogRepository provideAuditLogRepository(AppDatabase appDatabase) {
AuditLogDao auditLogDao = appDatabase.auditLogDao();
return new AuditLogRepository(auditLogDao);
}
@Provides
@Singleton
public static VaultManager provideVaultManager(@ApplicationContext Context context, AuditLogRepository auditLogRepository) {
return new VaultManager(context, auditLogRepository);
}
@Provides
public static Preferences providePreferences(@ApplicationContext Context context) {
return new Preferences(context);
}
@Provides
@Singleton
public static AppDatabase provideAppDatabase(@ApplicationContext Context context) {
return Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, "aegis-db")
.build();
}
}
@@ -0,0 +1,17 @@
package com.beemdevelopment.aegis;
public enum CopyBehavior {
NEVER,
SINGLETAP,
DOUBLETAP;
private static CopyBehavior[] _values;
static {
_values = values();
}
public static CopyBehavior fromInteger(int x) {
return _values[x];
}
}
@@ -0,0 +1,42 @@
package com.beemdevelopment.aegis;
public enum EventType {
VAULT_UNLOCKED,
VAULT_BACKUP_CREATED,
VAULT_ANDROID_BACKUP_CREATED,
VAULT_EXPORTED,
ENTRY_SHARED,
VAULT_UNLOCK_FAILED_PASSWORD,
VAULT_UNLOCK_FAILED_BIOMETRICS;
private static EventType[] _values;
static {
_values = values();
}
public static EventType fromInteger(int x) {
return _values[x];
}
public static int getEventTitleRes(EventType eventType) {
switch (eventType) {
case VAULT_UNLOCKED:
return R.string.event_title_vault_unlocked;
case VAULT_BACKUP_CREATED:
return R.string.event_title_backup_created;
case VAULT_ANDROID_BACKUP_CREATED:
return R.string.event_title_android_backup_created;
case VAULT_EXPORTED:
return R.string.event_title_vault_exported;
case ENTRY_SHARED:
return R.string.event_title_entry_shared;
case VAULT_UNLOCK_FAILED_PASSWORD:
return R.string.event_title_vault_unlock_failed_password;
case VAULT_UNLOCK_FAILED_BIOMETRICS:
return R.string.event_title_vault_unlock_failed_biometrics;
default:
return R.string.event_unknown;
}
}
}
@@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.TimeUtils;
import com.beemdevelopment.aegis.vault.VaultBackupPermissionException;
import org.json.JSONArray;
import org.json.JSONException;
@@ -20,9 +21,11 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class Preferences {
@@ -31,12 +34,26 @@ public class Preferences {
public static final int AUTO_LOCK_ON_MINIMIZE = 1 << 2;
public static final int AUTO_LOCK_ON_DEVICE_LOCK = 1 << 3;
public static final int SEARCH_IN_ISSUER = 1 << 0;
public static final int SEARCH_IN_NAME = 1 << 1;
public static final int SEARCH_IN_NOTE = 1 << 2;
public static final int SEARCH_IN_GROUPS = 1 << 3;
public static final int BACKUPS_VERSIONS_INFINITE = -1;
public static final int[] AUTO_LOCK_SETTINGS = {
AUTO_LOCK_ON_BACK_BUTTON,
AUTO_LOCK_ON_MINIMIZE,
AUTO_LOCK_ON_DEVICE_LOCK
};
public static final int[] SEARCH_BEHAVIOR_SETTINGS = {
SEARCH_IN_ISSUER,
SEARCH_IN_NAME,
SEARCH_IN_NOTE,
SEARCH_IN_GROUPS
};
private SharedPreferences _prefs;
public Preferences(Context context) {
@@ -45,6 +62,22 @@ public class Preferences {
if (getPasswordReminderTimestamp().getTime() == 0) {
resetPasswordReminderTimestamp();
}
migratePreferences();
}
public void migratePreferences() {
// Change copy on tap to copy behavior to new preference and delete the old key
String prefCopyOnTapKey = "pref_copy_on_tap";
if (_prefs.contains(prefCopyOnTapKey)) {
boolean isCopyOnTapEnabled = _prefs.getBoolean(prefCopyOnTapKey, false);
if (isCopyOnTapEnabled) {
setCopyBehavior(CopyBehavior.SINGLETAP);
}
_prefs.edit().remove(prefCopyOnTapKey).apply();
}
}
public boolean isTapToRevealEnabled() {
@@ -114,9 +147,7 @@ public class Preferences {
setPasswordReminderTimestamp(new Date().getTime());
}
public boolean isAccountNameVisible() {
return _prefs.getBoolean("pref_account_name", true);
}
public boolean onlyShowNecessaryAccountNames() { return _prefs.getBoolean("pref_shared_issuer_account_name", false); }
public boolean isIconVisible() {
return _prefs.getBoolean("pref_show_icons", true);
@@ -128,6 +159,10 @@ public class Preferences {
return CodeGrouping.valueOf(value);
}
public void setCodeGroupSize(CodeGrouping codeGroupSize) {
_prefs.edit().putString("pref_code_group_size_string", codeGroupSize.name()).apply();
}
public boolean isIntroDone() {
return _prefs.getBoolean("pref_intro", false);
}
@@ -141,6 +176,20 @@ public class Preferences {
return _prefs.getInt("pref_auto_lock_mask", def);
}
public int getSearchBehaviorMask() {
final int def = SEARCH_IN_ISSUER | SEARCH_IN_NAME;
return _prefs.getInt("pref_search_behavior_mask", def);
}
public boolean isSearchBehaviorTypeEnabled(int searchBehaviorType) {
return (getSearchBehaviorMask() & searchBehaviorType) == searchBehaviorType;
}
public void setSearchBehaviorMask(int searchBehavior) {
_prefs.edit().putInt("pref_search_behavior_mask", searchBehavior).apply();
}
public boolean isAutoLockEnabled() {
return getAutoLockMask() != AUTO_LOCK_OFF;
}
@@ -181,6 +230,10 @@ public class Preferences {
_prefs.edit().putInt("pref_current_theme", theme.ordinal()).apply();
}
public boolean isDynamicColorsEnabled() {
return _prefs.getBoolean("pref_dynamic_colors", false);
}
public ViewMode getCurrentViewMode() {
return ViewMode.fromInteger(_prefs.getInt("pref_current_view_mode", 0));
}
@@ -189,6 +242,14 @@ public class Preferences {
_prefs.edit().putInt("pref_current_view_mode", viewMode.ordinal()).apply();
}
public AccountNamePosition getAccountNamePosition() {
return AccountNamePosition.fromInteger(_prefs.getInt("pref_account_name_position", AccountNamePosition.END.ordinal()));
}
public void setAccountNamePosition(AccountNamePosition accountNamePosition) {
_prefs.edit().putInt("pref_account_name_position", accountNamePosition.ordinal()).apply();
}
public Integer getUsageCount(UUID uuid) {
Integer usageCount = getUsageCounts().get(uuid);
@@ -202,10 +263,51 @@ public class Preferences {
setUsageCount(usageCounts);
}
public long getLastUsedTimestamp(UUID uuid) {
Map<UUID, Long> timestamps = getLastUsedTimestamps();
if (timestamps != null && timestamps.size() > 0){
Long timestamp = timestamps.get(uuid);
return timestamp != null ? timestamp : 0;
}
return 0;
}
public void clearUsageCount() {
_prefs.edit().remove("pref_usage_count").apply();
}
public Map<UUID, Long> getLastUsedTimestamps() {
Map<UUID, Long> lastUsedTimestamps = new HashMap<>();
String lastUsedTimestamp = _prefs.getString("pref_last_used_timestamps", "");
try {
JSONArray arr = new JSONArray(lastUsedTimestamp);
for (int i = 0; i < arr.length(); i++) {
JSONObject json = arr.getJSONObject(i);
lastUsedTimestamps.put(UUID.fromString(json.getString("uuid")), json.getLong("timestamp"));
}
} catch (JSONException ignored) {
}
return lastUsedTimestamps;
}
public void setLastUsedTimestamps(Map<UUID, Long> lastUsedTimestamps) {
JSONArray lastUsedTimestampJson = new JSONArray();
for (Map.Entry<UUID, Long> entry : lastUsedTimestamps.entrySet()) {
JSONObject entryJson = new JSONObject();
try {
entryJson.put("uuid", entry.getKey());
entryJson.put("timestamp", entry.getValue());
lastUsedTimestampJson.put(entryJson);
} catch (JSONException e) {
e.printStackTrace();
}
}
_prefs.edit().putString("pref_last_used_timestamps", lastUsedTimestampJson.toString()).apply();
}
public Map<UUID, Integer> getUsageCounts() {
Map<UUID, Integer> usageCounts = new HashMap<>();
String usageCount = _prefs.getString("pref_usage_count", "");
@@ -241,8 +343,16 @@ public class Preferences {
return _prefs.getInt("pref_timeout", -1);
}
public String getLanguage() {
return _prefs.getString("pref_lang", "system");
}
public void setLanguage(String lang) {
_prefs.edit().putString("pref_lang", lang).apply();
}
public Locale getLocale() {
String lang = _prefs.getString("pref_lang", "system");
String lang = getLanguage();
if (lang.equals("system")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -278,6 +388,14 @@ public class Preferences {
setBuiltInBackupResult(null);
}
public boolean isBackupReminderEnabled() {
return _prefs.getBoolean("pref_backup_reminder", true);
}
public void setIsBackupReminderEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backup_reminder", enabled).apply();
}
public Uri getBackupsLocation() {
String str = _prefs.getString("pref_backups_location", null);
if (str != null) {
@@ -436,34 +554,38 @@ public class Preferences {
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
}
public boolean isCopyOnTapEnabled() {
return _prefs.getBoolean("pref_copy_on_tap", false);
public CopyBehavior getCopyBehavior() {
return CopyBehavior.fromInteger(_prefs.getInt("pref_current_copy_behavior", 0));
}
public void setCopyBehavior(CopyBehavior copyBehavior) {
_prefs.edit().putInt("pref_current_copy_behavior", copyBehavior.ordinal()).apply();
}
public boolean isMinimizeOnCopyEnabled() {
return _prefs.getBoolean("pref_minimize_on_copy", false);
}
public void setGroupFilter(List<String> groupFilter) {
public void setGroupFilter(Set<UUID> groupFilter) {
JSONArray json = new JSONArray(groupFilter);
_prefs.edit().putString("pref_group_filter", json.toString()).apply();
_prefs.edit().putString("pref_group_filter_uuids", json.toString()).apply();
}
public List<String> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter", null);
public Set<UUID> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter_uuids", null);
if (raw == null || raw.isEmpty()) {
return Collections.emptyList();
return Collections.emptySet();
}
try {
JSONArray json = new JSONArray(raw);
List<String> filter = new ArrayList<>();
Set<UUID> filter = new HashSet<>();
for (int i = 0; i < json.length(); i++) {
filter.add(json.isNull(i) ? null : json.optString(i));
filter.add(json.isNull(i) ? null : UUID.fromString(json.getString(i)));
}
return filter;
} catch (JSONException e) {
return Collections.emptyList();
return Collections.emptySet();
}
}
@@ -471,14 +593,16 @@ public class Preferences {
private final Date _time;
private boolean _isBuiltIn;
private final String _error;
private final boolean _isPermissionError;
public BackupResult(@Nullable Exception e) {
this(new Date(), e == null ? null : e.toString());
this(new Date(), e == null ? null : e.toString(), e instanceof VaultBackupPermissionException);
}
private BackupResult(Date time, @Nullable String error) {
private BackupResult(Date time, @Nullable String error, boolean isPermissionError) {
_time = time;
_error = error;
_isPermissionError = isPermissionError;
}
@Nullable
@@ -506,12 +630,17 @@ public class Preferences {
_isBuiltIn = isBuiltIn;
}
public boolean isPermissionError() {
return _isPermissionError;
}
public String toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("time", _time.getTime());
obj.put("error", _error == null ? JSONObject.NULL : _error);
obj.put("isPermissionError", _isPermissionError);
} catch (JSONException e) {
throw new RuntimeException(e);
}
@@ -523,7 +652,8 @@ public class Preferences {
JSONObject obj = new JSONObject(json);
long time = obj.getLong("time");
String error = JsonUtils.optString(obj, "error");
return new BackupResult(new Date(time), error);
boolean isPermissionError = obj.optBoolean("isPermissionError");
return new BackupResult(new Date(time), error, isPermissionError);
}
}
@@ -1,5 +1,6 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.helpers.comparators.LastUsedComparator;
import com.beemdevelopment.aegis.helpers.comparators.UsageCountComparator;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
@@ -14,7 +15,8 @@ public enum SortCategory {
ACCOUNT_REVERSED,
ISSUER,
ISSUER_REVERSED,
USAGE_COUNT;
USAGE_COUNT,
LAST_USED;
private static SortCategory[] _values;
@@ -31,20 +33,22 @@ public enum SortCategory {
switch (this) {
case ACCOUNT:
comparator = new AccountNameComparator();
comparator = new AccountNameComparator().thenComparing(new IssuerNameComparator());
break;
case ACCOUNT_REVERSED:
comparator = Collections.reverseOrder(new AccountNameComparator());
comparator = Collections.reverseOrder(new AccountNameComparator().thenComparing(new IssuerNameComparator()));
break;
case ISSUER:
comparator = new IssuerNameComparator();
comparator = new IssuerNameComparator().thenComparing(new AccountNameComparator());
break;
case ISSUER_REVERSED:
comparator = Collections.reverseOrder(new IssuerNameComparator());
comparator = Collections.reverseOrder(new IssuerNameComparator().thenComparing(new AccountNameComparator()));
break;
case USAGE_COUNT:
comparator = Collections.reverseOrder(new UsageCountComparator());
break;
case LAST_USED:
comparator = Collections.reverseOrder(new LastUsedComparator());
}
return comparator;
@@ -64,6 +68,8 @@ public enum SortCategory {
return R.id.menu_sort_alphabetically_reverse;
case USAGE_COUNT:
return R.id.menu_sort_usage_count;
case LAST_USED:
return R.id.menu_sort_last_used;
default:
return R.id.menu_sort_custom;
}
@@ -10,20 +10,14 @@ public class ThemeMap {
}
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light_Default,
Theme.DARK, R.style.Theme_Aegis_Dark_Default,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Default
);
public static final Map<Theme, Integer> NO_ACTION_BAR = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light_NoActionBar,
Theme.DARK, R.style.Theme_Aegis_Dark_NoActionBar,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_NoActionBar
Theme.LIGHT, R.style.Theme_Aegis_Light,
Theme.DARK, R.style.Theme_Aegis_Dark,
Theme.AMOLED, R.style.Theme_Aegis_Amoled
);
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light_Fullscreen,
Theme.DARK, R.style.Theme_Aegis_Dark_Fullscreen,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Fullscreen
Theme.AMOLED, R.style.Theme_Aegis_Amoled_Fullscreen
);
}
@@ -5,7 +5,8 @@ import androidx.annotation.LayoutRes;
public enum ViewMode {
NORMAL,
COMPACT,
SMALL;
SMALL,
TILES;
private static ViewMode[] _values;
@@ -26,19 +27,39 @@ public enum ViewMode {
return R.layout.card_entry_compact;
case SMALL:
return R.layout.card_entry_small;
case TILES:
return R.layout.card_entry_tile;
default:
return R.layout.card_entry;
}
}
/**
* Retrieves the height (in dp) that the divider between entries should have in this view mode.
* Retrieves the offset (in dp) that should exist between entries in this view mode.
*/
public float getDividerHeight() {
public float getItemOffset() {
if (this == ViewMode.COMPACT) {
return 0;
return 1;
} else if (this == ViewMode.TILES) {
return 4;
}
return 20;
return 8;
}
public int getSpanCount() {
if (this == ViewMode.TILES) {
return 2;
}
return 1;
}
public String getFormattedAccountName(String accountName) {
if (this == ViewMode.TILES) {
return accountName;
}
return String.format("(%s)", accountName);
}
}
@@ -0,0 +1,15 @@
package com.beemdevelopment.aegis.database;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Database(entities = {AuditLogEntry.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract AuditLogDao auditLogDao();
}
@@ -0,0 +1,17 @@
package com.beemdevelopment.aegis.database;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;
@Dao
public interface AuditLogDao {
@Insert
void insert(AuditLogEntry log);
@Query("SELECT * FROM audit_logs WHERE timestamp >= strftime('%s', 'now', '-30 days') ORDER BY timestamp DESC")
LiveData<List<AuditLogEntry>> getAll();
}
@@ -0,0 +1,61 @@
package com.beemdevelopment.aegis.database;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.beemdevelopment.aegis.EventType;
@Entity(tableName = "audit_logs")
public class AuditLogEntry {
@PrimaryKey(autoGenerate = true)
protected long id;
@NonNull
@ColumnInfo(name = "event_type")
private final EventType _eventType;
@ColumnInfo(name = "reference")
private final String _reference;
@ColumnInfo(name = "timestamp")
private final long _timestamp;
@Ignore
public AuditLogEntry(@NonNull EventType eventType) {
this(eventType, null);
}
@Ignore
public AuditLogEntry(@NonNull EventType eventType, @Nullable String reference) {
_eventType = eventType;
_reference = reference;
_timestamp = System.currentTimeMillis();
}
AuditLogEntry(long id, @NonNull EventType eventType, @Nullable String reference, long timestamp) {
this.id = id;
_eventType = eventType;
_reference = reference;
_timestamp = timestamp;
}
public long getId() {
return id;
}
public EventType getEventType() {
return _eventType;
}
public String getReference() {
return _reference;
}
public long getTimestamp() {
return _timestamp;
}
}
@@ -0,0 +1,66 @@
package com.beemdevelopment.aegis.database;
import androidx.lifecycle.LiveData;
import com.beemdevelopment.aegis.EventType;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class AuditLogRepository {
private final AuditLogDao _auditLogDao;
private final Executor _executor;
public AuditLogRepository(AuditLogDao auditLogDao) {
_auditLogDao = auditLogDao;
_executor = Executors.newSingleThreadExecutor();
}
public LiveData<List<AuditLogEntry>> getAllAuditLogEntries() {
return _auditLogDao.getAll();
}
public void addVaultUnlockedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCKED);
insert(auditLogEntry);
}
public void addBackupCreatedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_BACKUP_CREATED);
insert(auditLogEntry);
}
public void addAndroidBackupCreatedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_ANDROID_BACKUP_CREATED);
insert(auditLogEntry);
}
public void addVaultExportedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_EXPORTED);
insert(auditLogEntry);
}
public void addEntrySharedEvent(String reference) {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.ENTRY_SHARED, reference);
insert(auditLogEntry);
}
public void addVaultUnlockFailedPasswordEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_PASSWORD);
insert(auditLogEntry);
}
public void addVaultUnlockFailedBiometricsEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_BIOMETRICS);
insert(auditLogEntry);
}
public void insert(AuditLogEntry auditLogEntry) {
_executor.execute(() -> {
_auditLogDao.insert(auditLogEntry);
});
}
}
@@ -0,0 +1,54 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.provider.Settings;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
public class AnimationsHelper {
private AnimationsHelper() {
}
public static Animation loadScaledAnimation(Context context, int animationResId) {
return loadScaledAnimation(context, animationResId, Scale.ANIMATOR);
}
public static Animation loadScaledAnimation(Context context, int animationResId, Scale scale) {
Animation animation = AnimationUtils.loadAnimation(context, animationResId);
long newDuration = (long) (animation.getDuration() * scale.getValue(context));
animation.setDuration(newDuration);
return animation;
}
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId) {
return loadScaledLayoutAnimation(context, animationResId, Scale.ANIMATOR);
}
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId, Scale scale) {
LayoutAnimationController controller = AnimationUtils.loadLayoutAnimation(context, animationResId);
Animation animation = controller.getAnimation();
animation.setDuration((long) (animation.getDuration() * scale.getValue(context)));
return controller;
}
public enum Scale {
ANIMATOR(Settings.Global.ANIMATOR_DURATION_SCALE),
TRANSITION(Settings.Global.TRANSITION_ANIMATION_SCALE);
private final String _setting;
Scale(String setting) {
_setting = setting;
}
public float getValue(Context context) {
return Settings.Global.getFloat(context.getContentResolver(), _setting, 1.0f);
}
public boolean isZero(Context context) {
return getValue(context) == 0;
}
}
}
@@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.helpers;
import android.graphics.Rect;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.NonNull;
public class CenterVerticalSpan extends MetricAffectingSpan {
Rect _substringBounds;
public CenterVerticalSpan(Rect substringBounds) {
_substringBounds = substringBounds;
}
@Override
public void updateMeasureState(@NonNull TextPaint textPaint) {
applyBaselineShift(textPaint);
}
@Override
public void updateDrawState(@NonNull TextPaint textPaint) {
applyBaselineShift(textPaint);
}
private void applyBaselineShift(TextPaint textPaint) {
float topDifference = textPaint.getFontMetrics().top - _substringBounds.top;
textPaint.baselineShift -= (topDifference / 2f);
}
}
@@ -1,26 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Build;
import android.widget.ImageView;
import com.beemdevelopment.aegis.icons.IconType;
public class IconViewHelper {
private IconViewHelper() {
}
/**
* Sets the layer type of the given ImageView based on the given IconType. If the
* icon type is SVG and SDK <= 27, the layer type is set to software. Otherwise, it
* is set to hardware.
*/
public static void setLayerType(ImageView view, IconType iconType) {
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return;
}
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
}
}
@@ -1,14 +1,60 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.base.Strings;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
public class PasswordStrengthHelper {
// Material design color palette
private static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
// Limit the password length to prevent zxcvbn4j from exploding
private static final int MAX_PASSWORD_LENGTH = 64;
public static String getString(int score, Context context) {
// Material design color palette
private final static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
private final Zxcvbn _zxcvbn = new Zxcvbn();
private final EditText _textPassword;
private final ProgressBar _barPasswordStrength;
private final TextView _textPasswordStrength;
private final TextInputLayout _textPasswordWrapper;
public PasswordStrengthHelper(
EditText textPassword,
ProgressBar barPasswordStrength,
TextView textPasswordStrength,
TextInputLayout textPasswordWrapper
) {
_textPassword = textPassword;
_barPasswordStrength = barPasswordStrength;
_textPasswordStrength = textPasswordStrength;
_textPasswordWrapper = textPasswordWrapper;
}
public void measure(Context context) {
if (_textPassword.getText().length() > MAX_PASSWORD_LENGTH) {
_barPasswordStrength.setProgress(0);
_textPasswordStrength.setText(R.string.password_strength_unknown);
} else {
Strength strength = _zxcvbn.measure(_textPassword.getText());
_barPasswordStrength.setProgress(strength.getScore());
_barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(getColor(strength.getScore()))));
_textPasswordStrength.setText((_textPassword.getText().length() != 0) ? getString(strength.getScore(), context) : "");
String warning = strength.getFeedback().getWarning();
_textPasswordWrapper.setError(warning);
_textPasswordWrapper.setErrorEnabled(!Strings.isNullOrEmpty(warning));
strength.wipe();
}
}
private static String getString(int score, Context context) {
if (score < 0 || score > 4) {
throw new IllegalArgumentException("Not a valid zxcvbn score");
}
@@ -17,7 +63,7 @@ public class PasswordStrengthHelper {
return strings[score];
}
public static String getColor(int score) {
private static String getColor(int score) {
if (score < 0 || score > 4) {
throw new IllegalArgumentException("Not a valid zxcvbn score");
}
@@ -16,6 +16,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final EntryAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
private int _dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
public SimpleItemTouchHelperCallback(EntryAdapter adapter) {
_adapter = adapter;
@@ -46,43 +47,55 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
return false;
}
public void setDragFlags(int dragFlags) {
_dragFlags = dragFlags;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
// It's not clear when this can happen, but sometimes the ViewHolder
// that's passed to this function has a position of -1, leading
// to a crash down the line.
int position = viewHolder.getAdapterPosition();
int position = viewHolder.getBindingAdapterPosition();
if (position == NO_POSITION) {
return 0;
}
int swipeFlags = 0;
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
EntryAdapter adapter = (EntryAdapter) recyclerView.getAdapter();
if (adapter.isPositionFooter(position)
|| adapter.getEntryAt(position) != _selectedEntry
|| !isLongPressDragEnabled()) {
dragFlags = 0;
if (adapter == null) {
return 0;
}
return makeMovementFlags(dragFlags, swipeFlags);
int swipeFlags = 0;
if (adapter.isPositionFooter(position)
|| adapter.isPositionErrorCard(position)
|| adapter.getEntryAtPos(position) != _selectedEntry
|| !isLongPressDragEnabled()) {
return makeMovementFlags(0, swipeFlags);
}
return makeMovementFlags(_dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
if (target.getAdapterPosition() < _adapter.getShownFavoritesCount()){
int targetIndex = _adapter.translateEntryPosToIndex(target.getBindingAdapterPosition());
if (targetIndex < _adapter.getShownFavoritesCount()) {
return false;
}
_adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
int firstPosition = viewHolder.getLayoutPosition();
int secondPosition = target.getBindingAdapterPosition();
_adapter.onItemMove(firstPosition, secondPosition);
_positionChanged = true;
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
_adapter.onItemDismiss(viewHolder.getAdapterPosition());
_adapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
}
@Override
@@ -90,8 +103,9 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
super.clearView(recyclerView, viewHolder);
if (_positionChanged) {
_adapter.onItemDrop(viewHolder.getAdapterPosition());
_adapter.onItemDrop(viewHolder.getBindingAdapterPosition());
_positionChanged = false;
_adapter.refresh(false);
}
}
}
@@ -1,23 +1,55 @@
package com.beemdevelopment.aegis.helpers;
import android.content.res.Resources;
import android.graphics.Color;
import android.util.TypedValue;
import android.content.res.Configuration;
import androidx.annotation.ColorInt;
import androidx.appcompat.app.AppCompatActivity;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.google.android.material.color.DynamicColors;
import com.google.android.material.color.DynamicColorsOptions;
import java.util.Map;
public class ThemeHelper {
private ThemeHelper() {
private final AppCompatActivity _activity;
private final Preferences _prefs;
public ThemeHelper(AppCompatActivity activity, Preferences prefs) {
_activity = activity;
_prefs = prefs;
}
public static int getThemeColor(int attributeId, Resources.Theme currentTheme) {
TypedValue typedValue = new TypedValue();
currentTheme.resolveAttribute(attributeId, typedValue, true);
@ColorInt int color = typedValue.data;
/**
* Sets the theme of the activity. The actual style that is set is picked from the
* given map, based on the theme configured by the user.
*/
public void setTheme(Map<Theme, Integer> themeMap) {
int theme = themeMap.get(getConfiguredTheme());
_activity.setTheme(theme);
return color;
if (_prefs.isDynamicColorsEnabled()) {
DynamicColorsOptions.Builder optsBuilder = new DynamicColorsOptions.Builder();
if (getConfiguredTheme().equals(Theme.AMOLED)) {
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Amoled);
}
DynamicColors.applyToActivityIfAvailable(_activity, optsBuilder.build());
}
}
public Theme getConfiguredTheme() {
Theme theme = _prefs.getCurrentTheme();
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
int currentNightMode = _activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
} else {
theme = Theme.LIGHT;
}
}
return theme;
}
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class LastUsedComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return Long.compare(a.getLastUsedTimestamp(), b.getLastUsedTimestamp());
}
}
@@ -3,6 +3,7 @@ package com.beemdevelopment.aegis.icons;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.google.common.base.Objects;
import com.google.common.io.Files;
@@ -17,7 +18,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class IconPack {
private UUID _uuid;
@@ -58,9 +58,21 @@ public class IconPack {
return new ArrayList<>();
}
return _icons.stream()
.filter(i -> i.isSuggestedFor(issuer))
.collect(Collectors.toList());
List<Icon> icons = new ArrayList<>();
for (Icon icon : _icons) {
MatchType matchType = icon.getMatchFor(issuer);
if (matchType != null) {
// Inverse matches (entry issuer contains icon name) are less likely
// to be good, so position them at the end of the list.
if (matchType.equals(MatchType.NORMAL)) {
icons.add(0, icon);
} else if (matchType.equals(MatchType.INVERSE)) {
icons.add(icon);
}
}
}
return icons;
}
@Nullable
@@ -120,13 +132,15 @@ public class IconPack {
public static class Icon implements Serializable {
private final String _relFilename;
private final String _name;
private final String _category;
private final List<String> _issuers;
private File _file;
protected Icon(String filename, String category, List<String> issuers) {
protected Icon(String filename, String name, String category, List<String> issuers) {
_relFilename = filename;
_name = name;
_category = category;
_issuers = issuers;
}
@@ -149,6 +163,9 @@ public class IconPack {
}
public String getName() {
if (_name != null) {
return _name;
}
return Files.getNameWithoutExtension(new File(_relFilename).getName());
}
@@ -156,19 +173,29 @@ public class IconPack {
return _category;
}
public List<String> getIssuers() {
return Collections.unmodifiableList(_issuers);
}
private MatchType getMatchFor(String issuer) {
String lowerEntryIssuer = issuer.toLowerCase();
public boolean isSuggestedFor(String issuer) {
String lowerIssuer = issuer.toLowerCase();
return getIssuers().stream()
.map(String::toLowerCase)
.anyMatch(is -> is.contains(lowerIssuer) || lowerIssuer.contains(is));
boolean inverseMatch = false;
for (String is : _issuers) {
String lowerIconIssuer = is.toLowerCase();
if (lowerIconIssuer.contains(lowerEntryIssuer)) {
return MatchType.NORMAL;
}
if (lowerEntryIssuer.contains(lowerIconIssuer)) {
inverseMatch = true;
}
}
if (inverseMatch) {
return MatchType.INVERSE;
}
return null;
}
public static Icon fromJson(JSONObject obj) throws JSONException {
String filename = obj.getString("filename");
String name = JsonUtils.optString(obj, "name");
String category = obj.isNull("category") ? null : obj.getString("category");
JSONArray array = obj.getJSONArray("issuer");
@@ -178,7 +205,12 @@ public class IconPack {
issuers.add(issuer);
}
return new Icon(filename, category, issuers);
return new Icon(filename, name, category, issuers);
}
}
private enum MatchType {
NORMAL,
INVERSE
}
}
@@ -42,6 +42,10 @@ public class IconPackManager {
return packs.get(0);
}
public boolean hasIconPack() {
return _iconPacks.size() > 0;
}
public List<IconPack> getIconPacks() {
return new ArrayList<>(_iconPacks);
}
@@ -16,6 +16,7 @@ import com.beemdevelopment.aegis.vault.VaultEntryException;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.topjohnwu.superuser.io.SuFile;
@@ -27,6 +28,7 @@ import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
public class AegisImporter extends DatabaseImporter {
@@ -132,11 +134,31 @@ public class AegisImporter extends DatabaseImporter {
Result result = new Result();
try {
JSONArray array = _obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
JSONObject entryObj = array.getJSONObject(i);
if (_obj.has("groups")) {
JSONArray groupArray = _obj.getJSONArray("groups");
for (int i = 0; i < groupArray.length(); i++) {
JSONObject groupObj = groupArray.getJSONObject(i);
try {
VaultGroup group = convertGroup(groupObj);
if (!result.getGroups().has(group)) {
result.addGroup(group);
}
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
}
JSONArray entryArray = _obj.getJSONArray("entries");
for (int i = 0; i < entryArray.length(); i++) {
JSONObject entryObj = entryArray.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
for (UUID groupUuid : entry.getGroups()) {
if (!result.getGroups().has(groupUuid)) {
entry.getGroups().remove(groupUuid);
}
}
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
@@ -156,5 +178,13 @@ public class AegisImporter extends DatabaseImporter {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
private static VaultGroup convertGroup(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultGroup.fromJson(obj);
} catch (VaultEntryException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -18,9 +18,10 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
@@ -35,8 +36,6 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Locale;
@@ -45,8 +44,6 @@ import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AndOtpImporter extends DatabaseImporter {
@@ -117,7 +114,7 @@ public class AndOtpImporter extends DatabaseImporter {
}
}
private KeyDerivationParams getKeyDerivationParams(char[] password) throws DatabaseImporterException {
private PBKDFTask.Params getKeyDerivationParams(char[] password) throws DatabaseImporterException {
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1) {
@@ -131,7 +128,7 @@ public class AndOtpImporter extends DatabaseImporter {
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, INT_SIZE + SALT_SIZE);
return new KeyDerivationParams(password, salt, iterations);
return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, salt, iterations);
}
protected DecryptedState decryptOldFormat(char[] password) throws DatabaseImporterException {
@@ -155,8 +152,8 @@ public class AndOtpImporter extends DatabaseImporter {
protected DecryptedState decryptNewFormat(char[] password)
throws DatabaseImporterException {
KeyDerivationParams params = getKeyDerivationParams(password);
SecretKey key = AndOtpKeyDerivationTask.deriveKey(params);
PBKDFTask.Params params = getKeyDerivationParams(password);
SecretKey key = PBKDFTask.deriveKey(params);
return decryptNewFormat(key);
}
@@ -165,8 +162,8 @@ public class AndOtpImporter extends DatabaseImporter {
DecryptedState state = decryptOldFormat(password);
listener.onStateDecrypted(state);
} else {
KeyDerivationParams params = getKeyDerivationParams(password);
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key -> {
PBKDFTask.Params params = getKeyDerivationParams(password);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
DecryptedState state = decryptNewFormat(key);
listener.onStateDecrypted(state);
@@ -186,7 +183,7 @@ public class AndOtpImporter extends DatabaseImporter {
context.getResources().getString(R.string.andotp_old_format)
};
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
.setTitle(R.string.choose_andotp_importer)
.setSingleChoiceItems(choices, 0, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
@@ -269,71 +266,10 @@ public class AndOtpImporter extends DatabaseImporter {
}
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | EncodingException | OtpInfoException | JSONException e) {
} catch (DatabaseImporterException | EncodingException | OtpInfoException |
JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
protected static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpImporter.KeyDerivationParams, SecretKey> {
private Callback _cb;
public AndOtpKeyDerivationTask(Context context, Callback cb) {
super(context, context.getString(R.string.unlocking_vault));
_cb = cb;
}
@Override
protected SecretKey doInBackground(AndOtpImporter.KeyDerivationParams... args) {
setPriority();
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);
SecretKey key = factory.generateSecret(spec);
return new SecretKeySpec(key.getEncoded(), "AES");
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}
@Override
protected void onPostExecute(SecretKey key) {
super.onPostExecute(key);
_cb.onTaskFinished(key);
}
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;
}
}
}
@@ -0,0 +1,380 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.Argon2Task;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UTFDataFormatException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
public class AuthenticatorProImporter extends DatabaseImporter {
private static final String HEADER = "AUTHENTICATORPRO";
private static final String HEADER_LEGACY = "AuthenticatorPro";
private static final String PKG_NAME = "me.jmh.authenticatorpro";
private static final String PKG_DB_PATH = "files/proauth.db3";
private enum Algorithm {
SHA1,
SHA256,
SHA512
}
public AuthenticatorProImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(PKG_NAME, PKG_DB_PATH);
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
return isInternal ? readInternal(stream) : readExternal(stream);
}
private State readInternal(InputStream stream) throws DatabaseImporterException {
List<SqlEntry> entries = new SqlImporterHelper(requireContext()).read(SqlEntry.class, stream, "authenticator");
return new SqlState(entries);
}
private static State readExternal(InputStream stream) throws DatabaseImporterException {
byte[] data;
try {
data = IOUtils.readAll(stream);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try {
return new JsonState(new JSONObject(new String(data, StandardCharsets.UTF_8)));
} catch (JSONException e) {
return readEncrypted(new DataInputStream(new ByteArrayInputStream(data)));
}
}
private static State readEncrypted(DataInputStream stream) throws DatabaseImporterException {
try {
byte[] headerBytes = new byte[HEADER.getBytes(StandardCharsets.UTF_8).length];
stream.readFully(headerBytes);
String header = new String(headerBytes, StandardCharsets.UTF_8);
switch (header) {
case HEADER:
return EncryptedState.parseHeader(stream);
case HEADER_LEGACY:
return LegacyEncryptedState.parseHeader(stream);
default:
throw new DatabaseImporterException("Invalid file header");
}
} catch (UTFDataFormatException e) {
throw new DatabaseImporterException("Invalid file header");
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException e) {
throw new DatabaseImporterException(e);
}
}
private static OtpInfo parseOtpInfo(int type, byte[] secret, Algorithm algo, int digits, int period, int counter)
throws OtpInfoException, DatabaseImporterEntryException {
switch (type) {
case 1:
return new HotpInfo(secret, algo.name(), digits, counter);
case 2:
return new TotpInfo(secret, algo.name(), digits, period);
case 4:
return new SteamInfo(secret, algo.name(), digits, period);
default:
throw new DatabaseImporterEntryException(String.format("Unsupported otp type: %d", type), null);
}
}
static class EncryptedState extends State {
private static final int KEY_SIZE = 32;
private static final int MEMORY_COST = 16; // 2^16 KiB = 64 MiB
private static final int PARALLELISM = 4;
private static final int ITERATIONS = 3;
private static final int SALT_SIZE = 16;
private static final int IV_SIZE = 12;
private final Cipher _cipher;
private final byte[] _salt;
private final byte[] _iv;
private final byte[] _data;
public EncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
super(true);
_cipher = cipher;
_salt = salt;
_iv = iv;
_data = data;
}
public JsonState decrypt(char[] password) throws DatabaseImporterException {
Argon2Task.Params params = getKeyDerivationParams(password);
SecretKey key = Argon2Task.deriveKey(params);
return decrypt(key);
}
public JsonState decrypt(SecretKey key) throws DatabaseImporterException {
try {
_cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv));
byte[] decrypted = _cipher.doFinal(_data);
return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8)));
} catch (InvalidAlgorithmParameterException | IllegalBlockSizeException
| JSONException | InvalidKeyException | BadPaddingException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
Argon2Task.Params params = getKeyDerivationParams(password);
Argon2Task task = new Argon2Task(context, key -> {
try {
AuthenticatorProImporter.JsonState state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog -> listener.onCanceled());
}
private Argon2Task.Params getKeyDerivationParams(char[] password) {
Argon2Parameters argon2Params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withIterations(ITERATIONS)
.withParallelism(PARALLELISM)
.withMemoryPowOfTwo(MEMORY_COST)
.withSalt(_salt)
.build();
return new Argon2Task.Params(password, argon2Params, KEY_SIZE);
}
private static EncryptedState parseHeader(DataInputStream stream)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
byte[] salt = new byte[SALT_SIZE];
stream.readFully(salt);
byte[] iv = new byte[IV_SIZE];
stream.readFully(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
}
}
static class LegacyEncryptedState extends State {
private static final int ITERATIONS = 64000;
private static final int KEY_SIZE = 32 * Byte.SIZE;
private static final int SALT_SIZE = 20;
private final Cipher _cipher;
private final byte[] _salt;
private final byte[] _iv;
private final byte[] _data;
public LegacyEncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
super(true);
_cipher = cipher;
_salt = salt;
_iv = iv;
_data = data;
}
public JsonState decrypt(char[] password) throws DatabaseImporterException {
PBKDFTask.Params params = getKeyDerivationParams(password);
SecretKey key = PBKDFTask.deriveKey(params);
return decrypt(key);
}
public JsonState decrypt(SecretKey key) throws DatabaseImporterException {
try {
_cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv));
byte[] decrypted = _cipher.doFinal(_data);
return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8)));
} catch (InvalidAlgorithmParameterException | IllegalBlockSizeException
| JSONException | InvalidKeyException | BadPaddingException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
PBKDFTask.Params params = getKeyDerivationParams(password);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
AuthenticatorProImporter.JsonState state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog -> listener.onCanceled());
}
private PBKDFTask.Params getKeyDerivationParams(char[] password) {
return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, _salt, ITERATIONS);
}
private static LegacyEncryptedState parseHeader(DataInputStream stream)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
byte[] salt = new byte[SALT_SIZE];
stream.readFully(salt);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
int ivSize = cipher.getBlockSize();
byte[] iv = new byte[ivSize];
stream.readFully(iv);
return new LegacyEncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
}
}
private static class JsonState extends State {
private final JSONObject _obj;
public JsonState(JSONObject obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result res = new Result();
try {
JSONArray array = _obj.getJSONArray("Authenticators");
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
try {
res.addEntry(convertEntry(obj));
} catch (DatabaseImporterEntryException e) {
res.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return res;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
int type = obj.getInt("Type");
String issuer = obj.getString("Issuer");
Object nullableUsername = obj.get("Username");
String username = nullableUsername == JSONObject.NULL ? "" : nullableUsername.toString();
byte[] secret = Base32.decode(obj.getString("Secret"));
Algorithm algo = Algorithm.values()[obj.getInt("Algorithm")];
int digits = obj.getInt("Digits");
int period = obj.getInt("Period");
int counter = obj.getInt("Counter");
OtpInfo info = parseOtpInfo(type, secret, algo, digits, period, counter);
return new VaultEntry(info, username, issuer);
} catch (OtpInfoException | EncodingException | JSONException e) {
throw new DatabaseImporterEntryException(e, null);
}
}
}
private static class SqlState extends State {
private final List<SqlEntry> _entries;
public SqlState(List<SqlEntry> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() throws DatabaseImporterException {
Result res = new Result();
for (SqlEntry entry : _entries) {
try {
res.addEntry(entry.convert());
} catch (DatabaseImporterEntryException e) {
res.addError(e);
}
}
return res;
}
}
private static class SqlEntry extends SqlImporterHelper.Entry {
private final int _type;
private final String _issuer;
private final String _username;
private final String _secret;
private final Algorithm _algo;
private final int _digits;
private final int _period;
private final int _counter;
public SqlEntry(Cursor cursor) {
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "type");
_issuer = SqlImporterHelper.getString(cursor, "issuer");
_username = SqlImporterHelper.getString(cursor, "username");
_secret = SqlImporterHelper.getString(cursor, "secret");
_algo = Algorithm.values()[SqlImporterHelper.getInt(cursor, "algorithm")];
_digits = SqlImporterHelper.getInt(cursor, "digits");
_period = SqlImporterHelper.getInt(cursor, "period");
_counter = SqlImporterHelper.getInt(cursor, "counter");
}
public VaultEntry convert() throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(_secret);
OtpInfo info = parseOtpInfo(_type, secret, _algo, _digits, _period, _counter);
return new VaultEntry(info, _username, _issuer);
} catch (EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, null);
}
}
}
}
@@ -75,8 +75,13 @@ public class AuthyImporter extends DatabaseImporter {
JSONArray array;
JSONArray authyArray;
try {
array = readFile(new SuFile(path, String.format("%s.xml", _authFilename)), String.format("%s.key", _authFilename));
authyArray = readFile(new SuFile(path, String.format("%s.xml", _authyFilename)), String.format("%s.key", _authyFilename));
SuFile file1 = new SuFile(path, String.format("%s.xml", _authFilename));
file1.setShell(shell);
SuFile file2 = new SuFile(path, String.format("%s.xml", _authyFilename));
file2.setShell(shell);
array = readFile(file1, String.format("%s.key", _authFilename));
authyArray = readFile(file2, String.format("%s.key", _authyFilename));
} catch (IOException | XmlPullParserException e) {
throw new DatabaseImporterException(e);
}
@@ -11,6 +11,7 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.base.Strings;
import com.topjohnwu.superuser.io.SuFile;
import org.xmlpull.v1.XmlPullParser;
@@ -18,12 +19,10 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class BattleNetImporter extends DatabaseImporter {
private static final String _pkgName = "com.blizzard.bma";
private static final String _subPath = "shared_prefs/com.blizzard.bma.AUTH_STORE.xml";
private static final String _pkgName = "com.blizzard.messenger";
private static final String _subPath = "shared_prefs/com.blizzard.messenger.authenticator_preferences.xml";
private static final byte[] _key;
@@ -46,71 +45,80 @@ public class BattleNetImporter extends DatabaseImporter {
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
final String serialKey = "com.blizzard.messenger.AUTHENTICATOR_SERIAL";
final String secretKey = "com.blizzard.messenger.AUTHENTICATOR_DEVICE_SECRET";
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
List<String> entries = new ArrayList<>();
String serial = "";
String secretValue = null;
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals("com.blizzard.bma.AUTH_STORE.HASH")) {
entries.add(entry.Value);
break;
if (entry.Name.equals(secretKey)) {
secretValue = entry.Value;
} else if (entry.Name.equals(serialKey)) {
serial = entry.Value;
}
}
return new BattleNetImporter.State(entries);
if (secretValue == null) {
throw new DatabaseImporterException(String.format("Key not found: %s", secretKey));
}
return new BattleNetImporter.State(serial, secretValue);
} catch (XmlPullParserException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private final List<String> _entries;
private final String _serial;
private final String _secretValue;
public State(List<String> entries) {
public State(String serial, String secretValue) {
super(false);
_entries = entries;
_serial = serial;
_secretValue = secretValue;
}
@Override
public Result convert() {
Result result = new Result();
for (String str : _entries) {
try {
VaultEntry entry = convertEntry(str);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
try {
VaultEntry entry = convertEntry(_serial, _secretValue);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
return result;
}
private static VaultEntry convertEntry(String hashString) throws DatabaseImporterEntryException {
private static VaultEntry convertEntry(String serial, String secretString) throws DatabaseImporterEntryException {
try {
byte[] hash = Hex.decode(hashString);
if (hash.length != _key.length) {
throw new DatabaseImporterEntryException(String.format("Unexpected hash length: %d", hash.length), hashString);
if (!Strings.isNullOrEmpty(serial)) {
serial = unmask(serial);
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hash.length; i++) {
char c = (char) (hash[i] ^ _key[i]);
sb.append(c);
}
final int secretLen = 40;
byte[] secret = Hex.decode(sb.substring(0, secretLen));
String serial = sb.substring(secretLen);
byte[] secret = Hex.decode(unmask(secretString));
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, 8, TotpInfo.DEFAULT_PERIOD);
return new VaultEntry(info, serial, "Battle.net");
} catch (OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, hashString);
throw new DatabaseImporterEntryException(e, secretString);
}
}
private static String unmask(String s) throws EncodingException {
byte[] ds = Hex.decode(s);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ds.length; i++) {
char c = (char) (ds[i] ^ _key[i]);
sb.append(c);
}
return sb.toString();
}
}
}
@@ -8,6 +8,7 @@ import androidx.annotation.StringRes;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
@@ -33,12 +34,13 @@ public abstract class DatabaseImporter {
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
_importers.add(new Definition("Authenticator Pro", AuthenticatorProImporter.class, R.string.importer_help_authenticator_pro, true));
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
_importers.add(new Definition("Battle.net Authenticator", BattleNetImporter.class, R.string.importer_help_battle_net_authenticator, true));
_importers.add(new Definition("Bitwarden", BitwardenImporter.class, R.string.importer_help_bitwarden, false));
_importers.add(new Definition("Duo", DuoImporter.class, R.string.importer_help_duo, true));
_importers.add(new Definition("FreeOTP", 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("FreeOTP (1.x)", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+ (JSON)", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
@@ -167,12 +169,17 @@ public abstract class DatabaseImporter {
public static class Result {
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private UUIDMap<VaultGroup> _groups = new UUIDMap<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(VaultEntry entry) {
_entries.add(entry);
}
public void addGroup(VaultGroup group) {
_groups.add(group);
}
public void addError(DatabaseImporterEntryException error) {
_errors.add(error);
}
@@ -181,6 +188,10 @@ public abstract class DatabaseImporter {
return _entries;
}
public UUIDMap<VaultGroup> getGroups() {
return _groups;
}
public List<DatabaseImporterEntryException> getErrors() {
return _errors;
}
@@ -18,6 +18,10 @@ import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Array;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class SteamImporter extends DatabaseImporter {
private static final String _subDir = "files";
@@ -57,29 +61,43 @@ public class SteamImporter extends DatabaseImporter {
try {
byte[] bytes = IOUtils.readAll(stream);
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
return new State(obj);
List<JSONObject> objs = new ArrayList<>();
if (obj.has("accounts")) {
JSONObject accounts = obj.getJSONObject("accounts");
Iterator<String> keys = accounts.keys();
while (keys.hasNext()) {
String key = keys.next();
objs.add(accounts.getJSONObject(key));
}
} else {
objs.add(obj);
}
return new State(objs);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private JSONObject _obj;
private final List<JSONObject> _objs;
private State(JSONObject obj) {
private State(List<JSONObject> objs) {
super(false);
_obj = obj;
_objs = objs;
}
@Override
public Result convert() {
Result result = new Result();
try {
VaultEntry entry = convertEntry(_obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
for (JSONObject obj : _objs) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
@@ -4,8 +4,6 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base32;
@@ -18,6 +16,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
@@ -154,7 +153,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
.setMessage(R.string.choose_totpauth_importer)
.setPositiveButton(R.string.yes, (dialog, which) -> {
Dialogs.showPasswordInputDialog(context, password -> {
@@ -10,6 +10,7 @@ import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
@@ -60,7 +61,7 @@ public class TwoFASImporter extends DatabaseImporter {
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
int version = obj.getInt("schemaVersion");
if (version > 3) {
if (version > 4) {
throw new DatabaseImporterException(String.format("Unsupported schema version: %d", version));
}
@@ -187,6 +188,9 @@ public class TwoFASImporter extends DatabaseImporter {
} else if (tokenType.equals("HOTP")) {
long counter = info.optLong("counter", 0);
otp = new HotpInfo(secret, algorithm, digits, counter);
} else if (tokenType.equals("STEAM")) {
int period = info.optInt("period", TotpInfo.DEFAULT_PERIOD);
otp = new SteamInfo(secret, algorithm, digits, period);
} else {
throw new DatabaseImporterEntryException(String.format("Unrecognized tokenType: %s", tokenType), obj.toString());
}
@@ -1,34 +0,0 @@
package com.beemdevelopment.aegis.licenses;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import de.psdev.licensesdialog.licenses.License;
public class GlideLicense extends License {
@Override
public String getName() {
return "Glide License";
}
@Override
public String readSummaryTextFromResources(Context context) {
return getContent(context, R.raw.glide_license);
}
@Override
public String readFullTextFromResources(Context context) {
return getContent(context, R.raw.glide_license);
}
@Override
public String getVersion() {
return null;
}
@Override
public String getUrl() {
return "https://github.com/bumptech/glide/blob/master/LICENSE";
}
}
@@ -1,34 +0,0 @@
package com.beemdevelopment.aegis.licenses;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import de.psdev.licensesdialog.licenses.License;
public class ProtobufLicense extends License {
@Override
public String getName() {
return "Protocol Buffers License";
}
@Override
public String readSummaryTextFromResources(Context context) {
return getContent(context, R.raw.protobuf_license);
}
@Override
public String readFullTextFromResources(Context context) {
return getContent(context, R.raw.protobuf_license);
}
@Override
public String getVersion() {
return null;
}
@Override
public String getUrl() {
return "https://raw.githubusercontent.com/protocolbuffers/protobuf/master/LICENSE";
}
}
@@ -1,5 +1,7 @@
package com.beemdevelopment.aegis.services;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import android.service.quicksettings.Tile;
@@ -16,10 +18,13 @@ public class LaunchAppTileService extends TileService {
public void onStartListening() {
super.onStartListening();
Tile tile = getQsTile();
tile.setState(Tile.STATE_INACTIVE);
tile.updateTile();
if (tile != null) {
tile.setState(Tile.STATE_INACTIVE);
tile.updateTile();
}
}
@SuppressLint("StartActivityAndCollapseDeprecated")
@Override
public void onClick() {
super.onClick();
@@ -28,6 +33,12 @@ public class LaunchAppTileService extends TileService {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setAction(Intent.ACTION_MAIN);
startActivityAndCollapse(intent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, flags);
startActivityAndCollapse(pendingIntent);
} else {
startActivityAndCollapse(intent);
}
}
}
}
@@ -1,5 +1,7 @@
package com.beemdevelopment.aegis.services;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import android.service.quicksettings.Tile;
@@ -16,10 +18,13 @@ public class LaunchScannerTileService extends TileService {
public void onStartListening() {
super.onStartListening();
Tile tile = getQsTile();
tile.setState(Tile.STATE_INACTIVE);
tile.updateTile();
if (tile != null) {
tile.setState(Tile.STATE_INACTIVE);
tile.updateTile();
}
}
@SuppressLint("StartActivityAndCollapseDeprecated")
@Override
public void onClick() {
super.onClick();
@@ -29,6 +34,12 @@ public class LaunchScannerTileService extends TileService {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setAction(Intent.ACTION_MAIN);
startActivityAndCollapse(intent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, flags);
startActivityAndCollapse(pendingIntent);
} else {
startActivityAndCollapse(intent);
}
}
}
}
@@ -47,7 +47,8 @@ public class NotificationService extends Service {
.setOngoing(true)
.setContentIntent(pendingIntent);
startForeground(NOTIFICATION_VAULT_UNLOCKED, builder.build());
// NOTE: Disabled for now. See issue: #1047
//startForeground(NOTIFICATION_VAULT_UNLOCKED, builder.build());
}
@Override
@@ -13,20 +13,12 @@ import android.widget.Toast;
import androidx.annotation.AttrRes;
import androidx.annotation.StringRes;
import androidx.core.view.LayoutInflaterCompat;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.licenses.GlideLicense;
import com.beemdevelopment.aegis.licenses.ProtobufLicense;
import com.beemdevelopment.aegis.ui.dialogs.ChangelogDialog;
import com.beemdevelopment.aegis.ui.dialogs.LicenseDialog;
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
import de.psdev.licensesdialog.LicenseResolver;
import de.psdev.licensesdialog.LicensesDialog;
import com.google.android.material.color.MaterialColors;
public class AboutActivity extends AegisActivity {
@@ -40,7 +32,6 @@ public class AboutActivity extends AegisActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new IconicsLayoutInflater2(getDelegate()));
super.onCreate(savedInstanceState);
if (abortIfOrphan(savedInstanceState)) {
return;
@@ -57,12 +48,15 @@ public class AboutActivity extends AegisActivity {
View btnLicense = findViewById(R.id.btn_license);
btnLicense.setOnClickListener(v -> {
LicenseDialog.create()
.setTheme(getConfiguredTheme())
.setTheme(_themeHelper.getConfiguredTheme())
.show(getSupportFragmentManager(), null);
});
View btnThirdPartyLicenses = findViewById(R.id.btn_third_party_licenses);
btnThirdPartyLicenses.setOnClickListener(v -> showThirdPartyLicenseDialog());
btnThirdPartyLicenses.setOnClickListener(v -> {
Intent intent = new Intent(this, LicensesActivity.class);
startActivity(intent);
});
TextView appVersion = findViewById(R.id.app_version);
appVersion.setText(getCurrentAppVersion());
@@ -93,7 +87,7 @@ public class AboutActivity extends AegisActivity {
View btnChangelog = findViewById(R.id.btn_changelog);
btnChangelog.setOnClickListener(v -> {
ChangelogDialog.create()
.setTheme(getConfiguredTheme())
.setTheme(_themeHelper.getConfiguredTheme())
.show(getSupportFragmentManager(), null);
});
}
@@ -130,30 +124,9 @@ public class AboutActivity extends AegisActivity {
startActivity(Intent.createChooser(mailIntent, getString(R.string.email)));
}
private void showThirdPartyLicenseDialog() {
String stylesheet = getString(R.string.custom_notices_format_style);
int backgroundColorResource = getConfiguredTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
String backgroundColor = getThemeColorAsHex(backgroundColorResource);
String textColor = getThemeColorAsHex(R.attr.primaryText);
String licenseColor = getThemeColorAsHex(R.attr.cardBackgroundFocused);
String linkColor = getThemeColorAsHex(R.attr.colorAccent);
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor, linkColor);
LicenseResolver.registerLicense(new GlideLicense());
LicenseResolver.registerLicense(new ProtobufLicense());
new LicensesDialog.Builder(this)
.setNotices(R.raw.notices)
.setTitle(R.string.third_party_licenses)
.setNoticesCssStyle(stylesheet)
.setIncludeOwnLicense(true)
.build()
.show();
}
private String getThemeColorAsHex(@AttrRes int attributeId) {
return String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(attributeId, getTheme())));
int color = MaterialColors.getColor(this, attributeId, getClass().getCanonicalName());
return String.format("%06X", 0xFFFFFF & color);
}
@Override
@@ -4,25 +4,34 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.core.view.ViewPropertyAnimatorCompat;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.color.MaterialColors;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.Map;
import javax.inject.Inject;
@@ -35,21 +44,30 @@ import dagger.hilt.components.SingletonComponent;
@AndroidEntryPoint
public abstract class AegisActivity extends AppCompatActivity implements VaultManager.LockListener {
protected Preferences _prefs;
protected ThemeHelper _themeHelper;
@Inject
protected VaultManager _vaultManager;
@Inject
protected AuditLogRepository _auditLogRepository;
@Inject
protected IconPackManager _iconPackManager;
private ActionModeStatusGuardHack _statusGuardHack;
@Override
protected void onCreate(Bundle savedInstanceState) {
// set the theme and locale before creating the activity
_prefs = EarlyEntryPoints.get(getApplicationContext(), PrefEntryPoint.class).getPreferences();
_themeHelper = new ThemeHelper(this, _prefs);
onSetTheme();
setLocale(_prefs.getLocale());
super.onCreate(savedInstanceState);
_statusGuardHack = new ActionModeStatusGuardHack();
// set FLAG_SECURE on the window of every AegisActivity
if (_prefs.isSecureScreenEnabled()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
@@ -96,31 +114,7 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
* Called when the activity is expected to set its theme.
*/
protected void onSetTheme() {
setTheme(ThemeMap.DEFAULT);
}
/**
* Sets the theme of the activity. The actual style that is set is picked from the
* given map, based on the theme configured by the user.
*/
protected void setTheme(Map<Theme, Integer> themeMap) {
int theme = themeMap.get(getConfiguredTheme());
setTheme(theme);
}
protected Theme getConfiguredTheme() {
Theme theme = _prefs.getCurrentTheme();
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
} else {
theme = Theme.LIGHT;
}
}
return theme;
_themeHelper.setTheme(ThemeMap.DEFAULT);
}
protected void setLocale(Locale locale) {
@@ -169,6 +163,77 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
return true;
}
@Override
public void onSupportActionModeStarted(@NonNull ActionMode mode) {
super.onSupportActionModeStarted(mode);
_statusGuardHack.apply(View.VISIBLE);
}
@Override
public void onSupportActionModeFinished(@NonNull ActionMode mode) {
super.onSupportActionModeFinished(mode);
_statusGuardHack.apply(View.GONE);
}
/**
* When starting/finishing an action mode, forcefully cancel the fade in/out animation and
* set the status bar color. This requires the abc_decor_view_status_guard colors to be set
* to transparent.
*
* This should fix any inconsistencies between the color of the action bar and the status bar
* when an action mode is active.
*/
private class ActionModeStatusGuardHack {
private Field _fadeAnimField;
private Field _actionModeViewField;
@ColorInt
private final int _statusBarColor;
private ActionModeStatusGuardHack() {
_statusBarColor = getWindow().getStatusBarColor();
try {
_fadeAnimField = getDelegate().getClass().getDeclaredField("mFadeAnim");
_fadeAnimField.setAccessible(true);
_actionModeViewField = getDelegate().getClass().getDeclaredField("mActionModeView");
_actionModeViewField.setAccessible(true);
} catch (NoSuchFieldException ignored) {
}
}
private void apply(int visibility) {
if (_fadeAnimField == null || _actionModeViewField == null) {
return;
}
ViewPropertyAnimatorCompat fadeAnim;
ViewGroup actionModeView;
try {
fadeAnim = (ViewPropertyAnimatorCompat) _fadeAnimField.get(getDelegate());
actionModeView = (ViewGroup) _actionModeViewField.get(getDelegate());
} catch (IllegalAccessException e) {
return;
}
if (fadeAnim == null || actionModeView == null) {
return;
}
fadeAnim.cancel();
actionModeView.setVisibility(visibility);
actionModeView.setAlpha(visibility == View.VISIBLE ? 1f : 0f);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int statusBarColor = visibility == View.VISIBLE
? MaterialColors.getColor(actionModeView, com.google.android.material.R.attr.colorSurfaceContainer)
: _statusBarColor;
getWindow().setStatusBarColor(statusBarColor);
}
}
}
/**
* Reports whether this Activity instance has become an orphan. This can happen if
* the vault was killed/locked by an external trigger while the Activity was still open.
@@ -0,0 +1,279 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
import com.beemdevelopment.aegis.ui.views.AssignIconAdapter;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
public class AssignIconsActivity extends AegisActivity implements AssignIconAdapter.Listener {
private AssignIconAdapter _adapter;
private ArrayList<AssignIconEntry> _entries = new ArrayList<>();
private RecyclerView _entriesView;
private AssignIconsActivity.BackPressHandler _backPressHandler;
private ViewPreloadSizeProvider<AssignIconEntry> _preloadSizeProvider;
private IconPack _favoriteIconPack;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (abortIfOrphan(savedInstanceState)) {
return;
}
setContentView(R.layout.activity_assign_icons);
setSupportActionBar(findViewById(R.id.toolbar));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
ArrayList<UUID> assignIconEntriesIds = (ArrayList<UUID>) getIntent().getSerializableExtra("entries");
for (UUID entryId: assignIconEntriesIds) {
VaultEntry vaultEntry = _vaultManager.getVault().getEntryByUUID(entryId);
_entries.add(new AssignIconEntry(vaultEntry));
}
_backPressHandler = new AssignIconsActivity.BackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
IconPreloadProvider modelProvider1 = new IconPreloadProvider();
EntryIconPreloadProvider modelProvider2 = new EntryIconPreloadProvider();
_preloadSizeProvider = new ViewPreloadSizeProvider<>();
RecyclerViewPreloader<IconPack.Icon> preloader1 = new RecyclerViewPreloader(this, modelProvider1, _preloadSizeProvider, 10);
RecyclerViewPreloader<VaultEntry> preloader2 = new RecyclerViewPreloader(this, modelProvider2, _preloadSizeProvider, 10);
_adapter = new AssignIconAdapter(this);
_entriesView = findViewById(R.id.list_assign_icons);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
_entriesView.setLayoutManager(layoutManager);
_entriesView.setAdapter(_adapter);
_entriesView.setNestedScrollingEnabled(false);
_entriesView.addItemDecoration(new SpacesItemDecoration(8));
_entriesView.addOnScrollListener(preloader1);
_entriesView.addOnScrollListener(preloader2);
Optional<IconPack> favoriteIconPack = _iconPackManager.getIconPacks().stream()
.sorted(Comparator.comparing(IconPack::getName))
.findFirst();
if (!favoriteIconPack.isPresent()) {
throw new RuntimeException(String.format("Started %s without any icon packs present", AssignIconsActivity.class.getName()));
}
_favoriteIconPack = favoriteIconPack.get();
for (AssignIconEntry entry : _entries) {
IconPack.Icon suggestedIcon = findSuggestedIcon(entry);
if (suggestedIcon != null) {
entry.setNewIcon(suggestedIcon);
}
}
_adapter.addEntries(_entries);
}
private IconPack.Icon findSuggestedIcon(AssignIconEntry entry) {
List<IconPack.Icon> suggestedIcons = _favoriteIconPack.getSuggestedIcons(entry.getEntry().getIssuer());
if (suggestedIcons.size() > 0) {
return suggestedIcons.get(0);
}
return null;
}
private void saveAndFinish() throws IOException {
ArrayList<UUID> uuids = new ArrayList<>();
for (AssignIconEntry selectedEntry : _entries) {
VaultEntry entry = selectedEntry.getEntry();
if (selectedEntry.getNewIcon() != null) {
byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(selectedEntry.getNewIcon().getFile())){
iconBytes = IOUtils.readFile(inStream);
}
VaultEntryIcon icon = new VaultEntryIcon(iconBytes, selectedEntry.getNewIcon().getIconType());
entry.setIcon(icon);
uuids.add(entry.getUUID());
_vaultManager.getVault().replaceEntry(entry);
}
}
Intent intent = new Intent();
intent.putExtra("entryUUIDs", uuids);
if (saveAndBackupVault()) {
setResult(RESULT_OK, intent);
finish();
}
}
private void discardAndFinish() {
Dialogs.showDiscardDialog(this,
(dialog, which) -> {
try {
saveAndFinish();
} catch (IOException e) {
Toast.makeText(this, R.string.saving_assign_icons_error, Toast.LENGTH_SHORT).show();
}
},
(dialog, which) -> finish());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_assign_icons, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
discardAndFinish();
} else if (itemId == R.id.action_save) {
try {
saveAndFinish();
} catch (IOException e) {
Toast.makeText(this, R.string.saving_assign_icons_error, Toast.LENGTH_SHORT).show();
}
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
public void onAssignIconEntryClick(AssignIconEntry entry) {
List<IconPack> iconPacks = _iconPackManager.getIconPacks().stream()
.sorted(Comparator.comparing(IconPack::getName))
.collect(Collectors.toList());
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, entry.getEntry().getIssuer(), false, new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
entry.setNewIcon(icon);
}
@Override
public void onCustomSelected() { }
});
Dialogs.showSecureDialog(dialog);
}
@Override
public void onSetPreloadView(View view) {
_preloadSizeProvider.setView(view);
}
private class BackPressHandler extends OnBackPressedCallback {
public BackPressHandler() {
super(false);
}
@Override
public void handleOnBackPressed() {
discardAndFinish();
}
}
private class EntryIconPreloadProvider implements ListPreloader.PreloadModelProvider<VaultEntry> {
@NonNull
@Override
public List<VaultEntry> getPreloadItems(int position) {
VaultEntry entry = _entries.get(position).getEntry();
if (entry.hasIcon()) {
return Collections.singletonList(entry);
}
return Collections.emptyList();
}
@Nullable
@Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) {
RequestBuilder<Drawable> rb = Glide.with(AssignIconsActivity.this)
.load(entry.getIcon());
return GlideHelper.setCommonOptions(rb, entry.getIcon().getType());
}
}
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<IconPack.Icon> {
@NonNull
@Override
public List<IconPack.Icon> getPreloadItems(int position) {
AssignIconEntry entry = _entries.get(position);
if (entry.getNewIcon() != null) {
return Collections.singletonList(entry.getNewIcon());
}
return Collections.emptyList();
}
@Nullable
@Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
RequestBuilder<Drawable> rb = Glide.with(AssignIconsActivity.this)
.load(icon.getFile());
return GlideHelper.setCommonOptions(rb, icon.getIconType());
}
}
private class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private final int _space;
public SpacesItemDecoration(int dpSpace) {
this._space = MetricsHelper.convertDpToPixels(AssignIconsActivity.this, dpSpace);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.left = _space;
outRect.right = _space;
outRect.bottom = _space;
if (parent.getChildLayoutPosition(view) == 0) {
outRect.top = _space;
}
}
}
}
@@ -2,7 +2,6 @@ package com.beemdevelopment.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
import android.view.KeyEvent;
@@ -20,11 +19,9 @@ import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.crypto.MasterKey;
@@ -36,6 +33,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@@ -43,6 +41,7 @@ import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.List;
@@ -55,10 +54,13 @@ public class AuthActivity extends AegisActivity {
private EditText _textPassword;
private VaultFile _vaultFile;
private SlotList _slots;
private SecretKey _bioKey;
private BiometricSlot _bioSlot;
private BiometricPrompt _bioPrompt;
private Button _decryptButton;
private int _failedUnlockAttempts;
@@ -72,14 +74,14 @@ public class AuthActivity extends AegisActivity {
setContentView(R.layout.activity_auth);
_textPassword = findViewById(R.id.text_password);
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
Button decryptButton = findViewById(R.id.button_decrypt);
_decryptButton = findViewById(R.id.button_decrypt);
TextView biometricsButton = findViewById(R.id.button_biometrics);
getOnBackPressedDispatcher().addCallback(this, new BackPressHandler());
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
decryptButton.performClick();
_decryptButton.performClick();
}
return false;
});
@@ -104,17 +106,17 @@ public class AuthActivity extends AegisActivity {
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
}
if (_vaultManager.getVaultFileError() != null) {
Dialogs.showErrorDialog(this, R.string.vault_load_error, _vaultManager.getVaultFileError(), (dialog, which) -> {
try {
_vaultFile = VaultRepository.readVaultFile(this);
} catch (VaultRepositoryException e) {
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
getOnBackPressedDispatcher().onBackPressed();
});
return;
}
VaultFile vaultFile = _vaultManager.getVaultFile();
_slots = vaultFile.getHeader().getSlots();
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
_slots = _vaultFile.getHeader().getSlots();
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
boolean invalidated = false;
@@ -150,7 +152,7 @@ public class AuthActivity extends AegisActivity {
}
}
decryptButton.setOnClickListener(v -> {
_decryptButton.setOnClickListener(v -> {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
@@ -159,10 +161,24 @@ public class AuthActivity extends AegisActivity {
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener());
task.execute(getLifecycle(), params);
_decryptButton.setEnabled(false);
});
biometricsButton.setOnClickListener(v -> {
showBiometricPrompt();
if (_prefs.isPasswordReminderNeeded()) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(getString(R.string.password_reminder_dialog_title))
.setMessage(getString(R.string.password_reminder_dialog_message))
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
showBiometricPrompt();
})
.create());
} else {
showBiometricPrompt();
}
});
}
@@ -172,11 +188,6 @@ public class AuthActivity extends AegisActivity {
outState.putBoolean("inhibitBioPrompt", _inhibitBioPrompt);
}
@Override
protected void onSetTheme() {
setTheme(ThemeMap.NO_ACTION_BAR);
}
private void selectPassword() {
_textPassword.selectAll();
@@ -229,11 +240,8 @@ public class AuthActivity extends AegisActivity {
PopupWindow popup = new PopupWindow(popupLayout, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
popup.setFocusable(false);
popup.setOutsideTouchable(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
popup.setElevation(5.0f);
}
_textPassword.post(() -> {
if (isFinishing()) {
if (isFinishing() || !_textPassword.isAttachedToWindow()) {
return;
}
@@ -276,7 +284,7 @@ public class AuthActivity extends AegisActivity {
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
try {
_vaultManager.unlock(creds);
_vaultManager.loadFrom(_vaultFile, creds);
if (isSlotRepaired) {
saveAndBackupVault();
}
@@ -291,10 +299,11 @@ public class AuthActivity extends AegisActivity {
}
private void onInvalidPassword() {
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(AuthActivity.this, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(getString(R.string.unlock_vault_error))
.setMessage(getString(R.string.unlock_vault_error_description))
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
.create());
@@ -334,6 +343,9 @@ public class AuthActivity extends AegisActivity {
finish(result.getKey(), result.isSlotRepaired());
} else {
_decryptButton.setEnabled(true);
_auditLogRepository.addVaultUnlockFailedPasswordEvent();
onInvalidPassword();
}
}
@@ -346,6 +358,7 @@ public class AuthActivity extends AegisActivity {
_bioPrompt = null;
if (!BiometricsHelper.isCanceled(errorCode)) {
_auditLogRepository.addVaultUnlockFailedBiometricsEvent();
Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show();
}
}
@@ -1,7 +1,6 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
@@ -15,17 +14,19 @@ import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import com.amulyakhare.textdrawable.TextDrawable;
import com.avito.android.krop.KropView;
@@ -33,9 +34,9 @@ import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.SafHelper;
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
import com.beemdevelopment.aegis.helpers.SimpleTextWatcher;
@@ -52,18 +53,25 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.otp.YandexInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.imageview.ShapeableImageView;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
@@ -71,34 +79,36 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.TreeSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import de.hdodenhof.circleimageview.CircleImageView;
public class EditEntryActivity extends AegisActivity {
private static final int PICK_IMAGE_REQUEST = 0;
private boolean _isNew = false;
private boolean _isManual = false;
private VaultEntry _origEntry;
private TreeSet<String> _groups;
private Collection<VaultGroup> _groups;
private boolean _hasCustomIcon = false;
// keep track of icon changes separately as the generated jpeg's are not deterministic
private boolean _hasChangedIcon = false;
private IconPack.Icon _selectedIcon;
private CircleImageView _iconView;
private ShapeableImageView _iconView;
private ImageView _saveImageButton;
private TextInputEditText _textName;
private TextInputEditText _textIssuer;
private TextInputLayout _textGroupLayout;
private TextInputEditText _textGroup;
private TextInputEditText _textPeriodCounter;
private TextInputLayout _textPeriodCounterLayout;
private TextInputEditText _textDigits;
@@ -108,21 +118,44 @@ public class EditEntryActivity extends AegisActivity {
private LinearLayout _textPinLayout;
private TextInputEditText _textUsageCount;
private TextInputEditText _textNote;
private TextView _textLastUsed;
private AutoCompleteTextView _dropdownType;
private AutoCompleteTextView _dropdownAlgo;
private TextInputLayout _dropdownAlgoLayout;
private AutoCompleteTextView _dropdownGroup;
private List<String> _dropdownGroupList = new ArrayList<>();
private List<UUID> _selectedGroups = new ArrayList<>();
private KropView _kropView;
private RelativeLayout _advancedSettingsHeader;
private RelativeLayout _advancedSettings;
private LinearLayout _advancedSettingsLayout;
private BackPressHandler _backPressHandler;
private IconBackPressHandler _iconBackPressHandler;
private final ActivityResultLauncher<Intent> pickImageResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
Intent data = activityResult.getData();
if (activityResult.getResultCode() != RESULT_OK || data == null || data.getData() == null) {
return;
}
String fileType = SafHelper.getMimeType(this, data.getData());
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
ImportFileTask task = new ImportFileTask(this, result -> {
if (result.getError() == null) {
CustomSvgIcon icon = new CustomSvgIcon(result.getFile());
selectIcon(icon);
} else {
Dialogs.showErrorDialog(this, R.string.reading_file_error, result.getError());
}
});
task.execute(getLifecycle(), params);
} else {
startEditingIcon(data.getData());
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -136,7 +169,7 @@ public class EditEntryActivity extends AegisActivity {
ActionBar bar = getSupportActionBar();
if (bar != null) {
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setHomeAsUpIndicator(R.drawable.ic_outline_close_24);
bar.setDisplayHomeAsUpEnabled(true);
}
@@ -163,6 +196,8 @@ public class EditEntryActivity extends AegisActivity {
_saveImageButton = findViewById(R.id.iv_saveImage);
_textName = findViewById(R.id.text_name);
_textIssuer = findViewById(R.id.text_issuer);
_textGroup = findViewById(R.id.text_group);
_textGroupLayout = findViewById(R.id.text_group_layout);
_textPeriodCounter = findViewById(R.id.text_period_counter);
_textPeriodCounterLayout = findViewById(R.id.text_period_counter_layout);
_textDigits = findViewById(R.id.text_digits);
@@ -172,14 +207,12 @@ public class EditEntryActivity extends AegisActivity {
_textPinLayout = findViewById(R.id.layout_pin);
_textUsageCount = findViewById(R.id.text_usage_count);
_textNote = findViewById(R.id.text_note);
_textLastUsed = findViewById(R.id.text_last_used);
_dropdownType = findViewById(R.id.dropdown_type);
DropdownHelper.fillDropdown(this, _dropdownType, R.array.otp_types_array);
_dropdownAlgoLayout = findViewById(R.id.dropdown_algo_layout);
_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 || !_isManual) {
@@ -208,22 +241,12 @@ public class EditEntryActivity extends AegisActivity {
_advancedSettingsHeader = findViewById(R.id.accordian_header);
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
_advancedSettings = findViewById(R.id.expandableLayout);
_advancedSettingsLayout = findViewById(R.id.layout_advanced);
// fill the fields with values if possible
GlideHelper.loadEntryIcon(Glide.with(this), _origEntry, _iconView);
if (_origEntry.hasIcon()) {
IconViewHelper.setLayerType(_iconView, _origEntry.getIconType());
Glide.with(this)
.asDrawable()
.load(_origEntry)
.set(IconLoader.ICON_TYPE, _origEntry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
_hasCustomIcon = true;
} else {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
}
_textName.setText(_origEntry.getName());
@@ -261,8 +284,18 @@ public class EditEntryActivity extends AegisActivity {
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
updatePinFieldVisibility(_origEntry.getInfo().getTypeId());
String group = _origEntry.getGroup();
setGroup(group);
Set<UUID> groups = _origEntry.getGroups();
if (groups.isEmpty()) {
_textGroup.setText(getString(R.string.no_group));
} else {
String text = groups.stream().map(uuid -> {
VaultGroup group = _vaultManager.getVault().getGroupByUUID(uuid);
return group.getName();
})
.collect(Collectors.joining(", "));
_selectedGroups.addAll(groups);
_textGroup.setText(text);
}
// Update the icon if the issuer or name has changed
_textIssuer.addTextChangedListener(_nameChangeListener);
@@ -270,11 +303,11 @@ public class EditEntryActivity extends AegisActivity {
// Register listeners to trigger validation
_textIssuer.addTextChangedListener(_validationListener);
_textGroup.addTextChangedListener(_validationListener);
_textName.addTextChangedListener(_validationListener);
_textNote.addTextChangedListener(_validationListener);
_textSecret.addTextChangedListener(_validationListener);
_dropdownType.addTextChangedListener(_validationListener);
_dropdownGroup.addTextChangedListener(_validationListener);
_dropdownAlgo.addTextChangedListener(_validationListener);
_textPeriodCounter.addTextChangedListener(_validationListener);
_textDigits.addTextChangedListener(_validationListener);
@@ -326,28 +359,107 @@ public class EditEntryActivity extends AegisActivity {
startIconSelection();
});
_dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
private int prevPosition = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (position == _dropdownGroupList.size() - 1) {
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, text -> {
String groupName = new String(text);
if (!groupName.isEmpty()) {
_groups.add(groupName);
updateGroupDropdownList();
_dropdownGroup.setText(groupName, false);
}
});
_dropdownGroup.setText(_dropdownGroupList.get(prevPosition), false);
} else {
prevPosition = position;
}
_textGroup.setShowSoftInputOnFocus(false);
_textGroup.setOnClickListener(v -> showGroupSelectionDialog());
_textGroup.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
showGroupSelectionDialog();
}
});
_textGroupLayout.setOnClickListener(v -> {
showGroupSelectionDialog();
});
_textUsageCount.setText(_prefs.getUsageCount(entryUUID).toString());
setLastUsedTimestamp(_prefs.getLastUsedTimestamp(entryUUID));
}
private void showGroupSelectionDialog() {
BottomSheetDialog dialog = new BottomSheetDialog(this);
View view = getLayoutInflater().inflate(R.layout.dialog_select_groups, null);
dialog.setContentView(view);
ChipGroup chipGroup = view.findViewById(R.id.groupChipGroup);
TextView addGroupInfo = view.findViewById(R.id.addGroupInfo);
LinearLayout addGroup = view.findViewById(R.id.addGroup);
Button clearButton = view.findViewById(R.id.btnClear);
Button saveButton = view.findViewById(R.id.btnSave);
chipGroup.removeAllViews();
addGroupInfo.setVisibility(View.VISIBLE);
addGroup.setVisibility(View.VISIBLE);
for (VaultGroup group : _groups) {
addChipTo(chipGroup, new VaultGroupModel(group), false);
}
addGroup.setOnClickListener(v1 -> {
Dialogs.TextInputListener onAddGroup = text -> {
String groupName = new String(text).trim();
if (!groupName.isEmpty()) {
VaultGroup group = _vaultManager.getVault().findGroupByName(groupName);
if (group == null) {
group = new VaultGroup(groupName);
_vaultManager.getVault().addGroup(group);
}
_selectedGroups.add(group.getUUID());
addChipTo(chipGroup, new VaultGroupModel(group), true);
}
};
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, onAddGroup);
});
saveButton.setOnClickListener(v1 -> {
if(getCheckedUUID(chipGroup).isEmpty()) {
_selectedGroups.clear();
_textGroup.setText(getString(R.string.no_group));
} else {
_selectedGroups.clear();
_selectedGroups.addAll(getCheckedUUID(chipGroup));
_textGroup.setText(getCheckedNames(chipGroup));
}
dialog.dismiss();
});
clearButton.setOnClickListener(v1 -> {
chipGroup.clearCheck();
});
Dialogs.showSecureDialog(dialog);
}
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group, Boolean isNew) {
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
chip.setText(group.getName());
chip.setCheckable(true);
chip.setChecked((!_selectedGroups.isEmpty() && _selectedGroups.contains(group.getUUID())) || isNew);
chip.setCheckedIconVisible(true);
chip.setTag(group);
chipGroup.addView(chip);
}
private static Set<UUID> getCheckedUUID(ChipGroup chipGroup) {
return chipGroup.getCheckedChipIds().stream()
.map(i -> {
Chip chip = chipGroup.findViewById(i);
VaultGroupModel group = (VaultGroupModel) chip.getTag();
return group.getUUID();
})
.collect(Collectors.toSet());
}
private static String getCheckedNames(ChipGroup chipGroup) {
return chipGroup.getCheckedChipIds().stream()
.map(i -> {
Chip chip = chipGroup.findViewById(i);
VaultGroupModel group = (VaultGroupModel) chip.getTag();
return group.getName();
})
.collect(Collectors.joining(", "));
}
private void updateAdvancedFieldStatus(String otpType) {
@@ -364,41 +476,20 @@ public class EditEntryActivity extends AegisActivity {
_textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin);
}
private void setGroup(String groupName) {
int pos = 0;
if (groupName != null) {
pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() + 1 : 0;
}
_dropdownGroup.setText(_dropdownGroupList.get(pos), false);
}
private void openAdvancedSettings() {
Animation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new AccelerateInterpolator());
fadeOut.setDuration(220);
fadeOut.setDuration((long) (220 * AnimationsHelper.Scale.ANIMATOR.getValue(this)));
_advancedSettingsHeader.startAnimation(fadeOut);
Animation fadeIn = new AlphaAnimation(0, 1);
fadeIn.setInterpolator(new AccelerateInterpolator());
fadeIn.setDuration(250);
fadeOut.setAnimationListener(new SimpleAnimationEndListener((a) -> {
_advancedSettingsHeader.setVisibility(View.GONE);
_advancedSettings.startAnimation(fadeIn);
_advancedSettingsLayout.setVisibility(View.VISIBLE);
_advancedSettingsLayout.animate()
.setInterpolator(new AccelerateInterpolator())
.setDuration((long) (250 * AnimationsHelper.Scale.ANIMATOR.getValue(this)))
.alpha(1);
}));
fadeIn.setAnimationListener(new SimpleAnimationEndListener((a) -> {
_advancedSettings.setVisibility(View.VISIBLE);
}));
}
private void updateGroupDropdownList() {
Resources res = getResources();
_dropdownGroupList.clear();
_dropdownGroupList.add(res.getString(R.string.no_group));
_dropdownGroupList.addAll(_groups);
_dropdownGroupList.add(res.getString(R.string.new_group));
}
private boolean hasUnsavedChanges(VaultEntry newEntry) {
@@ -436,38 +527,33 @@ public class EditEntryActivity extends AegisActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
discardAndFinish();
break;
case R.id.action_save:
onSave();
break;
case R.id.action_delete:
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry), (dialog, which) -> {
deleteAndFinish(_origEntry);
});
break;
case R.id.action_edit_icon:
startIconSelection();
break;
case R.id.action_reset_usage_count:
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.action_reset_usage_count)
.setMessage(R.string.action_reset_usage_count_dialog)
.setPositiveButton(android.R.string.yes, (dialog, which) -> resetUsageCount())
.setNegativeButton(android.R.string.no, null)
.create());
break;
case R.id.action_default_icon:
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
discardAndFinish();
} else if (itemId == R.id.action_save) {
onSave();
} else if (itemId == R.id.action_delete) {
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry), (dialog, which) -> {
deleteAndFinish(_origEntry);
});
} else if (itemId == R.id.action_edit_icon) {
startIconSelection();
} else if (itemId == R.id.action_reset_usage_count) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this)
.setTitle(R.string.action_reset_usage_count)
.setMessage(R.string.action_reset_usage_count_dialog)
.setPositiveButton(android.R.string.yes, (dialog, which) -> resetUsageCount())
.setNegativeButton(android.R.string.no, null)
.create());
} else if (itemId == R.id.action_default_icon) {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
_selectedIcon = null;
_hasCustomIcon = false;
_hasChangedIcon = true;
default:
return super.onOptionsItemSelected(item);
_selectedIcon = null;
_hasCustomIcon = false;
_hasChangedIcon = true;
} else {
return super.onOptionsItemSelected(item);
}
return true;
@@ -482,7 +568,7 @@ public class EditEntryActivity extends AegisActivity {
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
_vaultManager.startActivityForResult(this, chooserIntent, PICK_IMAGE_REQUEST);
_vaultManager.fireIntentLauncher(this, chooserIntent, pickImageResultLauncher);
}
private void resetUsageCount() {
@@ -499,7 +585,7 @@ public class EditEntryActivity extends AegisActivity {
return;
}
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), new IconAdapter.Listener() {
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), true, new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
selectIcon(icon);
@@ -518,14 +604,7 @@ public class EditEntryActivity extends AegisActivity {
_hasCustomIcon = true;
_hasChangedIcon = true;
IconViewHelper.setLayerType(_iconView, icon.getIconType());
Glide.with(EditEntryActivity.this)
.asDrawable()
.load(icon.getFile())
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
GlideHelper.loadIcon(Glide.with(EditEntryActivity.this), icon, _iconView);
}
private void startEditingIcon(Uri data) {
@@ -595,6 +674,16 @@ public class EditEntryActivity extends AegisActivity {
saveAndFinish(entry, false);
}
private void setLastUsedTimestamp(long timestamp) {
String readableDate = getString(R.string.last_used_never);
if (timestamp != 0) {
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.getDefault());
readableDate = dateFormat.format(new Date(timestamp));
}
_textLastUsed.setText(String.format("%s: %s", getString(R.string.last_used), readableDate));
}
private void deleteAndFinish(VaultEntry entry) {
_vaultManager.getVault().removeEntry(entry);
saveAndFinish(entry, true);
@@ -611,29 +700,6 @@ public class EditEntryActivity extends AegisActivity {
}
}
@Override
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
String fileType = SafHelper.getMimeType(this, data.getData());
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
ImportFileTask task = new ImportFileTask(this, result -> {
if (result.getError() == null) {
CustomSvgIcon icon = new CustomSvgIcon(result.getFile());
selectIcon(icon);
} else {
Dialogs.showErrorDialog(this, R.string.reading_file_error, result.getError());
}
});
task.execute(getLifecycle(), params);
} else {
startEditingIcon(data.getData());
}
}
super.onActivityResult(requestCode, resultCode, data);
}
private int parsePeriod() throws ParseException {
try {
return Integer.parseInt(_textPeriodCounter.getText().toString());
@@ -725,23 +791,22 @@ public class EditEntryActivity extends AegisActivity {
entry.setName(_textName.getText().toString());
entry.setNote(_textNote.getText().toString());
int groupPos = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
if (groupPos != 0) {
String group = _dropdownGroupList.get(groupPos);
entry.setGroup(group);
if (_selectedGroups.isEmpty()) {
entry.setGroups(new HashSet<>());
} else {
entry.setGroup(null);
entry.setGroups(new HashSet<>(_selectedGroups));
}
if (_hasChangedIcon) {
if (_hasCustomIcon) {
VaultEntryIcon icon;
if (_selectedIcon == null) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// the quality parameter is ignored for PNG
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] data = stream.toByteArray();
entry.setIcon(data, IconType.PNG);
icon = new VaultEntryIcon(data, IconType.PNG);
} else {
byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
@@ -749,11 +814,12 @@ public class EditEntryActivity extends AegisActivity {
} catch (IOException e) {
throw new ParseException(e.getMessage());
}
entry.setIcon(iconBytes, _selectedIcon.getIconType());
icon = new VaultEntryIcon(iconBytes, _selectedIcon.getIconType());
}
entry.setIcon(icon);
} else {
entry.setIcon(null, IconType.INVALID);
entry.setIcon(null);
}
}
@@ -761,9 +827,10 @@ public class EditEntryActivity extends AegisActivity {
}
private void onSaveError(String msg) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(getString(R.string.saving_profile_error))
.setMessage(msg)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, null)
.create());
}
@@ -851,11 +918,12 @@ public class EditEntryActivity extends AegisActivity {
private final File _file;
protected CustomSvgIcon(File file) {
super(file.getAbsolutePath(), null, null);
super(file.getAbsolutePath(), null, null, null);
_file = file;
}
@Nullable
@Override
public File getFile() {
return _file;
}
@@ -7,24 +7,31 @@ import android.view.View;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.vault.VaultEntryException;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
private GroupAdapter _adapter;
private HashSet<String> _removedGroups;
private RecyclerView _slotsView;
private HashSet<UUID> _removedGroups;
private HashSet<VaultGroup> _renamedGroups;
private RecyclerView _groupsView;
private View _emptyStateView;
private BackPressHandler _backPressHandler;
@@ -43,24 +50,45 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
_backPressHandler = new BackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
_removedGroups = new HashSet<>();
_renamedGroups = new HashSet<>();
if (savedInstanceState != null) {
List<String> groups = savedInstanceState.getStringArrayList("removedGroups");
_removedGroups = new HashSet<>(Objects.requireNonNull(groups));
} else {
_removedGroups = new HashSet<>();
List<String> removedGroups = savedInstanceState.getStringArrayList("removedGroups");
List<String> renamedGroups = savedInstanceState.getStringArrayList("renamedGroups");
if (removedGroups != null) {
for (String uuid : removedGroups) {
_removedGroups.add(UUID.fromString(uuid));
}
}
if (renamedGroups != null) {
for (String groupObject : renamedGroups) {
try {
_renamedGroups.add(VaultGroup.fromJson(new JSONObject(groupObject)));
} catch (VaultEntryException | JSONException ignored) {
// This is intentionally ignored since the json object is valid
}
}
}
}
_adapter = new GroupAdapter(this);
_slotsView= findViewById(R.id.list_slots);
_groupsView = findViewById(R.id.list_groups);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
_slotsView.setLayoutManager(layoutManager);
_slotsView.setAdapter(_adapter);
_slotsView.setNestedScrollingEnabled(false);
_groupsView.setLayoutManager(layoutManager);
_groupsView.setAdapter(_adapter);
_groupsView.setNestedScrollingEnabled(false);
for (String group : _vaultManager.getVault().getGroups()) {
_adapter.addGroup(group);
for (VaultGroup group : _vaultManager.getVault().getGroups()) {
if (!_removedGroups.contains(group.getUUID())) {
_adapter.addGroup(group);
}
}
for(VaultGroup group: _renamedGroups) {
_adapter.replaceGroup(group.getUUID(), group);
}
_emptyStateView = findViewById(R.id.vEmptyList);
updateEmptyState();
}
@@ -68,16 +96,43 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putStringArrayList("removedGroups", new ArrayList<>(_removedGroups));
ArrayList<String> removed = new ArrayList<>();
for (UUID uuid : _removedGroups) {
removed.add(uuid.toString());
}
ArrayList<String> renamed = new ArrayList<>();
for (VaultGroup group : _renamedGroups) {
renamed.add(group.toJson().toString());
}
outState.putStringArrayList("renamedGroups", renamed);
outState.putStringArrayList("removedGroups", removed);
}
@Override
public void onRemoveGroup(String group) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
public void onEditGroup(VaultGroup group) {
Dialogs.TextInputListener onEditGroup = text -> {
String newGroupName = new String(text).trim();
if (!newGroupName.isEmpty()) {
VaultGroup newGroup = Cloner.clone(group);
newGroup.setName(newGroupName);
_renamedGroups.add(newGroup);
_adapter.replaceGroup(group.getUUID(), newGroup);
_backPressHandler.setEnabled(true);
}
};
Dialogs.showTextInputDialog(GroupManagerActivity.this, R.string.rename_group, R.string.group_name_hint, onEditGroup, group.getName());
}
@Override
public void onRemoveGroup(VaultGroup group) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.remove_group)
.setMessage(R.string.remove_group_description)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
_removedGroups.add(group);
_removedGroups.add(group.getUUID());
_adapter.removeGroup(group);
_backPressHandler.setEnabled(true);
updateEmptyState();
@@ -86,12 +141,38 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
.create());
}
public void onRemoveUnusedGroups() {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.remove_unused_groups)
.setMessage(R.string.remove_unused_groups_description)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
Set<VaultGroup> unusedGroups = new HashSet<>(_vaultManager.getVault().getGroups());
unusedGroups.removeAll(_vaultManager.getVault().getUsedGroups());
for (VaultGroup group : unusedGroups) {
_removedGroups.add(group.getUUID());
_adapter.removeGroup(group);
}
_backPressHandler.setEnabled(true);
updateEmptyState();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
private void saveAndFinish() {
if (!_removedGroups.isEmpty()) {
for (VaultEntry entry : _vaultManager.getVault().getEntries()) {
if (_removedGroups.contains(entry.getGroup())) {
entry.setGroup(null);
}
for (UUID uuid : _removedGroups) {
_vaultManager.getVault().removeGroup(uuid);
}
saveAndBackupVault();
}
if (!_renamedGroups.isEmpty()) {
_renamedGroups.removeIf(group -> _removedGroups.contains(group.getUUID()));
for (VaultGroup renamedGroup : _renamedGroups) {
_vaultManager.getVault().renameGroup(renamedGroup);
}
saveAndBackupVault();
@@ -101,7 +182,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
}
private void discardAndFinish() {
if (_removedGroups.isEmpty()) {
if (_removedGroups.isEmpty() && _renamedGroups.isEmpty()) {
finish();
return;
}
@@ -119,15 +200,15 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
discardAndFinish();
break;
case R.id.action_save:
saveAndFinish();
break;
default:
return super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
discardAndFinish();
} else if (itemId == R.id.action_save) {
saveAndFinish();
} else if (itemId == R.id.action_delete_unused_groups) {
onRemoveUnusedGroups();
} else {
return super.onOptionsItemSelected(item);
}
return true;
@@ -135,10 +216,10 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
private void updateEmptyState() {
if (_adapter.getItemCount() > 0) {
_slotsView.setVisibility(View.VISIBLE);
_groupsView.setVisibility(View.VISIBLE);
_emptyStateView.setVisibility(View.GONE);
} else {
_slotsView.setVisibility(View.GONE);
_groupsView.setVisibility(View.GONE);
_emptyStateView.setVisibility(View.VISIBLE);
}
}
@@ -1,15 +1,16 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -24,21 +25,31 @@ import com.beemdevelopment.aegis.ui.tasks.RootShellTask;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class ImportEntriesActivity extends AegisActivity {
private View _view;
private Menu _menu;
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
private UUIDMap<VaultGroup> _importedGroups;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -48,8 +59,10 @@ public class ImportEntriesActivity extends AegisActivity {
setContentView(R.layout.activity_import_entries);
setSupportActionBar(findViewById(R.id.toolbar));
_view = findViewById(R.id.importEntriesRootView);
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setHomeAsUpIndicator(R.drawable.ic_outline_close_24);
bar.setDisplayHomeAsUpEnabled(true);
_adapter = new ImportEntriesAdapter();
@@ -88,10 +101,11 @@ public class ImportEntriesActivity extends AegisActivity {
if (importer.isInstalledAppVersionSupported()) {
startImportApp(importer);
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.warning)
.setMessage(getString(R.string.app_version_error, importerDef.getName()))
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(R.string.yes, (dialog1, which) -> {
startImportApp(importer);
})
@@ -180,6 +194,7 @@ public class ImportEntriesActivity extends AegisActivity {
}
private void importDatabase(DatabaseImporter.State state) {
List<ImportEntry> importEntries = new ArrayList<>();
DatabaseImporter.Result result;
try {
result = state.convert();
@@ -191,14 +206,20 @@ public class ImportEntriesActivity extends AegisActivity {
UUIDMap<VaultEntry> entries = result.getEntries();
for (VaultEntry entry : entries.getValues()) {
_adapter.addEntry(new ImportEntry(entry));
ImportEntry importEntry = new ImportEntry(entry);
_adapter.addEntry(importEntry);
importEntries.add(importEntry);
}
_importedGroups = result.getGroups();
List<DatabaseImporterEntryException> errors = result.getErrors();
if (errors.size() > 0) {
String message = getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size());
Dialogs.showMultiErrorDialog(this, R.string.import_error_title, message, errors, null);
Dialogs.showMultiExceptionDialog(this, R.string.import_error_title, message, errors, null);
}
findDuplicates(importEntries);
}
private void showWipeEntriesDialog() {
@@ -212,10 +233,43 @@ public class ImportEntriesActivity extends AegisActivity {
private void saveAndFinish(boolean wipeEntries) {
VaultRepository vault = _vaultManager.getVault();
if (wipeEntries) {
vault.wipeEntries();
vault.wipeContents();
}
// Given the list of selected entries, collect the UUID's of all groups
// that we're actually going to import
List<ImportEntry> selectedEntries = _adapter.getCheckedEntries();
List<UUID> selectedGroupUuids = new ArrayList<>();
for (ImportEntry entry : selectedEntries) {
selectedGroupUuids.addAll(entry.getEntry().getGroups());
}
// Add all of the new groups to the vault. If a group with the same name already
// exists in the vault, rewrite all entries in that group to reference the existing group.
for (VaultGroup importedGroup : _importedGroups) {
if (!selectedGroupUuids.contains(importedGroup.getUUID())) {
continue;
}
VaultGroup existingGroup = vault.findGroupByUUID(importedGroup.getUUID());
if (existingGroup != null) {
continue;
}
existingGroup = vault.findGroupByName(importedGroup.getName());
if (existingGroup == null) {
vault.addGroup(importedGroup);
} else {
for (ImportEntry entry : selectedEntries) {
Set<UUID> entryGroups = entry.getEntry().getGroups();
if (entryGroups.contains(importedGroup.getUUID())) {
entryGroups.remove(importedGroup.getUUID());
entryGroups.add(existingGroup.getUUID());
}
}
}
}
for (ImportEntry selectedEntry : selectedEntries) {
VaultEntry entry = selectedEntry.getEntry();
@@ -231,11 +285,60 @@ public class ImportEntriesActivity extends AegisActivity {
String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size());
Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show();
setResult(RESULT_OK, null);
finish();
if (_iconPackManager.hasIconPack()) {
ArrayList<UUID> assignIconEntriesIds = new ArrayList<>();
Intent assignIconIntent = new Intent(getBaseContext(), AssignIconsActivity.class);
for (ImportEntry entry : selectedEntries) {
assignIconEntriesIds.add(entry.getEntry().getUUID());
}
assignIconIntent.putExtra("entries", assignIconEntriesIds);
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this)
.setTitle(R.string.import_assign_icons_dialog_title)
.setMessage(R.string.import_assign_icons_dialog_text)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
startActivity(assignIconIntent);
finish();
})
.setNegativeButton(android.R.string.no, ((dialogInterface, i) -> finish()))
.create());
} else {
finish();
}
}
}
private void findDuplicates(List<ImportEntry> importEntries) {
List<UUID> duplicateEntries = new ArrayList<>();
for (ImportEntry importEntry: importEntries) {
boolean exists = _vaultManager.getVault().getEntries().stream().anyMatch(item ->
item.getIssuer().equals(importEntry.getEntry().getIssuer()) &&
Arrays.equals(item.getInfo().getSecret(), importEntry.getEntry().getInfo().getSecret()));
if (exists) {
duplicateEntries.add(importEntry.getEntry().getUUID());
}
}
if (duplicateEntries.size() == 0) {
return;
}
_adapter.setCheckboxStates(duplicateEntries, false);
Snackbar snackbar = Snackbar.make(_view, getResources().getQuantityString(R.plurals.import_duplicate_toast, duplicateEntries.size(), duplicateEntries.size()), Snackbar.LENGTH_INDEFINITE);
snackbar.setAction(R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
_adapter.setCheckboxStates(duplicateEntries, true);
}
});
snackbar.show();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
@@ -245,18 +348,15 @@ public class ImportEntriesActivity extends AegisActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
break;
case R.id.toggle_checkboxes:
_adapter.toggleCheckboxes();
break;
case R.id.toggle_wipe_vault:
item.setChecked(!item.isChecked());
break;
default:
return super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
} else if (itemId == R.id.toggle_checkboxes) {
_adapter.toggleCheckboxes();
} else if (itemId == R.id.toggle_wipe_vault) {
item.setChecked(!item.isChecked());
} else {
return super.onOptionsItemSelected(item);
}
return true;
@@ -13,7 +13,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.intro.IntroBaseActivity;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
@@ -21,7 +20,9 @@ import com.beemdevelopment.aegis.ui.slides.DoneSlide;
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
import com.beemdevelopment.aegis.ui.slides.WelcomeSlide;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@@ -40,11 +41,6 @@ public class IntroActivity extends IntroBaseActivity {
addSlide(DoneSlide.class);
}
@Override
protected void onSetTheme() {
setTheme(ThemeMap.NO_ACTION_BAR);
}
@Override
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, @NonNull Class<? extends SlideFragment> newSlide) {
// hide the keyboard before every slide change
@@ -108,8 +104,17 @@ public class IntroActivity extends IntroBaseActivity {
return;
}
} else {
VaultFile vaultFile;
try {
_vaultManager.load(creds);
vaultFile = VaultRepository.readVaultFile(this);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e);
return;
}
try {
_vaultManager.loadFrom(vaultFile, creds);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e);
@@ -0,0 +1,40 @@
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.mikepenz.aboutlibraries.LibsBuilder;
import com.mikepenz.aboutlibraries.ui.LibsActivity;
import org.jetbrains.annotations.Nullable;
import dagger.hilt.InstallIn;
import dagger.hilt.android.EarlyEntryPoint;
import dagger.hilt.android.EarlyEntryPoints;
import dagger.hilt.components.SingletonComponent;
public class LicensesActivity extends LibsActivity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
LibsBuilder builder = new LibsBuilder()
.withSearchEnabled(true)
.withAboutMinimalDesign(true)
.withActivityTitle(getString(R.string.title_activity_licenses));
setIntent(builder.intent(this));
Preferences _prefs = EarlyEntryPoints.get(getApplicationContext(), PrefEntryPoint.class).getPreferences();
ThemeHelper themeHelper = new ThemeHelper(this, _prefs);
themeHelper.setTheme(ThemeMap.DEFAULT);
super.onCreate(savedInstanceState);
}
@EarlyEntryPoint
@InstallIn(SingletonComponent.class)
public interface PrefEntryPoint {
Preferences getPreferences();
}
}
@@ -23,22 +23,24 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
@@ -47,11 +49,16 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import com.beemdevelopment.aegis.util.TimeUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.common.base.Strings;
@@ -61,20 +68,11 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeSet;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class MainActivity extends AegisActivity implements EntryListView.Listener {
// activity request codes
private static final int CODE_SCAN = 0;
private static final int CODE_ADD_ENTRY = 1;
private static final int CODE_EDIT_ENTRY = 2;
private static final int CODE_DO_INTRO = 3;
private static final int CODE_DECRYPT = 4;
private static final int CODE_PREFERENCES = 5;
private static final int CODE_SCAN_IMAGE = 6;
// Permission request codes
private static final int CODE_PERM_CAMERA = 0;
@@ -92,8 +90,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private Menu _menu;
private SearchView _searchView;
private EntryListView _entryListView;
private LinearLayout _btnErrorBar;
private TextView _textErrorBar;
private FabScrollHelper _fabScrollHelper;
@@ -104,6 +100,64 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private SearchViewBackPressHandler _searchViewBackPressHandler;
private ActionModeBackPressHandler _actionModeBackPressHandler;
private final ActivityResultLauncher<Intent> authResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
_isAuthenticating = false;
if (activityResult.getResultCode() == RESULT_OK) {
onDecryptResult();
}
});
private final ActivityResultLauncher<Intent> introResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
_isDoingIntro = false;
if (activityResult.getResultCode() == RESULT_OK) {
onIntroResult();
}
});
private final ActivityResultLauncher<Intent> scanResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
return;
}
onScanResult(activityResult.getData());
});
private final ActivityResultLauncher<Intent> assignIconsResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
return;
}
onAssignEntriesResult(activityResult.getData());
});
private final ActivityResultLauncher<Intent> preferenceResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> onPreferencesResult());
private final ActivityResultLauncher<Intent> editEntryResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
return;
}
onEditEntryResult(activityResult.getData());
});
private final ActivityResultLauncher<Intent> addEntryResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
return;
}
onAddEntryResult(activityResult.getData());
});
private final ActivityResultLauncher<Intent> codeScanResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
if (activityResult.getResultCode() == RESULT_OK && activityResult.getData() != null) {
onScanImageResult(activityResult.getData());
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -131,15 +185,17 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
_entryListView.setListener(this);
_entryListView.setCodeGroupSize(_prefs.getCodeGroupSize());
_entryListView.setShowAccountName(_prefs.isAccountNameVisible());
_entryListView.setAccountNamePosition(_prefs.getAccountNamePosition());
_entryListView.setShowIcon(_prefs.isIconVisible());
_entryListView.setOnlyShowNecessaryAccountNames(_prefs.onlyShowNecessaryAccountNames());
_entryListView.setHighlightEntry(_prefs.isEntryHighlightEnabled());
_entryListView.setPauseFocused(_prefs.isPauseFocusedEnabled());
_entryListView.setTapToReveal(_prefs.isTapToRevealEnabled());
_entryListView.setTapToRevealTime(_prefs.getTapToRevealTime());
_entryListView.setSortCategory(_prefs.getCurrentSortCategory(), false);
_entryListView.setViewMode(_prefs.getCurrentViewMode());
_entryListView.setIsCopyOnTapEnabled(_prefs.isCopyOnTapEnabled());
_entryListView.setCopyBehavior(_prefs.getCopyBehavior());
_entryListView.setSearchBehaviorMask(_prefs.getSearchBehaviorMask());
_entryListView.setPrefGroupFilter(_prefs.getGroupFilter());
FloatingActionButton fab = findViewById(R.id.fab);
@@ -150,7 +206,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
view.findViewById(R.id.fab_enter).setOnClickListener(v1 -> {
dialog.dismiss();
startEditEntryActivityForManual(CODE_ADD_ENTRY);
startEditEntryActivityForManual();
});
view.findViewById(R.id.fab_scan_image).setOnClickListener(v2 -> {
dialog.dismiss();
@@ -164,9 +220,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
Dialogs.showSecureDialog(dialog);
});
_btnErrorBar = findViewById(R.id.btn_error_bar);
_textErrorBar = findViewById(R.id.text_error_bar);
_fabScrollHelper = new FabScrollHelper(fab);
_selectedEntries = new ArrayList<>();
}
@@ -184,6 +237,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_prefs.setUsageCount(usageMap);
}
Map<UUID, Long> lastUsedMap = _entryListView.getLastUsedTimestamps();
if (lastUsedMap != null) {
_prefs.setLastUsedTimestamps(lastUsedMap);
}
super.onPause();
}
@@ -196,44 +254,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
instance.putBoolean("isAuthenticating", _isAuthenticating);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CODE_DECRYPT) {
_isAuthenticating = false;
}
if (requestCode == CODE_DO_INTRO) {
_isDoingIntro = false;
}
if (resultCode != RESULT_OK) {
return;
}
switch (requestCode) {
case CODE_SCAN:
onScanResult(data);
break;
case CODE_ADD_ENTRY:
onAddEntryResult(data);
break;
case CODE_EDIT_ENTRY:
onEditEntryResult(data);
break;
case CODE_DO_INTRO:
onIntroResult();
break;
case CODE_DECRYPT:
onDecryptResult();
break;
case CODE_PREFERENCES:
onPreferencesResult(data);
break;
case CODE_SCAN_IMAGE:
onScanImageResult(data);
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (!PermissionHelper.checkResults(grantResults)) {
@@ -261,61 +281,59 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onEntryListTouch() {
_isDPadPressed = false;
}
private void onPreferencesResult(Intent data) {
// refresh the entire entry list if needed
if (_loaded) {
if (data.getBooleanExtra("needsRecreate", false)) {
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = _prefs.isAccountNameVisible();
boolean showIcons = _prefs.isIconVisible();
Preferences.CodeGrouping codeGroupSize = _prefs.getCodeGroupSize();
boolean highlightEntry = _prefs.isEntryHighlightEnabled();
boolean pauseFocused = _prefs.isPauseFocusedEnabled();
boolean tapToReveal = _prefs.isTapToRevealEnabled();
int tapToRevealTime = _prefs.getTapToRevealTime();
ViewMode viewMode = _prefs.getCurrentViewMode();
boolean copyOnTap = _prefs.isCopyOnTapEnabled();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setShowIcon(showIcons);
_entryListView.setCodeGroupSize(codeGroupSize);
_entryListView.setHighlightEntry(highlightEntry);
_entryListView.setPauseFocused(pauseFocused);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
_entryListView.setIsCopyOnTapEnabled(copyOnTap);
_entryListView.refresh(true);
if (_searchView != null && !_searchView.isIconified()) {
if (ViewCompat.getRootWindowInsets(findViewById(android.R.id.content).getRootView()).isVisible(WindowInsetsCompat.Type.ime())) {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null && getCurrentFocus() != null) {
inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
}
}
}
private void startEditEntryActivityForNew(int requestCode, VaultEntry entry) {
private void onPreferencesResult() {
// refresh the entire entry list if needed
if (_loaded) {
recreate();
}
}
private void startEditEntryActivityForNew(VaultEntry entry) {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("newEntry", entry);
intent.putExtra("isManual", false);
startActivityForResult(intent, requestCode);
addEntryResultLauncher.launch(intent);
}
private void startEditEntryActivityForManual(int requestCode) {
private void startEditEntryActivityForManual() {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("newEntry", VaultEntry.getDefault());
intent.putExtra("isManual", true);
startActivityForResult(intent, requestCode);
addEntryResultLauncher.launch(intent);
}
private void startEditEntryActivity(int requestCode, VaultEntry entry) {
private void startEditEntryActivity(VaultEntry entry) {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("entryUUID", entry.getUUID());
startActivityForResult(intent, requestCode);
editEntryResultLauncher.launch(intent);
}
private void startAssignIconsActivity(List<VaultEntry> entries) {
ArrayList<UUID> assignIconEntriesIds = new ArrayList<>();
Intent assignIconIntent = new Intent(getBaseContext(), AssignIconsActivity.class);
for (VaultEntry entry : entries) {
assignIconEntriesIds.add(entry.getUUID());
}
assignIconIntent.putExtra("entries", assignIconEntriesIds);
assignIconsResultLauncher.launch(assignIconIntent);
}
private void startIntroActivity() {
if (!_isDoingIntro) {
Intent intro = new Intent(this, IntroActivity.class);
startActivityForResult(intro, CODE_DO_INTRO);
introResultLauncher.launch(intro);
_isDoingIntro = true;
}
}
@@ -348,6 +366,17 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void onAssignEntriesResult(Intent data) {
if (_loaded) {
ArrayList<UUID> entryUUIDs = (ArrayList<UUID>) data.getSerializableExtra("entryUUIDs");
for (UUID entryUUID: entryUUIDs) {
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
_entryListView.replaceEntry(entryUUID, entry);
}
}
}
private void onScanImageResult(Intent intent) {
if (intent.getData() != null) {
startDecodeQrCodeImages(Collections.singletonList(intent.getData()));
@@ -413,7 +442,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
boolean isSingleBatch = GoogleAuthInfo.Export.isSingleBatch(googleAuthExports);
if (!isSingleBatch && errors.size() > 0) {
errors.add(getString(R.string.unrelated_google_auth_batches_error));
Dialogs.showMultiMessageDialog(this, R.string.import_error_title, getString(R.string.no_tokens_can_be_imported), errors, null);
Dialogs.showMultiErrorDialog(this, R.string.import_error_title, getString(R.string.no_tokens_can_be_imported), errors, null);
return;
} else if (!isSingleBatch) {
Dialogs.showErrorDialog(this, R.string.import_google_auth_failure, getString(R.string.unrelated_google_auth_batches_error));
@@ -428,7 +457,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
if ((errors.size() > 0 && results.size() > 1) || errors.size() > 1) {
Dialogs.showMultiMessageDialog(this, R.string.import_error_title, getString(R.string.unable_to_read_qrcode_files, uris.size() - errors.size(), uris.size()), errors, dialogDismissHandler);
Dialogs.showMultiErrorDialog(this, R.string.import_error_title, getString(R.string.unable_to_read_qrcode_files, uris.size() - errors.size(), uris.size()), errors, dialogDismissHandler);
} else if (errors.size() > 0) {
Dialogs.showErrorDialog(this, getString(R.string.unable_to_read_qrcode_file, results.get(0).getFileName()), errors.get(0), dialogDismissHandler);
} else {
@@ -440,7 +469,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void importScannedEntries(List<VaultEntry> entries) {
if (entries.size() == 1) {
startEditEntryActivityForNew(CODE_ADD_ENTRY, entries.get(0));
startEditEntryActivityForNew(entries.get(0));
} else if (entries.size() > 1) {
for (VaultEntry entry: entries) {
_vaultManager.getVault().addEntry(entry);
@@ -460,7 +489,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void onIntroResult() {
loadEntries();
checkTimeSyncSetting();
}
private void checkTimeSyncSetting() {
@@ -474,8 +502,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void onDecryptResult() {
_auditLogRepository.addVaultUnlockedEvent();
loadEntries();
checkTimeSyncSetting();
}
private void startScanActivity() {
@@ -484,7 +513,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
startActivityForResult(scannerActivity, CODE_SCAN);
scanResultLauncher.launch(scannerActivity);
}
private void startScanImageActivity() {
@@ -498,7 +527,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
_vaultManager.startActivityForResult(this, chooserIntent, CODE_SCAN_IMAGE);
_vaultManager.fireIntentLauncher(this, chooserIntent, codeScanResultLauncher);
}
private void startPreferencesActivity() {
@@ -509,7 +538,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
Intent intent = new Intent(this, PreferencesActivity.class);
intent.putExtra("fragment", fragmentType);
intent.putExtra("pref", preference);
startActivityForResult(intent, CODE_PREFERENCES);
preferenceResultLauncher.launch(intent);
}
private void doShortcutActions() {
@@ -556,7 +585,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
VaultEntry entry = new VaultEntry(info);
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
startEditEntryActivityForNew(entry);
}
break;
case Intent.ACTION_SEND:
@@ -584,7 +613,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
VaultEntry entry = new VaultEntry(info);
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
startEditEntryActivityForNew(entry);
}
}
break;
@@ -607,8 +636,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
@Override
protected void onResume() {
super.onResume();
protected void onStart() {
super.onStart();
if (_vaultManager.isVaultInitNeeded()) {
if (_prefs.isIntroDone()) {
@@ -618,20 +647,43 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return;
}
if (!_vaultManager.isVaultLoaded() && !_vaultManager.isVaultFileLoaded()) {
Dialogs.showErrorDialog(this, R.string.vault_load_error, _vaultManager.getVaultFileError(), (dialog1, which) -> finish());
return;
// If the vault is not loaded yet, try to load it now in case it's plain text
if (!_vaultManager.isVaultLoaded()) {
VaultFile vaultFile;
try {
vaultFile = VaultRepository.readVaultFile(this);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
finish();
});
return;
}
if (!vaultFile.isEncrypted()) {
try {
_vaultManager.loadFrom(vaultFile);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
finish();
});
return;
}
}
}
if (!_vaultManager.isVaultLoaded()) {
startAuthActivity(false);
} else if (_loaded) {
// update the list of groups in the entry list view so that the chip gets updated
_entryListView.setGroups(_vaultManager.getVault().getGroups());
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
// update the usage counts in case they are edited outside of the EntryListView
_entryListView.setUsageCounts(_prefs.getUsageCounts());
_entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps());
// refresh all codes to prevent showing old ones
_entryListView.refresh(false);
} else {
@@ -646,7 +698,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
handleIncomingIntent();
updateLockIcon();
doShortcutActions();
updateErrorBar();
updateErrorCard();
}
private void deleteEntries(List<VaultEntry> entries) {
@@ -665,7 +717,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
updateLockIcon();
if (_loaded) {
_entryListView.setGroups(_vaultManager.getVault().getGroups());
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
updateSortCategoryMenu();
}
@@ -737,51 +789,44 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings: {
startPreferencesActivity();
return true;
}
case R.id.action_about: {
Intent intent = new Intent(this, AboutActivity.class);
startActivity(intent);
return true;
}
case R.id.action_lock:
_vaultManager.lock(true);
return true;
default:
if (item.getGroupId() == R.id.action_sort_category) {
item.setChecked(true);
int itemId = item.getItemId();
if (itemId == R.id.action_settings) {
startPreferencesActivity();
} else if (itemId == R.id.action_about) {
Intent intent = new Intent(this, AboutActivity.class);
startActivity(intent);
} else if (itemId == R.id.action_lock) {
_vaultManager.lock(true);
} else {
if (item.getGroupId() == R.id.action_sort_category) {
item.setChecked(true);
SortCategory sortCategory;
switch (item.getItemId()) {
case R.id.menu_sort_alphabetically:
sortCategory = SortCategory.ISSUER;
break;
case R.id.menu_sort_alphabetically_reverse:
sortCategory = SortCategory.ISSUER_REVERSED;
break;
case R.id.menu_sort_alphabetically_name:
sortCategory = SortCategory.ACCOUNT;
break;
case R.id.menu_sort_alphabetically_name_reverse:
sortCategory = SortCategory.ACCOUNT_REVERSED;
break;
case R.id.menu_sort_usage_count:
sortCategory = SortCategory.USAGE_COUNT;
break;
case R.id.menu_sort_custom:
default:
sortCategory = SortCategory.CUSTOM;
break;
}
_entryListView.setSortCategory(sortCategory, true);
_prefs.setCurrentSortCategory(sortCategory);
SortCategory sortCategory;
int subItemId = item.getItemId();
if (subItemId == R.id.menu_sort_alphabetically) {
sortCategory = SortCategory.ISSUER;
} else if (subItemId == R.id.menu_sort_alphabetically_reverse) {
sortCategory = SortCategory.ISSUER_REVERSED;
} else if (subItemId == R.id.menu_sort_alphabetically_name) {
sortCategory = SortCategory.ACCOUNT;
} else if (subItemId == R.id.menu_sort_alphabetically_name_reverse) {
sortCategory = SortCategory.ACCOUNT_REVERSED;
} else if (subItemId == R.id.menu_sort_usage_count) {
sortCategory = SortCategory.USAGE_COUNT;
} else if (subItemId == R.id.menu_sort_last_used) {
sortCategory = SortCategory.LAST_USED;
} else {
sortCategory = SortCategory.CUSTOM;
}
return super.onOptionsItemSelected(item);
_entryListView.setSortCategory(sortCategory, true);
_prefs.setCurrentSortCategory(sortCategory);
}
return super.onOptionsItemSelected(item);
}
return true;
}
private void collapseSearchView() {
@@ -791,9 +836,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void loadEntries() {
if (!_loaded) {
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
_entryListView.setUsageCounts(_prefs.getUsageCounts());
_entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps());
_entryListView.addEntries(_vaultManager.getVault().getEntries());
_entryListView.runEntriesAnimation();
if (!_isRecreated) {
_entryListView.runEntriesAnimation();
}
_loaded = true;
}
}
@@ -802,7 +851,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
if (!_isAuthenticating) {
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("inhibitBioPrompt", inhibitBioPrompt);
startActivityForResult(intent, CODE_DECRYPT);
authResultLauncher.launch(intent);
_isAuthenticating = true;
}
}
@@ -815,40 +864,49 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void updateErrorBar() {
Preferences.BackupResult backupRes = _prefs.getErroredBackupResult();
if (backupRes != null) {
_textErrorBar.setText(R.string.backup_error_bar_message);
_btnErrorBar.setOnClickListener(view -> {
Dialogs.showBackupErrorDialog(this, backupRes, (dialog, which) -> {
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
});
});
_btnErrorBar.setVisibility(View.VISIBLE);
} else if (_prefs.isBackupsReminderNeeded()) {
Date date = _prefs.getLatestBackupOrExportTime();
if (date != null) {
_textErrorBar.setText(getString(R.string.backup_reminder_bar_message_with_latest, TimeUtils.getElapsedSince(this, date)));
} else {
_textErrorBar.setText(R.string.backup_reminder_bar_message);
}
_btnErrorBar.setOnClickListener(view -> startPreferencesActivity());
_btnErrorBar.setVisibility(View.VISIBLE);
} else if (_prefs.isPlaintextBackupWarningNeeded()) {
_textErrorBar.setText(R.string.backup_plaintext_export_warning);
_btnErrorBar.setOnClickListener(view -> showPlaintextExportWarningOptions());
_btnErrorBar.setVisibility(View.VISIBLE);
} else {
_btnErrorBar.setVisibility(View.GONE);
}
}
private void updateErrorCard() {
ErrorCardInfo info = null;
Preferences.BackupResult backupRes = _prefs.getErroredBackupResult();
if (backupRes != null) {
info = new ErrorCardInfo(getString(R.string.backup_error_bar_message), view -> {
Dialogs.showBackupErrorDialog(this, backupRes, (dialog, which) -> {
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
});
});
} else if (_prefs.isBackupsReminderNeeded() && _prefs.isBackupReminderEnabled()) {
String text;
Date date = _prefs.getLatestBackupOrExportTime();
if (date != null) {
text = getString(R.string.backup_reminder_bar_message_with_latest, TimeUtils.getElapsedSince(this, date));
} else {
text = getString(R.string.backup_reminder_bar_message);
}
info = new ErrorCardInfo(text, view -> {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(R.string.backup_reminder_bar_dialog_title)
.setMessage(R.string.backup_reminder_bar_dialog_summary)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(R.string.backup_reminder_bar_dialog_accept, (dialog, whichButton) -> {
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
})
.setNegativeButton(android.R.string.cancel, null)
.create());
});
} else if (_prefs.isPlaintextBackupWarningNeeded()) {
info = new ErrorCardInfo(getString(R.string.backup_plaintext_export_warning), view -> showPlaintextExportWarningOptions());
}
_entryListView.setErrorCardInfo(info);
}
private void showPlaintextExportWarningOptions() {
View view = LayoutInflater.from(this).inflate(R.layout.dialog_plaintext_warning, null);
AlertDialog dialog = new AlertDialog.Builder(this)
AlertDialog dialog = new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.backup_plaintext_export_warning)
.setView(view)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
@@ -865,7 +923,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_prefs.setIsPlaintextBackupWarningDisabled(checkBox.isChecked());
_prefs.setIsPlaintextBackupWarningNeeded(false);
updateErrorBar();
updateErrorCard();
});
});
@@ -881,8 +939,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
setFavoriteMenuItemVisiblity();
setIsMultipleSelected(_selectedEntries.size() > 1);
}
return;
}
}
@@ -902,19 +958,24 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_actionMode.getMenu().findItem(R.id.action_copy).setVisible(!multipleSelected);
}
private void setAssignIconsMenuItemVisibility() {
MenuItem assignIconsMenuItem = _actionMode.getMenu().findItem(R.id.action_assign_icons);
assignIconsMenuItem.setVisible(_iconPackManager.hasIconPack());
}
private void setFavoriteMenuItemVisiblity() {
MenuItem toggleFavoriteMenuItem = _actionMode.getMenu().findItem(R.id.action_toggle_favorite);
if (_selectedEntries.size() == 1){
if (_selectedEntries.get(0).isFavorite()) {
toggleFavoriteMenuItem.setIcon(R.drawable.ic_set_favorite);
toggleFavoriteMenuItem.setIcon(R.drawable.ic_filled_star_24);
toggleFavoriteMenuItem.setTitle(R.string.unfavorite);
} else {
toggleFavoriteMenuItem.setIcon(R.drawable.ic_unset_favorite);
toggleFavoriteMenuItem.setIcon(R.drawable.ic_outline_star_24);
toggleFavoriteMenuItem.setTitle(R.string.favorite);
}
} else {
toggleFavoriteMenuItem.setIcon(R.drawable.ic_unset_favorite);
toggleFavoriteMenuItem.setIcon(R.drawable.ic_outline_star_24);
toggleFavoriteMenuItem.setTitle(String.format("%s / %s", getString(R.string.favorite), getString(R.string.unfavorite)));
}
}
@@ -934,11 +995,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_actionMode = startSupportActionMode(_actionModeCallbacks);
_actionModeBackPressHandler.setEnabled(true);
setFavoriteMenuItemVisiblity();
setAssignIconsMenuItemVisibility();
}
@Override
public void onEntryMove(VaultEntry entry1, VaultEntry entry2) {
_vaultManager.getVault().swapEntries(entry1, entry2);
_vaultManager.getVault().moveEntry(entry1, entry2);
}
@Override
@@ -966,7 +1028,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
public void onListChange() { _fabScrollHelper.setVisible(true); }
@Override
public void onSaveGroupFilter(List<String> groupFilter) {
public void onSaveGroupFilter(Set<UUID> groupFilter) {
if (_vaultManager.getVault().isGroupsMigrationFresh()) {
saveAndBackupVault();
}
_prefs.setGroupFilter(groupFilter);
}
@@ -1057,88 +1122,81 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private class ActionModeCallbacks implements ActionMode.Callback {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_action_mode, menu);
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_action_mode, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (_selectedEntries.size() == 0) {
mode.finish();
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
int itemId = item.getItemId();
if (itemId == R.id.action_copy) {
copyEntryCode(_selectedEntries.get(0));
mode.finish();
} else if (itemId == R.id.action_edit) {
startEditEntryActivity(_selectedEntries.get(0));
mode.finish();
} else if (itemId == R.id.action_toggle_favorite) {
for (VaultEntry entry : _selectedEntries) {
entry.setIsFavorite(!entry.isFavorite());
_entryListView.replaceEntry(entry.getUUID(), entry);
}
_entryListView.refresh(true);
saveAndBackupVault();
mode.finish();
} else if (itemId == R.id.action_share_qr) {
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
ArrayList<GoogleAuthInfo> authInfos = new ArrayList<>();
for (VaultEntry entry : _selectedEntries) {
GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
authInfos.add(authInfo);
_auditLogRepository.addEntrySharedEvent(entry.getUUID().toString());
}
intent.putExtra("authInfos", authInfos);
startActivity(intent);
mode.finish();
} else if (itemId == R.id.action_delete) {
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
deleteEntries(_selectedEntries);
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
mode.finish();
});
} else if (itemId == R.id.action_select_all) {
_selectedEntries = _entryListView.selectAllEntries();
setFavoriteMenuItemVisiblity();
setIsMultipleSelected(_selectedEntries.size() > 1);
} else if (itemId == R.id.action_assign_icons) {
startAssignIconsActivity(_selectedEntries);
mode.finish();
} else {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (_selectedEntries.size() == 0) {
mode.finish();
return true;
}
switch (item.getItemId()) {
case R.id.action_copy:
copyEntryCode(_selectedEntries.get(0));
mode.finish();
return true;
return true;
}
case R.id.action_edit:
startEditEntryActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0));
mode.finish();
return true;
case R.id.action_toggle_favorite:
for (VaultEntry entry : _selectedEntries) {
entry.setIsFavorite(!entry.isFavorite());
_entryListView.replaceEntry(entry.getUUID(), entry);
}
_entryListView.refresh(true);
saveAndBackupVault();
mode.finish();
return true;
case R.id.action_share_qr:
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
ArrayList<GoogleAuthInfo> authInfos = new ArrayList<>();
for (VaultEntry entry : _selectedEntries) {
GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
authInfos.add(authInfo);
}
intent.putExtra("authInfos", authInfos);
startActivity(intent);
mode.finish();
return true;
case R.id.action_delete:
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
deleteEntries(_selectedEntries);
for (VaultEntry entry : _selectedEntries) {
if (entry.getGroup() != null) {
TreeSet<String> groups = _vaultManager.getVault().getGroups();
if (!groups.contains(entry.getGroup())) {
_entryListView.setGroups(groups);
break;
}
}
}
mode.finish();
});
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
_entryListView.setActionModeState(false, null);
_actionModeBackPressHandler.setEnabled(false);
_selectedEntries.clear();
_actionMode = null;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
_entryListView.setActionModeState(false, null);
_actionModeBackPressHandler.setEnabled(false);
_selectedEntries.clear();
_actionMode = null;
}
}
}
@@ -10,12 +10,14 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.fragments.preferences.AppearancePreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.preferences.MainPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
public class PreferencesActivity extends AegisActivity implements
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
private Fragment _fragment;
private CharSequence _prefTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -48,27 +50,16 @@ public class PreferencesActivity extends AegisActivity implements
}
} else {
_fragment = getSupportFragmentManager().findFragmentById(R.id.content);
}
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle inState) {
if (_fragment instanceof PreferencesFragment) {
// pass the stored result intent back to the fragment
if (inState.containsKey("result")) {
((PreferencesFragment) _fragment).setResult(inState.getParcelable("result"));
_prefTitle = savedInstanceState.getCharSequence("prefTitle");
if (_prefTitle != null) {
setTitle(_prefTitle);
}
}
super.onRestoreInstanceState(inState);
}
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
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());
}
outState.putCharSequence("prefTitle", _prefTitle);
super.onSaveInstanceState(outState);
}
@@ -90,7 +81,8 @@ public class PreferencesActivity extends AegisActivity implements
_fragment.setTargetFragment(caller, 0);
showFragment(_fragment);
setTitle(pref.getTitle());
_prefTitle = pref.getTitle();
setTitle(_prefTitle);
return true;
}
@@ -121,6 +113,9 @@ public class PreferencesActivity extends AegisActivity implements
public void onFragmentStarted(@NonNull FragmentManager fm, @NonNull Fragment f) {
if (f instanceof MainPreferencesFragment) {
setTitle(R.string.action_settings);
} else if (f instanceof AppearancePreferencesFragment) {
_prefTitle = getString(R.string.pref_section_appearance_title);
setTitle(_prefTitle);
}
}
}
@@ -87,6 +87,11 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
}, ContextCompat.getMainExecutor(this));
}
@Override
protected void onSetTheme() {
_themeHelper.setTheme(ThemeMap.FULLSCREEN);
}
@Override
protected void onDestroy() {
if (_executor != null) {
@@ -95,11 +100,6 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
super.onDestroy();
}
@Override
protected void onSetTheme() {
setTheme(ThemeMap.FULLSCREEN);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
@@ -142,10 +142,10 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
if (dual) {
switch (_currentLens) {
case CameraSelector.LENS_FACING_BACK:
item.setIcon(R.drawable.ic_camera_front_24dp);
item.setIcon(R.drawable.ic_outline_camera_front_24);
break;
case CameraSelector.LENS_FACING_FRONT:
item.setIcon(R.drawable.ic_camera_rear_24dp);
item.setIcon(R.drawable.ic_outline_camera_rear_24);
break;
}
}
@@ -152,9 +152,9 @@ public class TransferEntriesActivity extends AegisActivity {
_entriesCount.setText(getResources().getQuantityString(R.plurals.qr_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
@ColorInt int backgroundColor = Color.WHITE;
if (getConfiguredTheme() == Theme.LIGHT) {
if (_themeHelper.getConfiguredTheme() == Theme.LIGHT) {
TypedValue typedValue = new TypedValue();
getTheme().resolveAttribute(R.attr.background, typedValue, true);
getTheme().resolveAttribute(androidx.appcompat.R.attr.background, typedValue, true);
backgroundColor = typedValue.data;
}
@@ -0,0 +1,182 @@
package com.beemdevelopment.aegis.ui.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.PluralsRes;
import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
import com.beemdevelopment.aegis.R;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class DropdownCheckBoxes<T> extends AppCompatAutoCompleteTextView {
private @PluralsRes int _selectedCountPlural = R.plurals.dropdown_checkboxes_default_count;
private boolean _allowFiltering = false;
private final List<T> _items = new ArrayList<>();
private List<T> _visibleItems = new ArrayList<>();
private final Set<T> _checkedItems = new HashSet<>();
private CheckboxAdapter _adapter;
public DropdownCheckBoxes(Context context) {
super(context);
initialise(context, null);
}
public DropdownCheckBoxes(Context context, AttributeSet attrs) {
super(context, attrs);
initialise(context, attrs);
}
public DropdownCheckBoxes(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialise(context, attrs);
}
private void initialise(Context context, AttributeSet attrs) {
_adapter = new CheckboxAdapter();
setAdapter(_adapter);
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(
attrs,
R.styleable.DropdownCheckBoxes,
0, 0);
_allowFiltering = a.getBoolean(R.styleable.DropdownCheckBoxes_allow_filtering, false);
a.recycle();
}
if (!_allowFiltering) {
setInputType(0);
} else {
setInputType(InputType.TYPE_CLASS_TEXT);
}
}
/**
* Add parameterized items to be displayed as a checkbox in the dropdown view
* the label for the checkbox is determined by the toString() method of the items
* you add.
*
* @param items a list of the items you want to show in the dropdown
* @param startChecked whether the checkbox should be checked initially
*/
public void addItems(List<T> items, boolean startChecked) {
_items.addAll(items);
_visibleItems.addAll(items);
if (startChecked) {
_checkedItems.addAll(items);
}
updateCheckedItemsCountText();
_adapter.notifyDataSetChanged();
}
private void updateCheckedItemsCountText() {
if (_allowFiltering) {
return;
}
int count = _checkedItems.size();
String countString = getResources().getQuantityString(_selectedCountPlural, count, count);
setText(countString, false);
}
public void setCheckedItemsCountTextRes(@PluralsRes int resId) {
_selectedCountPlural = resId;
}
public Set<T> getCheckedItems() {
return _checkedItems;
}
private class CheckboxAdapter extends BaseAdapter implements Filterable {
@Override
public int getCount() {
return _visibleItems.size();
}
@Override
public T getItem(int i) {
return _visibleItems.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.dropdown_checkbox, viewGroup, false);
}
T item = _visibleItems.get(i);
CheckBox checkBox = convertView.findViewById(R.id.checkbox_in_dropdown);
checkBox.setText(item.toString());
checkBox.setTag(item);
checkBox.setChecked(_checkedItems.contains(item));
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
_checkedItems.add((T) buttonView.getTag());
} else {
_checkedItems.remove((T) buttonView.getTag());
}
updateCheckedItemsCountText();
});
return convertView;
}
@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence query) {
FilterResults results = new FilterResults();
results.values = (query == null || query.toString().isEmpty())
? _items
: _items.stream().filter(item -> {
String q = query.toString().toLowerCase();
String strLower = item.toString().toLowerCase();
return strLower.contains(q);
})
.collect(Collectors.toList());
return results;
}
@Override
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
_visibleItems = (List<T>) filterResults.values;
notifyDataSetChanged();
}
};
}
}
}
@@ -5,7 +5,7 @@ import android.content.Context;
import com.beemdevelopment.aegis.R;
public class ChangelogDialog extends SimpleWebViewDialog {
private ChangelogDialog() {
public ChangelogDialog() {
super(R.string.changelog);
}
@@ -5,8 +5,6 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
@@ -25,6 +23,7 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.ComponentActivity;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
@@ -39,10 +38,9 @@ import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
import java.util.ArrayList;
import java.util.List;
@@ -86,9 +84,10 @@ public class Dialogs {
}
textMessage.setText(message);
showSecureDialog(new AlertDialog.Builder(context)
showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(title)
.setView(view)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.yes, onDelete)
.setNegativeButton(android.R.string.no, null)
.create());
@@ -107,16 +106,16 @@ public class Dialogs {
}
public static void showDiscardDialog(Context context, DialogInterface.OnClickListener onSave, DialogInterface.OnClickListener onDiscard) {
showSecureDialog(new AlertDialog.Builder(context)
showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(context.getString(R.string.discard_changes))
.setMessage(context.getString(R.string.discard_changes_description))
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(R.string.save, onSave)
.setNegativeButton(R.string.discard, onDiscard)
.create());
}
public static void showSetPasswordDialog(ComponentActivity activity, PasswordSlotListener listener) {
Zxcvbn zxcvbn = new Zxcvbn();
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
EditText textPassword = view.findViewById(R.id.text_password);
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
@@ -124,6 +123,8 @@ public class Dialogs {
TextView textPasswordStrength = view.findViewById(R.id.text_password_strength);
TextInputLayout textPasswordWrapper = view.findViewById(R.id.text_password_wrapper);
CheckBox switchToggleVisibility = view.findViewById(R.id.check_toggle_visibility);
PasswordStrengthHelper passStrength = new PasswordStrengthHelper(
textPassword, barPasswordStrength, textPasswordStrength, textPasswordWrapper);
switchToggleVisibility.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
@@ -137,7 +138,7 @@ public class Dialogs {
}
});
AlertDialog dialog = new AlertDialog.Builder(activity)
AlertDialog dialog = new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.set_password)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
@@ -179,13 +180,7 @@ public class Dialogs {
TextWatcher watcher = new SimpleTextWatcher(text -> {
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
buttonOK.get().setEnabled(equal);
Strength strength = zxcvbn.measure(textPassword.getText());
barPasswordStrength.setProgress(strength.getScore());
barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(PasswordStrengthHelper.getColor(strength.getScore()))));
textPasswordStrength.setText((textPassword.getText().length() != 0) ? PasswordStrengthHelper.getString(strength.getScore(), activity) : "");
textPasswordWrapper.setError(strength.getFeedback().getWarning());
strength.wipe();
passStrength.measure(activity);
});
textPassword.addTextChangedListener(watcher);
textPasswordConfirm.addTextChangedListener(watcher);
@@ -193,10 +188,13 @@ public class Dialogs {
showSecureDialog(dialog);
}
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener, boolean isSecret) {
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener, boolean isSecret,@Nullable String hint) {
final AtomicReference<Button> buttonOK = new AtomicReference<>();
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
TextInputEditText input = view.findViewById(R.id.text_input);
if(hint != null) {
input.setText(hint);
}
input.addTextChangedListener(new SimpleTextWatcher(text -> {
if (buttonOK.get() != null) {
buttonOK.get().setEnabled(!text.toString().isEmpty());
@@ -208,7 +206,7 @@ public class Dialogs {
}
inputLayout.setHint(hintId);
AlertDialog.Builder builder = new AlertDialog.Builder(context)
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context)
.setTitle(titleId)
.setView(view)
.setPositiveButton(android.R.string.ok, null);
@@ -238,12 +236,16 @@ public class Dialogs {
showSecureDialog(dialog);
}
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, String text) {
showTextInputDialog(context, titleId, 0, hintId, listener, null, false, text);
}
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret);
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret, null);
}
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
showTextInputDialog(context, titleId, hintId, listener, false);
showTextInputDialog(context, titleId, 0, hintId, listener, null, false, null);
}
public static void showPasswordInputDialog(Context context, TextInputListener listener) {
@@ -251,19 +253,19 @@ public class Dialogs {
}
public static void showPasswordInputDialog(Context context, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
showTextInputDialog(context, R.string.set_password, 0, R.string.password, listener, cancelListener, true);
showTextInputDialog(context, R.string.set_password, 0, R.string.password, listener, cancelListener, true, null);
}
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, null, true);
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, null, true, null);
}
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);
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, cancelListener, true, null);
}
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);
showTextInputDialog(context, titleId, messageId, R.string.password, listener, cancelListener, true, null);
}
public static void showCheckboxDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int checkboxMessageId, CheckboxInputListener listener) {
@@ -271,7 +273,7 @@ public class Dialogs {
CheckBox checkBox = view.findViewById(R.id.checkbox);
checkBox.setText(checkboxMessageId);
AlertDialog.Builder builder = new AlertDialog.Builder(context)
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context)
.setTitle(titleId)
.setView(view)
.setNegativeButton(R.string.no, (dialog1, which) ->
@@ -305,7 +307,7 @@ public class Dialogs {
numberPicker.setValue(currentValue);
numberPicker.setWrapSelectorWheel(true);
AlertDialog dialog = new AlertDialog.Builder(context)
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.set_number)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
@@ -316,25 +318,32 @@ public class Dialogs {
}
public static void showBackupVersionsPickerDialog(Context context, int currentVersionCount, NumberInputListener listener) {
final int max = 30;
String[] numbers = new String[max / 5];
for (int i = 0; i < numbers.length; i++) {
numbers[i] = Integer.toString(i * 5 + 5);
String infinite = context.getString(R.string.pref_backups_versions_infinite);
String[] values = {"5", "10", "15", "20", "25", "30", infinite};
int[] numbers = {5, 10, 15, 20, 25, 30, Preferences.BACKUPS_VERSIONS_INFINITE};
int selectedIndex;
if (currentVersionCount == Preferences.BACKUPS_VERSIONS_INFINITE) {
selectedIndex = numbers.length - 1;
} else {
selectedIndex = currentVersionCount / 5 - 1;
}
View view = LayoutInflater.from(context).inflate(R.layout.dialog_number_picker, null);
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
numberPicker.setDisplayedValues(numbers);
numberPicker.setMaxValue(numbers.length - 1);
numberPicker.setDisplayedValues(values);
numberPicker.setMaxValue(values.length - 1);
numberPicker.setMinValue(0);
numberPicker.setValue(currentVersionCount / 5 - 1);
numberPicker.setValue(selectedIndex);
numberPicker.setWrapSelectorWheel(false);
AlertDialog dialog = new AlertDialog.Builder(context)
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.set_number)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
listener.onNumberInputResult(numberPicker.getValue()))
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
int index = numberPicker.getValue();
int number = numbers[index];
listener.onNumberInputResult(number);
})
.create();
showSecureDialog(dialog);
@@ -371,10 +380,11 @@ public class Dialogs {
TextView textMessage = view.findViewById(R.id.error_message);
textMessage.setText(message);
AlertDialog dialog = new AlertDialog.Builder(context)
AlertDialog dialog = new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(R.string.error_occurred)
.setView(view)
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
if (listener != null) {
listener.onClick(dialog1, which);
@@ -406,38 +416,40 @@ public class Dialogs {
public static void showBackupErrorDialog(Context context, Preferences.BackupResult backupRes, DialogInterface.OnClickListener listener) {
String system = context.getString(backupRes.isBuiltIn() ? R.string.backup_system_builtin : R.string.backup_system_android);
String message = context.getString(R.string.backup_error_dialog_details, system, backupRes.getElapsedSince(context));
@StringRes int details = backupRes.isPermissionError() ? R.string.backup_permission_error_dialog_details : R.string.backup_error_dialog_details;
String message = context.getString(details, system, backupRes.getElapsedSince(context));
Dialogs.showErrorDialog(context, message, backupRes.getError(), listener);
}
public static void showMultiMessageDialog(
public static void showMultiErrorDialog(
Context context, @StringRes int title, String message, List<CharSequence> messages, DialogInterface.OnClickListener listener) {
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(title)
.setMessage(message)
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
if (listener != null) {
listener.onClick(dialog, which);
}
})
.setNeutralButton(context.getString(R.string.details), (dialog, which) -> {
showDetailedMultiMessageDialog(context, title, messages, listener);
showDetailedMultiErrorDialog(context, title, messages, listener);
})
.create());
}
public static <T extends Throwable> void showMultiErrorDialog(
public static <T extends Throwable> void showMultiExceptionDialog(
Context context, @StringRes int title, String message, List<T> errors, DialogInterface.OnClickListener listener) {
List<CharSequence> messages = new ArrayList<>();
for (Throwable e : errors) {
messages.add(e.toString());
}
showMultiMessageDialog(context, title, message, messages, listener);
showMultiErrorDialog(context, title, message, messages, listener);
}
private static void showDetailedMultiMessageDialog(
private static void showDetailedMultiErrorDialog(
Context context, @StringRes int title, List<CharSequence> messages, DialogInterface.OnClickListener listener) {
SpannableStringBuilder builder = new SpannableStringBuilder();
for (CharSequence message : messages) {
@@ -445,10 +457,11 @@ public class Dialogs {
builder.append("\n\n");
}
AlertDialog dialog = new AlertDialog.Builder(context)
AlertDialog dialog = new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(title)
.setMessage(builder)
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
if (listener != null) {
listener.onClick(dialog1, which);
@@ -475,10 +488,11 @@ public class Dialogs {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_time_sync, null);
CheckBox checkBox = view.findViewById(R.id.check_warning_disable);
showSecureDialog(new AlertDialog.Builder(context)
showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.time_sync_warning_title)
.setView(view)
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(R.string.yes, (dialog, which) -> {
if (checkBox.isChecked()) {
prefs.setIsTimeSyncWarningEnabled(false);
@@ -513,7 +527,7 @@ public class Dialogs {
setImporterHelpText(helpText, importers.get(position), isDirect);
});
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
.setTitle(R.string.choose_application)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
@@ -534,11 +548,12 @@ public class Dialogs {
errorDetails.append("\n\n");
}
AlertDialog.Builder builder = new AlertDialog.Builder(context)
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.partial_google_auth_import)
.setMessage(context.getString(R.string.partial_google_auth_import_warning, missingIndexesAsString))
.setView(view)
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(context.getString(R.string.import_partial_export_anyway, entries), (dialog, which) -> {
dismissHandler.onClick(dialog, which);
})
@@ -18,14 +18,13 @@ import androidx.recyclerview.widget.GridLayoutManager;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.ui.views.IconRecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
@@ -40,7 +39,7 @@ public class IconPickerDialog {
}
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, IconAdapter.Listener listener) {
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, boolean showAddCustom, IconAdapter.Listener listener) {
View view = LayoutInflater.from(activity).inflate(R.layout.dialog_icon_picker, null);
TextView textIconPack = view.findViewById(R.id.text_icon_pack);
@@ -76,12 +75,9 @@ public class IconPickerDialog {
@Nullable
@Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
return Glide.with(dialog.getContext())
.asDrawable()
.load(icon.getFile())
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
RequestBuilder<Drawable> rb = Glide.with(dialog.getContext())
.load(icon.getFile());
return GlideHelper.setCommonOptions(rb, icon.getIconType());
}
}
@@ -128,7 +124,7 @@ public class IconPickerDialog {
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
recyclerView.addOnScrollListener(preloader);
adapter.loadIcons(iconPacks.get(0));
adapter.loadIcons(iconPacks.get(0), showAddCustom);
textIconPack.setText(iconPacks.get(0).getName());
view.findViewById(R.id.btn_icon_pack).setOnClickListener(v -> {
@@ -139,7 +135,7 @@ public class IconPickerDialog {
PopupMenu popupMenu = new PopupMenu(activity, v);
popupMenu.setOnMenuItemClickListener(item -> {
IconPack pack = iconPacks.get(iconPackNames.indexOf(item.getTitle().toString()));
adapter.loadIcons(pack);
adapter.loadIcons(pack, showAddCustom);
String query = iconSearch.getText().toString();
if (!query.isEmpty()) {
@@ -5,7 +5,7 @@ import android.content.Context;
import com.beemdevelopment.aegis.R;
public class LicenseDialog extends SimpleWebViewDialog {
private LicenseDialog() {
public LicenseDialog() {
super(R.string.license);
}
@@ -17,7 +17,8 @@ import androidx.fragment.app.DialogFragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.io.CharStreams;
import java.io.IOException;
@@ -44,14 +45,14 @@ public abstract class SimpleWebViewDialog extends DialogFragment {
view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_web_view, null);
} catch (InflateException e) {
e.printStackTrace();
return new AlertDialog.Builder(requireContext())
return new MaterialAlertDialogBuilder(requireContext())
.setTitle(android.R.string.dialog_alert_title)
.setMessage(getString(R.string.webview_error))
.setPositiveButton(android.R.string.ok, null)
.show();
}
AlertDialog dialog = new AlertDialog.Builder(requireContext())
AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext())
.setTitle(_title)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
@@ -69,12 +70,21 @@ public abstract class SimpleWebViewDialog extends DialogFragment {
}
protected String getBackgroundColor() {
int backgroundColorResource = _theme == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
return colorToCSS(ThemeHelper.getThemeColor(backgroundColorResource, requireContext().getTheme()));
int color = MaterialColors.getColor(
requireContext(),
com.google.android.material.R.attr.colorSurfaceContainerHigh,
getClass().getCanonicalName()
);
return colorToCSS(color);
}
protected String getTextColor() {
return colorToCSS(0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, requireContext().getTheme()));
int color = MaterialColors.getColor(
requireContext(),
com.google.android.material.R.attr.colorOnSurface,
getClass().getCanonicalName()
);
return colorToCSS(0xFFFFFF & color);
}
@SuppressLint("DefaultLocale")
@@ -7,19 +7,26 @@ import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import com.beemdevelopment.aegis.AccountNamePosition;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.ui.GroupManagerActivity;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.google.android.material.color.DynamicColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.Arrays;
import java.util.List;
public class AppearancePreferencesFragment extends PreferencesFragment {
private Preference _groupsPreference;
private Preference _resetUsageCountPreference;
private Preference _currentAccountNamePositionPreference;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_appearance);
_groupsPreference = requirePreference("pref_groups");
@@ -31,7 +38,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
_resetUsageCountPreference = requirePreference("pref_reset_usage_count");
_resetUsageCountPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.preference_reset_usage_count)
.setMessage(R.string.preference_reset_usage_count_dialog)
.setPositiveButton(android.R.string.yes, (dialog, which) -> _prefs.clearUsageCount())
@@ -46,7 +53,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
darkModePreference.setOnPreferenceClickListener(preference -> {
int currentTheme1 = _prefs.getCurrentTheme().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.choose_theme)
.setSingleChoiceItems(R.array.theme_titles, currentTheme1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
@@ -54,7 +61,6 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
dialog.dismiss();
getResult().putExtra("needsRecreate", true);
requireActivity().recreate();
})
.setNegativeButton(android.R.string.cancel, null)
@@ -63,11 +69,34 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
return true;
});
Preference dynamicColorsPreference = requirePreference("pref_dynamic_colors");
dynamicColorsPreference.setEnabled(DynamicColors.isDynamicColorAvailable());
dynamicColorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
requireActivity().recreate();
return true;
});
Preference langPreference = requirePreference("pref_lang");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRecreate", true);
requireActivity().recreate();
String[] langs = getResources().getStringArray(R.array.pref_lang_values);
String[] langNames = getResources().getStringArray(R.array.pref_lang_entries);
List<String> langList = Arrays.asList(langs);
int curLangIndex = langList.contains(_prefs.getLanguage()) ? langList.indexOf(_prefs.getLanguage()) : 0;
langPreference.setSummary(langNames[curLangIndex]);
langPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.pref_lang_title)
.setSingleChoiceItems(langNames, curLangIndex, (dialog, which) -> {
int newLangIndex = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_prefs.setLanguage(langs[newLangIndex]);
langPreference.setSummary(langNames[newLangIndex]);
dialog.dismiss();
requireActivity().recreate();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
});
} else {
@@ -81,13 +110,13 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
viewModePreference.setOnPreferenceClickListener(preference -> {
int currentViewMode1 = _prefs.getCurrentViewMode().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.choose_view_mode)
.setSingleChoiceItems(R.array.view_mode_titles, currentViewMode1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_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);
refreshAccountNamePositionText();
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
@@ -96,22 +125,56 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
return true;
});
String[] codeGroupings = getResources().getStringArray(R.array.pref_code_groupings_values);
String[] codeGroupingNames = getResources().getStringArray(R.array.pref_code_groupings);
Preference codeDigitGroupingPreference = requirePreference("pref_code_group_size_string");
codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
codeDigitGroupingPreference.setOnPreferenceClickListener(preference -> {
int currentCodeGroupingIndex = Arrays.asList(codeGroupings).indexOf(_prefs.getCodeGroupSize().name());
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.pref_code_group_size_title)
.setSingleChoiceItems(codeGroupingNames, currentCodeGroupingIndex, (dialog, which) -> {
int newCodeGroupingIndex = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_prefs.setCodeGroupSize(Preferences.CodeGrouping.valueOf(codeGroupings[newCodeGroupingIndex]));
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
});
Preference issuerPreference = requirePreference("pref_account_name");
issuerPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
int currentAccountNamePosition = _prefs.getAccountNamePosition().ordinal();
_currentAccountNamePositionPreference = requirePreference("pref_account_name_position");
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[currentAccountNamePosition]));
_currentAccountNamePositionPreference.setOnPreferenceClickListener(preference -> {
int currentAccountNamePosition1 = _prefs.getAccountNamePosition().ordinal();
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.choose_account_name_position))
.setSingleChoiceItems(R.array.account_name_position_titles, currentAccountNamePosition1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_prefs.setAccountNamePosition(AccountNamePosition.fromInteger(i));
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[i]));
refreshAccountNamePositionText();
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
});
Preference showIconsPreference = requirePreference("pref_show_icons");
showIconsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
refreshAccountNamePositionText();
}
private void refreshAccountNamePositionText() {
boolean override = (_prefs.getCurrentViewMode() == ViewMode.TILES && _prefs.getAccountNamePosition() == AccountNamePosition.END);
if (override) {
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s. %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[_prefs.getAccountNamePosition().ordinal()], getString(R.string.pref_account_name_position_summary_override)));
} else {
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[_prefs.getAccountNamePosition().ordinal()]));
}
}
}
@@ -0,0 +1,97 @@
package com.beemdevelopment.aegis.ui.fragments.preferences;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.database.AuditLogEntry;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.ui.models.AuditLogEntryModel;
import com.beemdevelopment.aegis.ui.views.AuditLogAdapter;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
import java.util.List;
import java.util.UUID;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class AuditLogPreferencesFragment extends Fragment {
@Inject
AuditLogRepository _auditLogRepository;
private AuditLogAdapter _adapter;
private RecyclerView _auditLogRecyclerView;
private LinearLayout _noAuditLogsView;
@Inject
VaultManager _vaultManager;
public AuditLogPreferencesFragment() {
super(R.layout.fragment_audit_log);
}
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
LiveData<List<AuditLogEntry>> entries = _auditLogRepository.getAllAuditLogEntries();
_adapter = new AuditLogAdapter();
_noAuditLogsView = view.findViewById(R.id.vEmptyList);
_auditLogRecyclerView = view.findViewById(R.id.list_audit_log);
LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
_auditLogRecyclerView.addItemDecoration(new SpacesItemDecoration(8));
_auditLogRecyclerView.setLayoutManager(layoutManager);
_auditLogRecyclerView.setAdapter(_adapter);
_auditLogRecyclerView.setNestedScrollingEnabled(false);
entries.observe(getViewLifecycleOwner(), entries1 -> {
_noAuditLogsView.setVisibility(entries1.isEmpty() ? View.VISIBLE : View.GONE);
for (AuditLogEntry entry : entries1) {
VaultEntry referencedEntry = null;
if (entry.getReference() != null) {
UUID referencedEntryUUID = UUID.fromString(entry.getReference());
if (_vaultManager.getVault().hasEntryByUUID(referencedEntryUUID)) {
referencedEntry = _vaultManager.getVault().getEntryByUUID(referencedEntryUUID);
}
}
AuditLogEntryModel auditLogEntryModel = new AuditLogEntryModel(entry, referencedEntry);
_adapter.addAuditLogEntryModel(auditLogEntryModel);
}
});
}
private class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private final int _space;
public SpacesItemDecoration(int dpSpace) {
_space = MetricsHelper.convertDpToPixels(getContext(), dpSpace);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.left = _space;
outRect.right = _space;
outRect.bottom = _space;
if (parent.getChildLayoutPosition(view) == 0) {
outRect.top = _space;
}
}
}
}
@@ -11,6 +11,8 @@ import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
@@ -19,10 +21,12 @@ import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.color.MaterialColors;
public class BackupsPreferencesFragment extends PreferencesFragment {
private SwitchPreferenceCompat _androidBackupsPreference;
private SwitchPreferenceCompat _backupsPreference;
private SwitchPreferenceCompat _backupReminderPreference;
private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference;
@@ -31,6 +35,15 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
private Preference _builtinBackupStatusPreference;
private Preference _androidBackupStatusPreference;
private final ActivityResultLauncher<Intent> backupsResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
Intent data = activityResult.getData();
int resultCode = activityResult.getResultCode();
if (data != null) {
onSelectBackupsLocationResult(resultCode, data);
}
});
@Override
public void onResume() {
super.onResume();
@@ -39,7 +52,6 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_backups);
_backupsPasswordWarningPreference = requirePreference("pref_backups_warning_password");
@@ -72,6 +84,22 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return false;
});
_backupReminderPreference = requirePreference("pref_backup_reminder");
_backupReminderPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if (!(boolean)newValue) {
Dialogs.showCheckboxDialog(getContext(), R.string.pref_backups_reminder_dialog_title,
R.string.pref_backups_reminder_dialog_summary,
R.string.understand_risk_accept,
this::saveAndDisableBackupReminder
);
} else {
_prefs.setIsBackupReminderEnabled(true);
return true;
}
return false;
});
_androidBackupsPreference = requirePreference("pref_android_backups");
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_prefs.setIsAndroidBackupsEnabled((boolean) newValue);
@@ -102,21 +130,20 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
});
_backupsVersionsPreference = requirePreference("pref_backups_versions");
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, _prefs.getBackupsVersionCount(), _prefs.getBackupsVersionCount()));
updateBackupsVersionsSummary();
_backupsVersionsPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showBackupVersionsPickerDialog(requireContext(), _prefs.getBackupsVersionCount(), number -> {
number = number * 5 + 5;
_prefs.setBackupsVersionCount(number);
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, _prefs.getBackupsVersionCount(), _prefs.getBackupsVersionCount()));
updateBackupsVersionsSummary();
});
return false;
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data != null && requestCode == CODE_BACKUPS) {
onSelectBackupsLocationResult(resultCode, data);
private void saveAndDisableBackupReminder(boolean understand) {
if (understand) {
_prefs.setIsBackupReminderEnabled(false);
updateBackupPreference();
}
}
@@ -140,11 +167,13 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
boolean encrypted = _vaultManager.getVault().isEncryptionEnabled();
boolean androidBackupEnabled = _prefs.isAndroidBackupsEnabled() && encrypted;
boolean backupEnabled = _prefs.isBackupsEnabled() && encrypted;
boolean backupReminderEnabled = _prefs.isBackupReminderEnabled();
_backupsPasswordWarningPreference.setVisible(_vaultManager.getVault().isBackupPasswordSet());
_androidBackupsPreference.setChecked(androidBackupEnabled);
_androidBackupsPreference.setEnabled(encrypted);
_backupsPreference.setChecked(backupEnabled);
_backupsPreference.setEnabled(encrypted);
_backupReminderPreference.setChecked(backupReminderEnabled);
_backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled);
@@ -165,9 +194,9 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
// TODO: Find out why setting the tint of the icon doesn't work
if (backupFailed) {
pref.setIcon(R.drawable.ic_info_outline_black_24dp);
pref.setIcon(R.drawable.ic_outline_error_24);
} else if (res != null) {
pref.setIcon(R.drawable.ic_check_black_24dp);
pref.setIcon(R.drawable.ic_outline_check_24);
} else {
pref.setIcon(null);
}
@@ -175,21 +204,20 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
private CharSequence getBackupStatusMessage(@Nullable Preferences.BackupResult res) {
String message;
int color = R.color.warning_color;
int colorAttr = com.google.android.material.R.attr.colorError;
if (res == null) {
message = getString(R.string.backup_status_none);
} else if (res.isSuccessful()) {
color = R.color.success_color;
colorAttr = R.attr.colorSuccess;
message = getString(R.string.backup_status_success, res.getElapsedSince(requireContext()));
} else {
message = getString(R.string.backup_status_failed, res.getElapsedSince(requireContext()));
}
int color = MaterialColors.getColor(requireContext(), colorAttr, getClass().getCanonicalName());
Spannable spannable = new SpannableString(message);
spannable.setSpan(new ForegroundColorSpan(getResources().getColor(color)), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
if (color == R.color.warning_color) {
spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
spannable.setSpan(new ForegroundColorSpan(color), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannable;
}
@@ -200,7 +228,7 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
_vaultManager.startActivityForResult(this, intent, CODE_BACKUPS);
_vaultManager.fireIntentLauncher(this, intent, backupsResultLauncher);
}
private void scheduleBackup() {
@@ -212,4 +240,14 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
Dialogs.showErrorDialog(requireContext(), R.string.backup_error, e);
}
}
private void updateBackupsVersionsSummary() {
int count = _prefs.getBackupsVersionCount();
if (count == Preferences.BACKUPS_VERSIONS_INFINITE) {
_backupsVersionsPreference.setSummary(R.string.pref_backups_versions_infinite_summary);
} else {
String summary = getResources().getQuantityString(R.plurals.pref_backups_versions_summary, count, count);
_backupsVersionsPreference.setSummary(summary);
}
}
}
@@ -1,37 +1,113 @@
package com.beemdevelopment.aegis.ui.fragments.preferences;
import android.os.Bundle;
import android.widget.Button;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import com.beemdevelopment.aegis.CopyBehavior;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class BehaviorPreferencesFragment extends PreferencesFragment {
private Preference _entryPausePreference;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_behavior);
Preference copyOnTapPreference = requirePreference("pref_copy_on_tap");
copyOnTapPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
Preference currentSearchBehaviorPreference = requirePreference("pref_search_behavior");
currentSearchBehaviorPreference.setSummary(getSearchBehaviorSummary());
currentSearchBehaviorPreference.setOnPreferenceClickListener((preference) -> {
final int[] items = Preferences.SEARCH_BEHAVIOR_SETTINGS;
final String[] textItems = getResources().getStringArray(R.array.pref_search_behavior_types);
final boolean[] checkedItems = new boolean[items.length];
for (int i = 0; i < items.length; i++) {
checkedItems[i] = _prefs.isSearchBehaviorTypeEnabled(items[i]);
}
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.pref_search_behavior_prompt)
.setMultiChoiceItems(textItems, checkedItems, (dialog, index, isChecked) -> {
checkedItems[index] = isChecked;
boolean containsAtLeastOneCheckedItem = false;
for(boolean b: checkedItems) {
if (b) {
containsAtLeastOneCheckedItem = true;
break;
}
}
AlertDialog alertDialog = (AlertDialog) dialog;
Button positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
positiveButton.setEnabled(containsAtLeastOneCheckedItem);
})
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int searchBehavior = 0;
for (int i = 0; i < checkedItems.length; i++) {
if (checkedItems[i]) {
searchBehavior |= items[i];
}
}
_prefs.setSearchBehaviorMask(searchBehavior);
currentSearchBehaviorPreference.setSummary(getSearchBehaviorSummary());
})
.setNegativeButton(android.R.string.cancel, null);
Dialogs.showSecureDialog(builder.create());
return true;
});
int currentCopyBehavior = _prefs.getCopyBehavior().ordinal();
Preference copyBehaviorPreference = requirePreference("pref_copy_behavior");
copyBehaviorPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.copy_behavior_titles)[currentCopyBehavior]));
copyBehaviorPreference.setOnPreferenceClickListener(preference -> {
int currentCopyBehavior1 = _prefs.getCopyBehavior().ordinal();
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.choose_copy_behavior))
.setSingleChoiceItems(R.array.copy_behavior_titles, currentCopyBehavior1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_prefs.setCopyBehavior(CopyBehavior.fromInteger(i));
copyBehaviorPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.copy_behavior_titles)[i]));
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
});
Preference entryPausePreference = requirePreference("pref_pause_entry");
entryPausePreference.setEnabled(_prefs.isTapToRevealEnabled() || _prefs.isEntryHighlightEnabled());
Preference entryHighlightPreference = requirePreference("pref_highlight_entry");
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
_entryPausePreference.setEnabled(_prefs.isTapToRevealEnabled() || (boolean) newValue);
entryPausePreference.setEnabled(_prefs.isTapToRevealEnabled() || (boolean) newValue);
return true;
});
}
_entryPausePreference = requirePreference("pref_pause_entry");
_entryPausePreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
_entryPausePreference.setEnabled(_prefs.isTapToRevealEnabled() || _prefs.isEntryHighlightEnabled());
private String getSearchBehaviorSummary() {
final int[] settings = Preferences.SEARCH_BEHAVIOR_SETTINGS;
final String[] descriptions = getResources().getStringArray(R.array.pref_search_behavior_types);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < settings.length; i++) {
if (_prefs.isSearchBehaviorTypeEnabled(settings[i])) {
if (builder.length() != 0) {
builder.append(", ");
}
builder.append(descriptions[i].toLowerCase());
}
}
return getString(R.string.pref_search_behavior_summary, builder.toString());
}
}
@@ -6,16 +6,20 @@ import android.net.Uri;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.view.animation.Animation;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconPackException;
@@ -25,6 +29,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ImportIconPackTask;
import com.beemdevelopment.aegis.ui.views.IconPackAdapter;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import javax.inject.Inject;
@@ -33,20 +38,25 @@ import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class IconPacksManagerFragment extends Fragment implements IconPackAdapter.Listener {
private static final int CODE_IMPORT = 0;
@Inject
IconPackManager _iconPackManager;
@Inject
VaultManager _vaultManager;
private View _iconPacksView;
private RecyclerView _iconPacksRecyclerView;
private IconPackAdapter _adapter;
private LinearLayout _noIconPacksView;
private FabScrollHelper _fabScrollHelper;
private final ActivityResultLauncher<Intent> importResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
Intent data = activityResult.getData();
if (activityResult.getResultCode() == Activity.RESULT_OK && data != null && data.getData() != null) {
importIconPack(data.getData());
}
});
public IconPacksManagerFragment() {
super(R.layout.fragment_icon_packs);
}
@@ -59,7 +69,6 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
_noIconPacksView = view.findViewById(R.id.vEmptyList);
((TextView) view.findViewById(R.id.txt_no_icon_packs)).setMovementMethod(LinkMovementMethod.getInstance());
_iconPacksView = view.findViewById(R.id.view_icon_packs);
_adapter = new IconPackAdapter(this);
_iconPacksRecyclerView = view.findViewById(R.id.list_icon_packs);
LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
@@ -81,11 +90,22 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
updateEmptyState();
}
@Override
@Nullable
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (nextAnim != 0) {
return AnimationsHelper.loadScaledAnimation(requireContext(), nextAnim, AnimationsHelper.Scale.TRANSITION);
}
return super.onCreateAnimation(transit, enter, nextAnim);
}
@Override
public void onRemoveIconPack(IconPack pack) {
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.remove_icon_pack)
.setMessage(R.string.remove_icon_pack_description)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
try {
_iconPackManager.removeIconPack(pack);
@@ -101,22 +121,14 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
.create());
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CODE_IMPORT && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
importIconPack(data.getData());
}
}
private void importIconPack(Uri uri) {
ImportIconPackTask task = new ImportIconPackTask(requireContext(), result -> {
Exception e = result.getException();
if (e instanceof IconPackExistsException) {
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(R.string.error_occurred)
.setMessage(R.string.icon_pack_import_exists_error)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(R.string.yes, (dialog, which) -> {
if (removeIconPack(((IconPackExistsException) e).getIconPack())) {
importIconPack(uri);
@@ -151,15 +163,15 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
private void startImportIconPack() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
_vaultManager.startActivityForResult(this, intent, CODE_IMPORT);
_vaultManager.fireIntentLauncher(this, intent, importResultLauncher);
}
private void updateEmptyState() {
if (_adapter.getItemCount() > 0) {
_iconPacksView.setVisibility(View.VISIBLE);
_iconPacksRecyclerView.setVisibility(View.VISIBLE);
_noIconPacksView.setVisibility(View.GONE);
} else {
_iconPacksView.setVisibility(View.GONE);
_iconPacksRecyclerView.setVisibility(View.GONE);
_noIconPacksView.setVisibility(View.VISIBLE);
}
}
@@ -9,10 +9,11 @@ import android.view.View;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -30,29 +31,36 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.ImportEntriesActivity;
import com.beemdevelopment.aegis.ui.TransferEntriesActivity;
import com.beemdevelopment.aegis.ui.components.DropdownCheckBoxes;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
import com.beemdevelopment.aegis.ui.tasks.ExportTask;
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
import com.beemdevelopment.aegis.vault.Vault;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.crypto.Cipher;
@@ -61,9 +69,32 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
private DatabaseImporter.Definition _importerDef;
private Vault.EntryFilter _exportFilter;
private final ActivityResultLauncher<Intent> importSelectResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
Intent data = activityResult.getData();
if (data != null) {
onImportSelectResult(activityResult.getResultCode(), data);
}
});
private final ActivityResultLauncher<Intent> exportResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult ->
onExportResult(CODE_EXPORT, activityResult.getResultCode(), activityResult.getData()));
private final ActivityResultLauncher<Intent> exportPlainResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult ->
onExportResult(CODE_EXPORT_PLAIN, activityResult.getResultCode(), activityResult.getData()));
private final ActivityResultLauncher<Intent> exportHtmlResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult ->
onExportResult(CODE_EXPORT_HTML, activityResult.getResultCode(), activityResult.getData()));
private final ActivityResultLauncher<Intent> exportGoogleUriResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult ->
onExportResult(CODE_EXPORT_GOOGLE_URI, activityResult.getResultCode(), activityResult.getData()));
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_import_export);
if (savedInstanceState != null) {
@@ -77,7 +108,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
_vaultManager.startActivityForResult(this, intent, CODE_IMPORT_SELECT);
_vaultManager.fireIntentLauncher(this, intent, importSelectResultLauncher);
});
return true;
});
@@ -109,28 +140,6 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
outState.putSerializable("importerDef", _importerDef);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CODE_IMPORT) {
getResult().putExtra("needsRecreate", true);
} else if (data != null) {
switch (requestCode) {
case CODE_IMPORT_SELECT:
onImportSelectResult(resultCode, data);
break;
case CODE_EXPORT:
// intentional fallthrough
case CODE_EXPORT_PLAIN:
// intentional fallthrough
case CODE_EXPORT_HTML:
// intentional fallthrough
case CODE_EXPORT_GOOGLE_URI:
onExportResult(requestCode, resultCode, data);
break;
}
}
}
private void onImportSelectResult(int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
@@ -152,7 +161,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
Intent intent = new Intent(requireContext(), ImportEntriesActivity.class);
intent.putExtra("importerDef", importerDef);
intent.putExtra("file", file);
startActivityForResult(intent, CODE_IMPORT);
startActivity(intent);
}
private void startExport() {
@@ -162,8 +171,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
CheckBox checkBoxEncrypt = view.findViewById(R.id.checkbox_export_encrypt);
CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept);
CheckBox checkBoxExportAllGroups = view.findViewById(R.id.export_selected_groups);
LinearLayout groupsSelection = view.findViewById(R.id.select_groups);
TextView groupsSelectionDescriptor = view.findViewById(R.id.select_groups_hint);
TextInputLayout groupsSelectionLayout = view.findViewById(R.id.group_selection_layout);
DropdownCheckBoxes<VaultGroupModel> groupsSelection = view.findViewById(R.id.group_selection_dropdown);
TextView passwordInfoText = view.findViewById(R.id.text_separate_password);
passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE);
AutoCompleteTextView dropdown = view.findViewById(R.id.dropdown_export_format);
@@ -176,18 +185,19 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
passwordInfoText.setVisibility(checkBoxEncrypt.isChecked() && isBackupPasswordSet ? View.VISIBLE : View.GONE);
});
TreeSet<String> groups = _vaultManager.getVault().getGroups();
Collection<VaultGroup> groups = _vaultManager.getVault().getUsedGroups();
if (groups.size() > 0) {
checkBoxExportAllGroups.setVisibility(View.VISIBLE);
for (String group: groups) {
CheckBox box = new CheckBox(requireContext());
box.setText(group);
box.setChecked(false);
groupsSelection.addView(box);
}
ArrayList<VaultGroupModel> groupsArray = new ArrayList<>();
groupsArray.add(new VaultGroupModel(getString(R.string.no_group)));
groupsArray.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
groupsSelection.setCheckedItemsCountTextRes(R.plurals.export_groups_selected_count);
groupsSelection.addItems(groupsArray, false);
}
AlertDialog dialog = new AlertDialog.Builder(requireContext())
AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.pref_export_summary)
.setView(view)
.setNeutralButton(R.string.share, null)
@@ -199,26 +209,45 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
Button btnNeutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
DialogStateValidator stateValidator = () -> {
boolean noGroupsSelected = groupsSelection.getCheckedItems().isEmpty();
boolean validState = (checkBoxEncrypt.isChecked() || checkBoxAccept.isChecked()) &&
(checkBoxExportAllGroups.isChecked() || !noGroupsSelected);
if (noGroupsSelected && groupsSelectionLayout.getError() == null) {
CharSequence errorMsg = getString(R.string.export_no_groups_selected);
groupsSelectionLayout.setError(errorMsg);
} else if (!noGroupsSelected && groupsSelectionLayout.getError() != null) {
groupsSelectionLayout.setError(null);
groupsSelectionLayout.setErrorEnabled(false);
}
btnPos.setEnabled(validState);
btnNeutral.setEnabled(validState);
};
checkBoxEncrypt.setOnCheckedChangeListener((buttonView, isChecked) -> {
warningText.setVisibility(isChecked ? View.GONE : View.VISIBLE);
passwordInfoText.setVisibility(isChecked && isBackupPasswordSet ? View.VISIBLE : View.GONE);
checkBoxAccept.setVisibility(isChecked ? View.GONE : View.VISIBLE);
checkBoxAccept.setChecked(false);
btnPos.setEnabled(isChecked);
btnNeutral.setEnabled(isChecked);
stateValidator.enableIfValid();
});
checkBoxAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
btnPos.setEnabled(isChecked);
btnNeutral.setEnabled(isChecked);
stateValidator.enableIfValid();
});
checkBoxExportAllGroups.setOnCheckedChangeListener((button, isChecked) -> {
int visibility = isChecked ? View.GONE : View.VISIBLE;
groupsSelection.setVisibility(visibility);
groupsSelectionDescriptor.setVisibility(visibility);
groupsSelectionLayout.setVisibility(visibility);
stateValidator.enableIfValid();
});
groupsSelection.setOnDismissListener(stateValidator::enableIfValid);
btnPos.setOnClickListener(v -> {
dialog.dismiss();
@@ -229,22 +258,22 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
if (!checkBoxExportAllGroups.isChecked()) {
_exportFilter = getVaultEntryFilter(groupsSelection);
if (_exportFilter == null) {
Toast noGroupsSelected = new Toast(requireContext());
noGroupsSelected.setText(R.string.export_no_groups_selected);
noGroupsSelected.show();
Toast.makeText(requireContext(), R.string.export_no_groups_selected, Toast.LENGTH_SHORT).show();
return;
}
}
int pos = getStringResourceIndex(R.array.export_formats, dropdown.getText().toString());
int requestCode = getExportRequestCode(pos, checkBoxEncrypt.isChecked());
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked());
boolean encrypt = checkBoxEncrypt.isChecked();
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, encrypt);
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(getExportMimeType(requestCode))
.setType(getExportMimeType(getExportRequestCode(pos, encrypt)))
.putExtra(Intent.EXTRA_TITLE, fileInfo.toString());
_vaultManager.startActivityForResult(this, intent, requestCode);
_auditLogRepository.addVaultExportedEvent();
ActivityResultLauncher<Intent> resultLauncher = getExportRequestLauncher(pos, encrypt);
_vaultManager.fireIntentLauncher(this, intent, resultLauncher);
});
btnNeutral.setOnClickListener(v -> {
@@ -258,16 +287,14 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
if (!checkBoxExportAllGroups.isChecked()) {
_exportFilter = getVaultEntryFilter(groupsSelection);
if (_exportFilter == null) {
Toast noGroupsSelected = new Toast(requireContext());
noGroupsSelected.setText(R.string.export_no_groups_selected);
noGroupsSelected.show();
return;
}
}
File file;
boolean encrypt = checkBoxEncrypt.isChecked();
try {
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked());
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, encrypt);
file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir());
} catch (IOException e) {
e.printStackTrace();
@@ -275,7 +302,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
return;
}
int requestCode = getExportRequestCode(pos, checkBoxEncrypt.isChecked());
int requestCode = getExportRequestCode(pos, encrypt);
ActivityResultLauncher<Intent> resultLauncher = getExportRequestLauncher(pos, encrypt);
startExportVault(requestCode, cb -> {
try (OutputStream stream = new FileOutputStream(file)) {
cb.exportVault(stream);
@@ -287,14 +315,14 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
// if the user creates an export, hide the backup reminder
_prefs.setLatestExportTimeNow();
_auditLogRepository.addVaultExportedEvent();
Uri uri = FileProvider.getUriForFile(requireContext(), 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));
_vaultManager.startActivity(this, chooser);
_vaultManager.fireIntentLauncher(this, chooser, resultLauncher);
}, _exportFilter);
_exportFilter = null;
});
@@ -303,18 +331,19 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
Dialogs.showSecureDialog(dialog);
}
private Vault.EntryFilter getVaultEntryFilter(LinearLayout view) {
Set<String> groups = new HashSet<>();
for (int i=0; i<view.getChildCount(); i++) {
CheckBox group = (CheckBox) view.getChildAt(i);
if (group.isChecked() && group.getText().toString().equals(getString(R.string.no_group))) {
groups.add(null);
} else if (group.isChecked()) {
groups.add(group.getText().toString());
}
private Vault.EntryFilter getVaultEntryFilter(DropdownCheckBoxes<VaultGroupModel> dropdownCheckBoxes) {
Set<UUID> groups = new HashSet<>();
for (VaultGroupModel group : dropdownCheckBoxes.getCheckedItems()) {
groups.add(group.getUUID());
}
return groups.isEmpty() ? null : entry -> groups.contains(entry.getGroup());
return groups.isEmpty() ? null : entry -> {
if (entry.getGroups().isEmpty()) {
return groups.contains(null);
} else {
return entry.getGroups().stream().anyMatch(groups::contains);
}
};
}
private void startGoogleAuthenticatorStyleExport() {
@@ -370,6 +399,16 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
return CODE_EXPORT_GOOGLE_URI;
}
private ActivityResultLauncher<Intent> getExportRequestLauncher(int spinnerPos, boolean encrypt) {
if (spinnerPos == 0) {
return encrypt ? exportResultLauncher : exportPlainResultLauncher;
} else if (spinnerPos == 1) {
return exportHtmlResultLauncher;
}
return exportGoogleUriResultLauncher;
}
private static VaultBackupManager.FileInfo getExportFileInfo(int spinnerPos, boolean encrypt) {
if (spinnerPos == 0) {
String filename = encrypt ? VaultRepository.FILENAME_PREFIX_EXPORT : VaultRepository.FILENAME_PREFIX_EXPORT_PLAIN;
@@ -462,7 +501,10 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
}
}
private void onExportResult(int requestCode, int resultCode, Intent data) {
private void onExportResult(int requestCode, int resultCode, @Nullable Intent data) {
if (data == null) {
return;
}
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
@@ -512,7 +554,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
} else {
// if the user creates an export, hide the backup reminder
_prefs.setLatestExportTimeNow();
_auditLogRepository.addVaultExportedEvent();
Toast.makeText(requireContext(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
}
}
@@ -525,4 +567,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
private interface StartExportCallback {
void exportVault(FinishExportCallback exportCb);
}
private interface DialogStateValidator {
void enableIfValid();
}
}
@@ -7,7 +7,6 @@ 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);
}
}
@@ -1,16 +1,18 @@
package com.beemdevelopment.aegis.ui.fragments.preferences;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.animation.Animation;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
@@ -22,16 +24,10 @@ import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public abstract class PreferencesFragment extends PreferenceFragmentCompat {
// activity request codes
public static final int CODE_IMPORT_SELECT = 0;
public static final int CODE_GROUPS = 3;
public static final int CODE_IMPORT = 4;
public static final int CODE_EXPORT = 5;
public static final int CODE_EXPORT_PLAIN = 6;
public static final int CODE_EXPORT_GOOGLE_URI = 7;
public static final int CODE_EXPORT_HTML = 8;
public static final int CODE_BACKUPS = 9;
private Intent _result;
@Inject
Preferences _prefs;
@@ -39,11 +35,8 @@ public abstract class PreferencesFragment extends PreferenceFragmentCompat {
@Inject
VaultManager _vaultManager;
@Override
@CallSuper
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setResult(new Intent());
}
@Inject
protected AuditLogRepository _auditLogRepository;
@Override
@CallSuper
@@ -58,13 +51,14 @@ public abstract class PreferencesFragment extends PreferenceFragmentCompat {
}
}
public Intent getResult() {
return _result;
}
@Override
@Nullable
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (nextAnim != 0) {
return AnimationsHelper.loadScaledAnimation(requireContext(), nextAnim, AnimationsHelper.Scale.TRANSITION);
}
public void setResult(Intent result) {
_result = result;
requireActivity().setResult(Activity.RESULT_OK, _result);
return super.onCreateAnimation(transit, enter, nextAnim);
}
protected boolean saveAndBackupVault() {
@@ -30,6 +30,7 @@ import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.Arrays;
import java.util.List;
@@ -54,18 +55,10 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_security);
Preference tapToRevealPreference = requirePreference("pref_tap_to_reveal");
tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
Preference screenPreference = requirePreference("pref_secure_screen");
screenPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRecreate", true);
Window window = requireActivity().getWindow();
if ((boolean) newValue) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
@@ -81,7 +74,6 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
Dialogs.showTapToRevealTimeoutPickerDialog(requireContext(), _prefs.getTapToRevealTime(), number -> {
_prefs.setTapToRevealTime(number);
tapToRevealTimePreference.setSummary(number + " seconds");
getResult().putExtra("needsRefresh", true);
});
return false;
});
@@ -91,9 +83,10 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
if (!_vaultManager.getVault().isEncryptionEnabled()) {
Dialogs.showSetPasswordDialog(requireActivity(), new EnableEncryptionListener());
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.disable_encryption)
.setMessage(getText(R.string.disable_encryption_description))
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
try {
_vaultManager.disableEncryption();
@@ -169,9 +162,10 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
task.execute(getLifecycle(), params);
} else {
_pinKeyboardPreference.setChecked(false);
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(R.string.pin_keyboard_error)
.setMessage(R.string.pin_keyboard_error_description)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.create());
@@ -192,7 +186,7 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
checkedItems[i] = _prefs.isAutoLockTypeEnabled(items[i]);
}
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.pref_auto_lock_prompt)
.setMultiChoiceItems(textItems, checkedItems, (dialog, index, isChecked) -> checkedItems[index] = isChecked)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
@@ -221,7 +215,7 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
.map(f -> getString(f.getStringRes()))
.toArray(String[]::new);
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.pref_password_reminder_title)
.setSingleChoiceItems(textItems, currFreq.ordinal(), (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
@@ -459,9 +453,10 @@ public class SecurityPreferencesFragment extends PreferencesFragment {
if (result != null) {
_pinKeyboardPreference.setChecked(true);
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(R.string.pin_keyboard_error)
.setMessage(R.string.invalid_password)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.create());
@@ -6,6 +6,7 @@ import android.graphics.drawable.PictureDrawable;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
@@ -19,7 +20,7 @@ import java.nio.ByteBuffer;
public class AegisGlideModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.prepend(VaultEntry.class, ByteBuffer.class, new IconLoader.Factory());
registry.prepend(VaultEntryIcon.class, ByteBuffer.class, new VaultEntryIconLoader.Factory());
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
.append(InputStream.class, SVG.class, new SvgDecoder())
.append(ByteBuffer.class, SVG.class, new SvgBytesDecoder());
@@ -0,0 +1,111 @@
package com.beemdevelopment.aegis.ui.glide;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.DrawableImageViewTarget;
import com.bumptech.glide.request.target.Target;
import java.io.File;
public class GlideHelper {
private GlideHelper() {
}
public static void loadIconFile(RequestManager rm, File file, IconType iconType, ImageView targetView) {
load(rm.load(file), iconType, targetView);
}
public static void loadIcon(RequestManager rm, IconPack.Icon icon, ImageView targetView) {
loadIconFile(rm, icon.getFile(), icon.getIconType(), targetView);
}
public static void loadEntryIcon(RequestManager rm, VaultEntry entry, ImageView targetView) {
if (entry.hasIcon()) {
setCommonOptions(rm.load(entry.getIcon()), entry.getIcon().getType()).into(targetView);
} else {
// Clear any pending loads for targetView, so that the TextDrawable
// we're about to display doesn't get overwritten when that pending load finishes
rm.clear(targetView);
setLayerType(targetView, IconType.INVALID);
TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName(), targetView);
targetView.setImageDrawable(drawable);
}
}
private static void load(RequestBuilder<Drawable> rb, IconType iconType, ImageView targetView) {
setCommonOptions(rb, iconType).into(targetView);
}
public static RequestBuilder<Drawable> setCommonOptions(RequestBuilder<Drawable> rb, IconType iconType) {
if (iconType != null) {
rb = rb.set(VaultEntryIconLoader.ICON_TYPE, iconType)
.listener(new ViewReadyListener<>(targetView -> {
targetView.setImageTintList(null);
setLayerType(targetView, iconType);
}));
}
return rb.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
}
/**
* Sets the layer type of the given ImageView based on the given IconType. If the
* icon type is SVG and SDK <= 27, the layer type is set to software. Otherwise, it
* is set to hardware.
*/
private static void setLayerType(ImageView view, IconType iconType) {
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return;
}
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
}
private static class ViewReadyListener<T> implements RequestListener<T> {
private final Listener<T> _listener;
public ViewReadyListener(Listener<T> listener) {
_listener = listener;
}
@Override
public boolean onLoadFailed(@Nullable GlideException e, @Nullable Object model, @NonNull Target<T> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(@NonNull T resource, @NonNull Object model, Target<T> target, @NonNull DataSource dataSource, boolean isFirstResource) {
if (target instanceof DrawableImageViewTarget) {
DrawableImageViewTarget viewTarget = (DrawableImageViewTarget) target;
if (_listener != null) {
_listener.onConfigureImageView(viewTarget.getView());
}
}
return false;
}
public interface Listener<T> {
void onConfigureImageView(ImageView targetView);
}
}
}
@@ -22,7 +22,7 @@ public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
@Override
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
return options.get(IconLoader.ICON_TYPE) == IconType.SVG;
return options.get(VaultEntryIconLoader.ICON_TYPE) == IconType.SVG;
}
public Resource<SVG> decode(
@@ -2,30 +2,30 @@ package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.bumptech.glide.load.Key;
import java.security.MessageDigest;
import java.util.UUID;
public class UUIDKey implements Key {
private UUID _uuid;
public class VaultEntryIconKey implements Key {
private final VaultEntryIcon _icon;
public UUIDKey(UUID uuid) {
_uuid = uuid;
public VaultEntryIconKey(VaultEntryIcon icon) {
_icon = icon;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(_uuid.toString().getBytes(CHARSET));
messageDigest.update(_icon.getHash());
}
@Override
public boolean equals(Object o) {
return _uuid.equals(o);
return _icon.equals(o);
}
@Override
public int hashCode() {
return _uuid.hashCode();
return _icon.hashCode();
}
}
@@ -3,7 +3,7 @@ package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Option;
@@ -15,29 +15,29 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import java.nio.ByteBuffer;
public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
public class VaultEntryIconLoader implements ModelLoader<VaultEntryIcon, ByteBuffer> {
public static final Option<IconType> ICON_TYPE = Option.memory("ICON_TYPE", IconType.INVALID);
@Override
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntry model, int width, int height, @NonNull Options options) {
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model));
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntryIcon icon, int width, int height, @NonNull Options options) {
return new LoadData<>(new VaultEntryIconKey(icon), new Fetcher(icon));
}
@Override
public boolean handles(@NonNull VaultEntry model) {
public boolean handles(@NonNull VaultEntryIcon icon) {
return true;
}
public static class Fetcher implements DataFetcher<ByteBuffer> {
private final VaultEntry _model;
private final VaultEntryIcon _icon;
private Fetcher(VaultEntry model) {
_model = model;
private Fetcher(VaultEntryIcon icon) {
_icon = icon;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
byte[] bytes = _model.getIcon();
byte[] bytes = _icon.getBytes();
ByteBuffer buf = ByteBuffer.wrap(bytes);
callback.onDataReady(buf);
}
@@ -65,11 +65,11 @@ public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
}
}
public static class Factory implements ModelLoaderFactory<VaultEntry, ByteBuffer> {
public static class Factory implements ModelLoaderFactory<VaultEntryIcon, ByteBuffer> {
@NonNull
@Override
public ModelLoader<VaultEntry, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
return new IconLoader();
public ModelLoader<VaultEntryIcon, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
return new VaultEntryIconLoader();
}
@Override
@@ -2,7 +2,6 @@ package com.beemdevelopment.aegis.ui.intro;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
@@ -14,7 +13,9 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.ui.AegisActivity;
import com.google.android.material.button.MaterialButton;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -27,8 +28,8 @@ public abstract class IntroBaseActivity extends AegisActivity implements IntroAc
private List<Class<? extends SlideFragment>> _slides;
private WeakReference<SlideFragment> _currentSlide;
private ImageButton _btnPrevious;
private ImageButton _btnNext;
private MaterialButton _btnPrevious;
private MaterialButton _btnNext;
private SlideIndicator _slideIndicator;
@Override
@@ -135,7 +136,10 @@ public abstract class IntroBaseActivity extends AegisActivity implements IntroAc
Class<? extends SlideFragment> newSlide = _slides.get(pos);
if (!onBeforeSlideChanged(oldSlide, newSlide)) {
_pager.setCurrentItem(pos);
// We can't easily control the speed of the smooth scroll animation, but we
// can at least disable it if animations are disabled
boolean smoothScroll = !AnimationsHelper.Scale.TRANSITION.isZero(this);
_pager.setCurrentItem(pos, smoothScroll);
}
onAfterSlideChanged(oldSlide, newSlide);
@@ -154,7 +158,7 @@ public abstract class IntroBaseActivity extends AegisActivity implements IntroAc
? View.VISIBLE
: View.INVISIBLE);
if (pos == _slides.size() - 1) {
_btnNext.setImageResource(R.drawable.circular_button_done);
_btnNext.setIconResource(R.drawable.ic_outline_check_24);
}
_slideIndicator.setSlideCount(_slides.size());
_slideIndicator.setCurrentSlide(pos);
@@ -0,0 +1,40 @@
package com.beemdevelopment.aegis.ui.models;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.io.Serializable;
public class AssignIconEntry implements Serializable {
private final VaultEntry _entry;
private IconPack.Icon _newIcon;
private transient AssignIconEntry.Listener _listener;
public void setOnResetListener(AssignIconEntry.Listener listener) {
_listener = listener;
}
public AssignIconEntry(VaultEntry entry) {
_entry = entry;
}
public VaultEntry getEntry() {
return _entry;
}
public IconPack.Icon getNewIcon() { return _newIcon; }
public void setNewIcon(IconPack.Icon icon) {
_newIcon = icon;
if (_listener != null) {
_listener.onNewIconChanged();
}
}
public interface Listener {
void onNewIconChanged();
}
}
@@ -0,0 +1,24 @@
package com.beemdevelopment.aegis.ui.models;
import com.beemdevelopment.aegis.database.AuditLogEntry;
import com.beemdevelopment.aegis.vault.VaultEntry;
import javax.annotation.Nullable;
public class AuditLogEntryModel {
private AuditLogEntry _auditLogEntry;
private VaultEntry _referencedVaultEntry;
public AuditLogEntryModel(AuditLogEntry auditLogEntry, @Nullable VaultEntry referencedVaultEntry) {
_auditLogEntry = auditLogEntry;
_referencedVaultEntry = referencedVaultEntry;
}
public AuditLogEntry getAuditLogEntry() {
return _auditLogEntry;
}
public VaultEntry getReferencedVaultEntry() {
return _referencedVaultEntry;
}
}
@@ -0,0 +1,21 @@
package com.beemdevelopment.aegis.ui.models;
import android.view.View;
public class ErrorCardInfo {
private final String _message;
private final View.OnClickListener _listener;
public ErrorCardInfo(String message, View.OnClickListener listener) {
_message = message;
_listener = listener;
}
public String getMessage() {
return _message;
}
public View.OnClickListener getListener() {
return _listener;
}
}
@@ -0,0 +1,45 @@
package com.beemdevelopment.aegis.ui.models;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.vault.VaultGroup;
import java.io.Serializable;
import java.util.UUID;
public class VaultGroupModel implements Serializable {
private final VaultGroup _group;
private final String _placeholderName;
public VaultGroupModel(VaultGroup group) {
_group = group;
_placeholderName = null;
}
public VaultGroupModel(String placeholderName) {
_group = null;
_placeholderName = placeholderName;
}
public VaultGroup getGroup() {
return _group;
}
public String getName() {
return _group != null ? _group.getName() : _placeholderName;
}
public boolean isPlaceholder() {
return _group == null;
}
@Nullable
public UUID getUUID() {
return _group == null ? null : _group.getUUID();
}
@NonNull
@Override
public String toString() {
return getName();
}
}

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