Compare commits

..
311 Commits
Author SHA1 Message Date
Michael Schättgen 36bd8f945a Release v3.4
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2025-06-04 22:20:24 +02:00
Michael Schättgen ab9cdabddc Add support for 5 new languages 2025-06-04 22:15:01 +02:00
Michael Schättgen d35c67d9d3 Update translations from Crowdin 2025-06-04 22:14:46 +02:00
Michael Schättgen 313b33793f Add preview for every icon pack to README 2025-06-03 22:55:45 +02:00
Alexander Bakker 94aabdcd88 Add comment to string dialog_duplicate_entry_title 2025-05-31 14:26:10 +02:00
Alexander Bakker d6b372bad5 Release v3.4-beta1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2025-05-29 13:48:10 +02:00
Alexander Bakker af4e3d9566 Update translations from Crowdin 2025-05-29 13:44:23 +02:00
Alexander BakkerandGitHub 8d667cd26c Merge pull request #1652 from michaelschattgen/feature/duplicate-check
Add check for duplicates upon saving entry
2025-05-29 13:18:15 +02:00
Michael Schättgen 1335be6787 Add check for duplicates upon saving entry 2025-05-29 13:13:23 +02:00
Alexander BakkerandGitHub efaa6afda6 Merge pull request #1654 from michaelschattgen/fix/negative-animation-period
Fix animation crash when using period of 7
2025-05-29 13:10:10 +02:00
Michael Schättgen 1c30557779 Fix animation crash when using period of 7 2025-05-29 12:59:24 +02:00
Alexander BakkerandGitHub 6f270144e2 Merge pull request #1599 from michaelschattgen/feature/haptic-feedback
Add haptic feedback toggle for code refresh
2025-05-29 12:41:57 +02:00
Michael SchättgenandGitHub 62c0d273a5 Merge pull request #1649 from alexbakker/minsdk-23
Update gradle, update dependencies and bump minSdkVersion to 23
2025-05-28 13:38:10 +02:00
Michael SchättgenandGitHub 3c4e8b44a0 Merge pull request #1650 from alexbakker/fix-1244
Disable PNG cruncher and PNG generation for vector drawables
2025-05-28 13:23:22 +02:00
Michael Schättgen 63d2666230 Add haptic feedback toggle for code refresh
Improve haptic feedback logic
2025-05-28 13:21:37 +02:00
Alexander Bakker 6c40cfa748 Disable PNG cruncher and PNG generation for vector drawables 2025-05-23 14:33:21 +02:00
Alexander Bakker 6758ec498b Update gradle, update dependencies and bump minSdkVersion to 23
New versions of Jetpack libraries require minSdkVersion 23.

There are a couple of changes in the code to fix incorrect usage of
annotations that started breaking the build after this Gradle update.
2025-05-23 14:01:58 +02:00
Michael SchättgenandGitHub 819865230f Merge pull request #1642 from alexbakker/fix-1616
Specify country code for the Portuguese translation
2025-04-23 20:57:15 +02:00
Michael SchättgenandGitHub 97762fa146 Merge pull request #1641 from alexbakker/fix-1605
Fall back to default values in the FreeOTP importer
2025-04-23 20:56:59 +02:00
Alexander BakkerandGitHub 60eea0bcb2 Merge pull request #1612 from michaelschattgen/feature/brightness-slider-transfer
Add brightness slider for transfer activity
2025-04-23 17:54:54 +02:00
Michael Schättgen 3818f9408d Add brightness slider for transfer activity 2025-04-23 17:48:15 +02:00
mimi89999andAlexander Bakker 03c00d51ba Add Crowdin config file and update crowdin-cli
Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2025-04-23 17:08:36 +02:00
Alexander Bakker afa1fbd3ae Specify country code for the Portuguese translation 2025-04-23 16:20:34 +02:00
Alexander Bakker c81e08bf1f Fall back to default values in the FreeOTP importer 2025-04-23 15:51:49 +02:00
Alexander BakkerandGitHub a5dc861336 Merge pull request #1637 from jahway603/faq_encrypted-export
[FAQ] clarified about which password to use when importing your vault
2025-04-23 14:24:28 +02:00
jahway603 fbfdd50069 [FAQ] clarified about which password to use when importing your vault to resolve Issue #1636 2025-04-15 18:54:46 -04:00
Michael SchättgenandGitHub 19fe7bd4d2 Merge pull request #1611 from alexbakker/update-divider-decor
Update divider decoration when filter/sort changes
2025-02-25 18:22:19 +01:00
Michael SchättgenandGitHub 7882ecc33a Merge pull request #1604 from alexbakker/flush-export
Flush temporary export file before starting ExportTask
2025-02-24 23:43:56 +01:00
Alexander Bakker d39b44f0c3 Update divider decoration when filter/sort changes
This fixes an issue where the item decoration may be wrong in some
cases. For example, adding a new entry to the bottom of the list may not
update the decoration of the item that was previously the last one in
the list.

To reproduce, use this vault: https://alexbakker.me/u/mov4455gp5.json.
Start without a group filter, apply sorting based on Issuer (A to Z) and
enable group multiselect. Then:
- Tap the "Test" chip
- Tap the "Test2" chip
- Tap the "No group" chip
- Notice that the offset between the last 2 entries looks wrong: https://alexbakker.me/u/nedcyiro2q.png

Probably introduced in 9131cae944.
2025-02-24 14:16:43 +01:00
Alexander BakkerandGitHub 7c6e3ae2a8 Merge pull request #1559 from michaelschattgen/feature/multiselect-groups
Add ability to multiselect groups
2025-02-24 13:38:29 +01:00
Michael Schättgen 78ee38ba7d Add ability to multiselect groups 2025-01-25 20:21:02 +01:00
Alexander BakkerandGitHub 8ddf8c58da Merge pull request #1602 from michaelschattgen/feature/color-contrast-hidden-codes
Improve color contrast on hidden codes
2025-01-24 16:42:30 +01:00
Alexander BakkerandGitHub ce29d120a9 Merge pull request #1600 from michaelschattgen/fix/import-entries-padding
Fix obstructing snackbar padding
2025-01-24 16:15:33 +01:00
Alexander BakkerandGitHub 6bbb42fb83 Merge pull request #1593 from dcrewi/feature/test-html-exports
add test for html exports
2025-01-24 14:41:29 +01:00
Alexander Bakker e8d712ec71 Flush temporary export file before starting ExportTask
The previous logic was not an issue because FileOutputStream is
unbuffered, but still, this is more correct.
2025-01-24 14:34:41 +01:00
Alexander BakkerandGitHub ad2dc803fb Merge pull request #1592 from dcrewi/feature/delete-temp-file
delete temporary export file when finished
2025-01-24 14:17:01 +01:00
Michael Schättgen 3d50ab1b65 Improve color contrast on hidden codes 2025-01-22 22:17:52 +01:00
Michael Schättgen a4812c530d Fix obstructing snackbar padding 2025-01-22 18:33:54 +01:00
Alexander Bakker 9ab949a59e Release v3.3.4
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2025-01-12 19:01:02 +01:00
Alexander Bakker e8bf7b0506 Update translations from Crowdin 2025-01-12 18:49:41 +01:00
Michael SchättgenandGitHub ec92fb2b31 Merge pull request #1591 from alexbakker/resize-icons
Store non-SVG icons at a maximum of 512x512 and migrate existing icons
2025-01-12 15:29:08 +01:00
David Creswick 919e6854e8 add test for html exports
Do some basic tests of the html export code.

- Make sure that a file was created.
- Make sure that the file can be parsed by an xml parser without error.
- Make sure that the document tag is "html".
2025-01-05 16:31:49 -06:00
David Creswick d98e23a1e5 delete temporary export file when finished 2025-01-05 16:19:45 -06:00
Alexander Bakker e59df63e94 Store non-SVG icons at a maximum of 512x512 and migrate existing icons 2025-01-05 22:47:26 +01:00
Michael SchättgenandGitHub 14643b4000 Merge pull request #1588 from alexbakker/stratum
Rename Authenticator Pro -> Stratum
2025-01-05 17:22:58 +01:00
Alexander Bakker 5439067e9f Rename Authenticator Pro -> Stratum 2025-01-05 13:49:51 +01:00
Alexander Bakker 81a26ccad8 Release v3.3.3
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2025-01-02 22:08:00 +01:00
Alexander Bakker 1fb36b0578 Update translations from Crowdin 2025-01-02 22:01:35 +01:00
Alexander BakkerandGitHub de74daef33 Merge pull request #1584 from alexbakker/large-heap
Set largeHeap to true in AndroidManifest
2025-01-02 21:59:41 +01:00
Alexander Bakker fe8b638818 Set largeHeap to true in AndroidManifest
This is a temporary measure to help users who are stuck in a situation
where they run into OOM conditions due to large icons in their vault
file.
2025-01-02 21:46:57 +01:00
Alexander Bakker 05a415bb38 Only run the scheduled codeql job on the upstream repo 2024-12-31 19:44:52 +01:00
Michael SchättgenandGitHub 920df1d9be Merge pull request #1577 from alexbakker/2fas-fixes
Store service name as issuer and tolerate spaces in secret for 2FAS
2024-12-22 23:53:46 +01:00
Alexander Bakker 19a77209d8 Store service name as issuer and tolerate spaces in secret for 2FAS 2024-12-20 15:51:06 +01:00
Alexander Bakker aec16f22c1 Update reactivecircus/android-emulator-runner
Required for the ubuntu-latest switch to ubuntu-24.04
2024-12-20 15:50:35 +01:00
Alexander BakkerandGitHub a76f3394f3 Merge pull request #1550 from cillyvms/window-insets
Apply window insets to accommodate system UI.
2024-12-04 21:18:34 +01:00
cillyvms 6039cfa20e Apply window insets to prevent UI elements from going behind system windows. 2024-12-04 19:50:51 +01:00
Alexander Bakker 0eb1194578 Release v3.3.2
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-12-02 22:35:13 +01:00
Alexander Bakker 170f626c9e Update translations from Crowdin 2024-12-02 22:29:48 +01:00
Alexander BakkerandGitHub c616a4f43c Merge pull request #1563 from michaelschattgen/feature/export-file-name-share
Make file name of exports consistent
2024-12-02 21:11:46 +01:00
Michael SchättgenandGitHub 411a677fbf Merge pull request #1548 from alexbakker/fix-menu-button-state
Fix state updates for the lock and sort menu items
2024-12-02 18:58:40 +01:00
Alexander BakkerandGitHub d48f2ead28 Merge pull request #1564 from michaelschattgen/fix/contrast-next-code
Improve contrast of next code color
2024-12-02 18:46:04 +01:00
Alexander BakkerandGitHub 8147d07606 Merge pull request #1562 from michaelschattgen/fix/group-entry-selection
Reset selection state when changing groups
2024-12-02 18:41:42 +01:00
Alexander BakkerandGitHub fa073371b5 Merge pull request #1561 from michaelschattgen/fix/import-multiple-entries
Add fix for importing multiple entries
2024-12-02 18:37:48 +01:00
Michael Schättgen 3a9e27bacb Improve contrast of next code color 2024-12-02 18:21:18 +01:00
Michael Schättgen 51f656dd6b Make file name of exports consistent 2024-12-02 18:06:12 +01:00
Michael Schättgen 3efe74d375 Reset selection state when changing groups 2024-12-02 17:56:04 +01:00
Michael Schättgen f9ada47956 Add fix for importing multiple entries 2024-12-02 17:38:28 +01:00
Alexander BakkerandGitHub 5213bafc97 Merge pull request #1545 from michaelschattgen/feature/search-improvements
Improve search feature for better UX
2024-11-25 22:54:38 +01:00
Michael Schättgen 503ce87c91 Improve search feature for better UX 2024-11-25 21:05:37 +01:00
Alexander Bakker d2fcb24d79 Fix state updates for the lock and sort menu items
Turns out I was a little too enthusiastically removing things in 9d383b85d8.
The menu may not necessarily have been created yet in all cases.
2024-11-25 20:47:20 +01:00
Alexander Bakker 337cb74f72 Release v3.3.1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-11-24 22:25:32 +01:00
Alexander Bakker b5b29a4f84 Revert "Fix layout height in tiles mode"
This reverts commit 5dba1db93d.

Fixes #1544
2024-11-24 22:20:52 +01:00
Alexander Bakker 3e3df919b2 Release v3.3
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-11-24 16:42:26 +01:00
Alexander Bakker 79ba822ccf Update translations from Crowdin 2024-11-24 16:40:17 +01:00
Alexander BakkerandGitHub 5f885cba29 Merge pull request #1542 from michaelschattgen/fix/text-height-tiles
Fix layout height in tiles mode
2024-11-24 16:36:48 +01:00
Michael Schättgen 5dba1db93d Fix layout height in tiles mode 2024-11-24 15:03:02 +01:00
Michael SchättgenandGitHub 161b79f0d4 Merge pull request #1535 from alexbakker/fix-steam
Make subclasses of TotpInfo override only getOtp(long time)
2024-11-24 14:21:19 +01:00
Michael SchättgenandGitHub c250a17a19 Merge pull request #1533 from alexbakker/fix-sort-category
Update sort category radio button state in the menu
2024-11-24 13:45:13 +01:00
Alexander Bakker 843e5f1ab5 Make subclasses of TotpInfo override only getOtp(long time)
This fixes an issue where Steam OTP's were displayed in the wrong
format. The underlying issue has been present for a while, but it first
became apparent in e4c9a584f4.
2024-11-17 10:39:33 +01:00
Alexander Bakker 9d383b85d8 Update sort category radio button state in the menu
This has been broken since 46e1421c28.

I also removed some other logic from ``onCreateOptionsMenu`` that
doesn't seem to belong there anymore.
2024-11-15 17:25:43 +01:00
Alexander Bakker 6d8eec0e21 Release v3.3-beta1
build / build (push) Canceled after 0s
build / test (push) Canceled after 0s
2024-11-15 16:10:16 +01:00
Alexander Bakker fb8765f8f0 Update translations from Crowdin 2024-11-15 15:33:26 +01:00
Alexander Bakker 337d2c3507 Fix a couple of entry equality checks in the adapter
With the introduction of DiffUtil, an entry might not be the same
instance in the in-memory vault as in the shown entry list of the
adapter.
2024-11-15 15:24:03 +01:00
Alexander BakkerandGitHub 8eabef2050 Merge pull request #1496 from michaelschattgen/fix/dynamic-progress-color
Fix progress bar colors when using dynamic
2024-11-15 14:14:57 +01:00
Michael SchättgenandAlexander Bakker eb7b8881a0 Fix progress bar colors when using dynamic 2024-11-15 14:14:11 +01:00
Alexander BakkerandGitHub b70654152d Merge pull request #1439 from r3dh3ck/feature/single_backup
Single backup
2024-11-15 13:53:54 +01:00
r3dh3ckandAlexander Bakker 37ebcd3a4b Implement single backup 2024-11-15 13:51:29 +01:00
Alexander Bakker 9751a38ebd Update dependencies 2024-11-15 12:36:18 +01:00
Michael SchättgenandGitHub 2ecde423a3 Merge pull request #1516 from alexbakker/api-35
Bump targetSdkVersion to 35 and update dependencies
2024-11-12 00:45:54 +01:00
Michael SchättgenandGitHub e8f06660dc Merge pull request #1506 from alexbakker/freeotp2
Add support for importing FreeOTP 2 backups
2024-11-12 00:17:27 +01:00
Michael SchättgenandGitHub c8d5be6462 Merge pull request #1510 from alexbakker/better-share-entries
Minor improvements to the entry sharing activity
2024-10-23 23:48:38 +02:00
Michael SchättgenandGitHub bc29242f55 Merge pull request #1505 from alexbakker/diffutil
Use DiffUtil for the RecyclerView of the entry list
2024-10-23 22:34:23 +02:00
Alexander Bakker d395bbeb8d Bump targetSdkVersion to 35 and update dependencies
This also includes changes to make the status guard hack work
on Android 15 and a couple of small adjustments to support edge-to-edge
in all activities.
2024-10-18 15:38:45 +02:00
Michael SchättgenandGitHub 939fa0e1ec Merge pull request #1514 from alexbakker/no-red-dots
Always cancel the delayed color change when hiding the code
2024-10-09 23:30:49 +02:00
Alexander Bakker 44358b3c95 Always cancel the delayed color change when hiding the code
Before, the dots may turn red if the animation duration scale is set to
0.
2024-10-09 21:06:09 +02:00
Alexander BakkerandGitHub 413e793c7b Merge pull request #1507 from michaelschattgen/feature/show-next-code
Add ability to show next code
2024-10-09 17:35:13 +02:00
Michael SchättgenandAlexander Bakker e4c9a584f4 Add ability to show next code
Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2024-10-09 17:15:13 +02:00
Alexander Bakker c9e8d4dbdf Minor improvements to the entry sharing activity
This patch makes a couple of minor improvements to the entry sharing
activity:
- Remove the double "Transfer entries" heading.
- Make the QR codes larger. Especially helpful with Google Authenticator
  exports.
- Increase screen brightness to 100%.

Before and after:

<img width="200" src="https://alexbakker.me/u/d91cl1x495.png"/>
<img width="200" src="https://alexbakker.me/u/ckzhrs5nf5.png"/>

<img width="200" src="https://alexbakker.me/u/6bo0womot0.png"/>
<img width="200" src="https://alexbakker.me/u/mw7yskjn7z.png"/>
2024-10-04 18:14:26 +02:00
Alexander BakkerandGitHub 0573dbb2fc Merge pull request #1509 from michaelschattgen/fix/secret-multiline
Make secret multiline
2024-10-04 11:02:33 +02:00
Michael Schättgen 7753b482b1 Make secret multiline 2024-10-02 13:33:27 +02:00
Alexander Bakker cc5ce485b1 Add support for importing FreeOTP 2 backups
I've held off on this in the past, because I was concerned about the
security issues related to Java object deserialization. To circumvent
that, I've written a parser that understands just enough of the Java
Object Serialization format to parse FreeOTP 2 backups.

Unfortunately there are a number of issues in FreeOTP 2 that may result in
corrupt backups. The importer warns the user about this and tries to
salvage as many entries as possible.
2024-09-28 15:49:22 +02:00
Alexander Bakker 9131cae944 Use DiffUtil for the RecyclerView of the entry list
Gets rid of all of the custom logic we had for notifying the
RecyclerView about changes in the entry list. This will allow for more
simplifications in the future around non-persisted changes to state in
the entry list.

A neat side effect is that any filtering/ordering changes in the entry
list are now also animated: https://alexbakker.me/u/4a4ie5yzpj.mp4

This touches the fundamentals of the entry list, so lots of careful
testing required.
2024-09-25 22:29:49 +02:00
Alexander Bakker 08d900c0c0 Rename onAssignEntriesResult to onAssignIconsResult 2024-09-25 11:36:35 +02:00
Alexander BakkerandGitHub 24d3d0ae8f Merge pull request #1503 from michaelschattgen/fix/locale-marshmallow
Fix visibility of locale setting on Marshmallow
2024-09-24 23:44:08 +02:00
Michael Schättgen 45831e117c Fix visibility of locale setting on Marshmallow 2024-09-24 23:39:03 +02:00
Alexander Bakker c559ed9e56 Add NEW_GROUP placeholder type for group models to fix the build 2024-09-24 22:03:27 +02:00
Alexander BakkerandGitHub 8e3279bb7e Merge pull request #1498 from michaelschattgen/feature/single-tap-group
Change group filter to a single selection
2024-09-24 21:48:31 +02:00
Michael SchättgenandAlexander Bakker 7ea2f5c4a5 Change group filter to a single selection 2024-09-24 21:47:40 +02:00
Alexander BakkerandGitHub 9ef3315a70 Merge pull request #1497 from michaelschattgen/feature/assign-groups
Add ability to easily assign groups
2024-09-24 21:26:25 +02:00
Michael SchättgenandAlexander Bakker 4a9f189897 Add ability to easily assign groups 2024-09-24 21:24:51 +02:00
Alexander BakkerandGitHub 92de13b176 Merge pull request #1494 from michaelschattgen/feature/show-code-expiration
Show when codes are about to expire
2024-09-24 20:56:21 +02:00
Michael SchättgenandAlexander Bakker 1e383463ae Show when codes are about to expire
Co-authored-by: Alexander Bakker <ab@alexbakker.me>
2024-09-24 20:55:11 +02:00
Alexander Bakker dbaec2d83f Use getQuantityString instead of getQuantityText for import_partial_export_anyway 2024-09-21 15:24:44 +02:00
Alexander Bakker 257a40eefa Remove some unused resources (and increase severity of check) 2024-09-21 15:09:54 +02:00
Alexander Bakker baa8068d51 Redefine "import_partial_export_anyway" as a quantity string 2024-09-21 14:17:56 +02:00
Alexander Bakker d433957c2f Remove usage of deprecated PreferenceManager 2024-09-21 14:12:06 +02:00
Alexander Bakker 6a54650635 Format multiple string substitutions in positional format
This resolves the following lint error: "Multiple substitutions
specified in non-positional format of string resource"
2024-09-21 14:01:04 +02:00
Alexander Bakker bab59e8d04 Update dependencies 2024-09-21 13:07:34 +02:00
Alexander BakkerandGitHub 83689a4c59 Merge pull request #1499 from michaelschattgen/fix/text-field-caps
Fix capitalization of multiple text fields
2024-09-19 21:39:47 +02:00
Alexander BakkerandGitHub 356fa8a36e Merge pull request #1470 from sigmundxia/master
Add support for Ente Auth import
2024-09-19 20:53:25 +02:00
Sigmund Xia 58002c31ef Add support for Ente Auth import 2024-09-19 10:26:05 +08:00
Michael Schättgen d81d741fee Fix capitalization of multiple text fields 2024-09-19 00:55:02 +02:00
Alexander BakkerandGitHub 17f106f70d Merge pull request #1492 from michaelschattgen/fix/google-auth-proto
Fix batch_index in Google Authenticator export
2024-09-17 21:30:48 +02:00
Michael Schättgen 91b632b9cf Fix batch_index in Google Authenticator export 2024-09-17 20:59:13 +02:00
Alexander BakkerandGitHub 8b8e071831 Merge pull request #1479 from michaelschattgen/feature/group-chipgroup
Improve group filters
2024-09-16 23:47:07 +02:00
Michael Schättgen 9c151d83c1 Improve group filters 2024-09-16 23:42:07 +02:00
Michael SchättgenandGitHub e63ec4d1e8 Merge pull request #1490 from alexbakker/fix-pack-crash
Fix a crash that could occur when deleting a broken icon pack import
2024-09-16 22:58:03 +02:00
Alexander Bakker f8603395fa Fix a crash that could occur when deleting a broken icon pack import
The following crash could occur when trying to import an icon pack for
which an empty folder still exists on disk:

```
Exception java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 1
  at jdk.internal.util.Preconditions.outOfBounds (Preconditions.java:64)
  at jdk.internal.util.Preconditions.outOfBoundsCheckIndex (Preconditions.java:70)
  at jdk.internal.util.Preconditions.checkIndex (Preconditions.java:266)
  at java.util.Objects.checkIndex (Objects.java:359)
  at java.util.ArrayList.remove (ArrayList.java:511)
  at com.beemdevelopment.aegis.ui.views.IconPackAdapter.removeIconPack (IconPackAdapter.java:38)
  at com.beemdevelopment.aegis.ui.fragments.preferences.IconPacksManagerFragment.removeIconPack (IconPacksManagerFragment.java:158)
  at com.beemdevelopment.aegis.ui.fragments.preferences.IconPacksManagerFragment.lambda$importIconPack$3 (IconPacksManagerFragment.java:133)
  at androidx.appcompat.app.AlertController$ButtonHandler.handleMessage (AlertController.java:167)
  at android.os.Handler.dispatchMessage (Handler.java:106)
  at android.os.Looper.loopOnce (Looper.java:226)
  at android.os.Looper.loop (Looper.java:313)
  at android.app.ActivityThread.main (ActivityThread.java:8762)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:604)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)
```

To reproduce on a debug build of Aegis with the latest aegis-icons
imported into the app, run the following commands and then try to import
the same aegis-icons ZIP again:

```
$ adb shell
$ su
# rm -r /data/data/com.beemdevelopment.aegis.debug/files/icons/c1018b93-4e8c-490a-b575-30dde62a833e/20230523/*
```
2024-09-16 22:19:02 +02:00
Alexander BakkerandGitHub df30e42318 Merge pull request #1481 from michaelschattgen/feature/reorder-groups
Add ability to reorder groups
2024-09-12 22:51:15 +02:00
Alexander BakkerandGitHub 6cb58789a2 Merge pull request #1483 from michaelschattgen/fix/single-copy-reveal
Prevent copying when revealing code
2024-09-11 21:04:02 +02:00
Alexander BakkerandGitHub eb6e26a8e4 Merge pull request #1482 from michaelschattgen/feature/rounded-progressbar
Make progressbar rounded on the right side
2024-09-11 20:48:49 +02:00
Michael Schättgen 8c1cc9a475 Prevent copying when revealing code 2024-09-11 14:03:34 +02:00
Michael Schättgen aab046ca04 Make progressbar rounded on the right side 2024-09-11 13:19:19 +02:00
Michael Schättgen d40e619cab Add ability to reorder groups 2024-09-11 12:24:50 +02:00
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
470 changed files with 23424 additions and 6748 deletions
-10
View File
@@ -1,10 +0,0 @@
project_id: "372633"
api_token: "<api-token-here>"
base_path: "../app/src/main"
base_url: "https://api.crowdin.com"
preserve_hierarchy: true
files:
- source: "res/values/strings.xml"
dest: "strings.xml"
translation: "res/values-%android_code%/%original_file_name%"
+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"
+15 -14
View File
@@ -5,36 +5,37 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@55e685c48d84285a5b0418cd094606e199cca3b6
- uses: actions/setup-java@v3
uses: gradle/wrapper-validation-action@699bb18358f12c5b78b37bb0111d3a0e2276e0e2
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Build the app
run: ./gradlew build
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: apk
path: app/build/outputs/apk/debug/app-debug.apk
test:
runs-on: macos-latest
# This is probably pretty expensive for GitHub, so restrict the repositories that this job runs on
if: github.repository == 'beemdevelopment/Aegis' || github.repository == 'alexbakker/Aegis'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-java@v3
uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Install HAXM
run: brew install --cask intel-haxm
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Tests
uses: reactivecircus/android-emulator-runner@50986b1464923454c95e261820bc626f38490ec0
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d
with:
api-level: 31
arch: x86_64
@@ -52,8 +53,8 @@ jobs:
adb logcat -d > artifacts/logcat.txt
cp -r app/build/reports/androidTests/connected/* artifacts/report/
if adb shell '[ -e /sdcard/Pictures/screenshots ]'; then adb pull /sdcard/Pictures/screenshots artifacts/; fi
# test ! -f tests_failing
- uses: actions/upload-artifact@v3
test ! -f tests_failing
- uses: actions/upload-artifact@v4
if: always()
with:
name: instrumented-test-report
+5 -4
View File
@@ -14,9 +14,10 @@ jobs:
actions: read
contents: read
security-events: write
if: github.event_name != 'schedule' || github.repository == 'beemdevelopment/Aegis'
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Exclude paths
# The importers are excluded from analysis, because some of the apps Aegis
# can import from don't have such great crypto, which will cause false
@@ -24,18 +25,18 @@ jobs:
run: |
find app/src/main/java/com/beemdevelopment/aegis/importers ! \( -name AegisImporter.java -o -name "DatabaseImporter*" \) -type f -exec rm -f {} +
sed -i '/Importer.class/d' app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
- uses: actions/setup-java@v3
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: java
- name: Build
run: ./gradlew assembleDebug
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+5 -11
View File
@@ -10,22 +10,16 @@ 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
echo "ee9f838b819ccedc33c9b2537055e5ba7d7934561b24df1e1a6274cbd6e27f2d crowdin-cli.zip" | sha256sum -c
wget https://github.com/crowdin/crowdin-cli/releases/download/4.6.1/crowdin-cli.zip
echo "7afd70de3a747ac631a5bad7866008163ae1d50c4606b5773f0b90a5481ffde2 crowdin-cli.zip" | sha256sum -c
unzip crowdin-cli.zip -d crowdin-cli
- name: Upload to Crowdin
env:
CROWDIN_TOKEN: "${{ secrets.CROWDIN_TOKEN }}"
CROWDIN_PERSONAL_TOKEN: "${{ secrets.CROWDIN_TOKEN }}"
run: |
java -jar ./crowdin-cli/3.7.2/crowdin-cli.jar upload sources \
java -jar ./crowdin-cli/4.6.1/crowdin-cli.jar upload sources \
--no-progress \
--token "$CROWDIN_TOKEN" \
--project-id 372633 \
--base-path app/src/main \
--source res/values/strings.xml \
--translation "res/values-%android_code%/%original_file_name%" \
--dest strings.xml \
--branch master
+8
View File
@@ -86,6 +86,14 @@ Another common setup is to configure Aegis to back up to a folder on local
storage of your device and then have a separate app (like
[Syncthing](https://syncthing.net/)) sync that folder anywhere you want.
## Encrypted Backups
### Why do I not get prompted to enter an encryption password when exporting?
Aegis uses the same password you have configured to encrypt your vault as the
password which is used when exporting and importing your vault; so when prompted,
you will enter that when importing your vault.
## Importing
### When importing from Authenticator Plus, an error is shown claiming that Accounts.txt is missing
+25 -6
View File
@@ -123,20 +123,33 @@ documentation](docs/iconpacks.md).
Unofficial monochrome-styled 2FA icons.
[<img width=500 alt="aegis-icons preview"
src="https://raw.githubusercontent.com/aegis-icons/aegis-icons/master/showcase.png">](https://github.com/aegis-icons/aegis-icons)
src="metadata/en-US/images/iconPacks/aegis-icons.png">](https://github.com/aegis-icons/aegis-icons)
- [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.
[<img width=500 alt="delta-icons preview"
src="metadata/en-US/images/iconPacks/delta-icons.png">](https://github.com/Delta-Icons/aegis-icons)
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons) \*
This project periodically generates an icon pack for Aegis based on [Simple
Icons](https://simpleicons.org/).
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) *
[<img width=500 alt="aegis-simple-icons preview"
src="metadata/en-US/images/iconPacks/aegis-simple-icons.png">](https://github.com/alexbakker/aegis-simple-icons)
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) \*
This is a variant on the aegis-simple-icons pack where the icons contain no solid background and just the outlines are being used.
[<img width=500 alt="aegis-simple-icons-outlined preview"
src="metadata/en-US/images/iconPacks/aegis-simple-icons-outlined.png">](https://github.com/michaelschattgen/aegis-simple-icons-outlined)
\* The icons are automatically generated, so
not all of them are as high quality as the ones you'll find in
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
not all of them are as high quality as the ones you'll find in
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
## Contributing
@@ -151,3 +164,9 @@ 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)
+83 -55
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 {
compileSdk 33
compileSdk 35
namespace packageName
defaultConfig {
applicationId "${packageName}"
minSdkVersion 21
targetSdkVersion 33
versionCode 62
versionName "2.2.2"
minSdkVersion 23
targetSdkVersion 35
versionCode 79
versionName "3.4"
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'
}
@@ -86,28 +93,41 @@ android {
}
}
// Required to make the APK reproducible
aaptOptions {
cruncherEnabled = false
}
defaultConfig {
vectorDrawables.generatedDensities = []
}
packagingOptions {
// R8 doesn't remove these resources, so exclude them manually. This reduces APK size by 4MB.
resources {
excludes += ['/org/bouncycastle/pqc/**/*.properties']
excludes += [
'/org/bouncycastle/pqc/**/*.properties',
'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
]
}
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true
}
lint {
abortOnError true
checkDependencies true
disable 'MissingQuantity', 'MissingTranslation'
}
buildFeatures {
buildConfig true
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.8.0'
artifact = 'com.google.protobuf:protoc:3.25.1'
}
generateProtoTasks {
all().each { task ->
@@ -120,37 +140,46 @@ protobuf {
}
}
dependencies {
def cameraxVersion = '1.2.3'
def glideVersion = '4.16.0'
def guavaVersion = '32.1.2'
def hiltVersion = '2.47'
def junitVersion = '4.13.2'
def libsuVersion = '5.2.0'
aboutLibraries {
// Tasks for aboutLibraries are not run automatically to keep the build reproducible
// To update manually: ./gradlew app:exportLibraryDefinitions -PaboutLibraries.exportPath=src/main/res/raw
prettyPrint = true
configPath = "app/config"
fetchRemoteFunding = false
registerAndroidTasks = false
exclusionPatterns = [~"javax.annotation.*"]
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
}
annotationProcessor 'androidx.annotation:annotation:1.6.0'
dependencies {
def cameraxVersion = '1.4.2'
def glideVersion = '4.16.0'
def guavaVersion = '33.4.8'
def hiltVersion = '2.56.2'
def junitVersion = '4.13.2'
def libsuVersion = '6.0.0'
def roomVersion = '2.7.1'
annotationProcessor 'androidx.annotation:annotation:1.9.1'
annotationProcessor "androidx.room:room-compiler:$roomVersion"
annotationProcessor "com.google.dagger:hilt-compiler:$hiltVersion"
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
// Ridiculous fix for a bunch of "Duplicate class" build errors:
implementation (platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.activity:activity:1.7.2'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.activity:activity:1.10.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.camera:camera-camera2:$cameraxVersion"
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
implementation 'androidx.camera:camera-view:1.2.3'
implementation 'androidx.cardview:cardview:1.0.0'
implementation "androidx.core:core:1.10.1"
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation "androidx.lifecycle:lifecycle-process:2.6.1"
implementation "androidx.camera:camera-view:$cameraxVersion"
implementation 'androidx.core:core:1.16.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.documentfile:documentfile:1.1.0'
implementation 'androidx.lifecycle:lifecycle-process:2.9.0'
implementation "androidx.preference:preference:1.2.1"
implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation "androidx.room:room-runtime:$roomVersion"
implementation 'androidx.viewpager2:viewpager2:1.1.0'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation "com.google.dagger:hilt-android:$hiltVersion"
implementation 'com.github.avito-tech:krop:0.52'
@@ -162,36 +191,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.9.0'
implementation 'com.google.protobuf:protobuf-javalite:3.22.0'
implementation 'com.google.zxing:core:3.5.2'
implementation "com.mikepenz:iconics-core:3.2.5"
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
implementation 'com.nulab-inc:zxcvbn:1.8.2'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation 'de.psdev.licensesdialog:licensesdialog:2.2.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.google.protobuf:protobuf-javalite:4.31.0'
implementation 'com.google.zxing:core:3.5.3'
implementation('com.mikepenz:aboutlibraries:11.2.3') {
exclude group: 'com.mikepenz', module: 'aboutlibraries-core'
}
implementation 'com.mikepenz:aboutlibraries-core-android:11.2.3'
implementation 'com.nulab-inc:zxcvbn:1.9.0'
implementation 'net.lingala.zip4j:zip4j:2.11.5'
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
implementation 'org.bouncycastle:bcprov-jdk18on:1.76'
implementation "org.simpleflatmapper:sfm-csv:8.2.3"
implementation 'org.bouncycastle:bcprov-jdk18on:1.80'
implementation 'org.simpleflatmapper:sfm-csv:8.2.3'
androidTestAnnotationProcessor "com.google.dagger:hilt-android-compiler:$hiltVersion"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hiltVersion"
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
androidTestImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
androidTestImplementation "junit:junit:${junitVersion}"
androidTestUtil 'androidx.test:orchestrator:1.4.2'
androidTestUtil 'androidx.test:orchestrator:1.5.1'
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:20230618'
testImplementation 'org.robolectric:robolectric:4.10.3'
testImplementation 'org.json:json:20250517'
testImplementation 'org.robolectric:robolectric:4.14.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}
+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"
}
+6 -1
View File
@@ -1,10 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="MissingTranslation" severity="ignore" />
<issue id="MissingQuantity" severity="ignore" />
<issue id="InvalidPackage">
<ignore regexp="X509LDAPCertStoreSpi" />
</issue>
<issue id="NotificationPermission">
<ignore regexp="com.bumptech.glide.request.target.NotificationTarget" />
</issue>
<issue id="ResourceType" severity="Warning" />
<issue id="UnusedResources" severity="error">
<ignore path="res/raw/aboutlibraries.json" />
<ignore regexp="res/mipmap.*/ic_launcher_debug.*.png" />
</issue>
</lint>
+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()));
}
};
}
}
@@ -3,8 +3,8 @@ package com.beemdevelopment.aegis;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.preference.PreferenceManager;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.runner.AndroidJUnitRunner;
@@ -61,13 +61,20 @@ import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
@@ -183,7 +190,9 @@ public class BackupExportTest extends AegisTest {
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
doExport();
File file = doExport();
checkHtmlExport(file);
}
@Test
@@ -196,7 +205,9 @@ public class BackupExportTest extends AegisTest {
onView(withText(R.string.export_format_html)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(android.R.id.button1)).perform(click());
onView(withId(R.id.checkbox_accept)).perform(click());
doExport();
File file = doExport();
checkHtmlExport(file);
}
@Test
@@ -380,6 +391,26 @@ public class BackupExportTest extends AegisTest {
checkReadEntries(entries);
}
private void checkHtmlExport(File file) {
try (InputStream inStream = new FileInputStream(file)) {
Reader inReader = new InputStreamReader(inStream, StandardCharsets.UTF_8);
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser parser = factory.newPullParser();
parser.setInput(inReader);
while (parser.getEventType() != XmlPullParser.START_TAG) {
parser.next();
}
if (!parser.getName().toLowerCase(Locale.ROOT).equals("html")) {
throw new RuntimeException("not an html document!");
}
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
parser.next();
}
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException("Unable to read html export file", e);
}
}
private void checkReadEntries(Collection<VaultEntry> entries) {
List<VaultEntry> vectors = VaultEntries.get();
assertEquals(vectors.size(), entries.size());
@@ -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());
@@ -170,12 +177,10 @@ public class OverallTest extends AegisTest {
}
private void changeGroupFilter(String text) {
onView(withId(R.id.chip_group)).perform(click());
if (text == null) {
onView(withId(R.id.btnClear)).perform(click());
onView(allOf(withText(R.string.no_group), isDescendantOfA(withId(R.id.groupChipGroup)))).perform(click());
} else {
onView(withText(text)).perform(click());
onView(isRoot()).perform(pressBack());
onView(allOf(withText(text), isDescendantOfA(withId(R.id.groupChipGroup)))).perform(click());
}
}
@@ -183,7 +188,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 +208,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()));
}
+8 -3
View File
@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- NOTE: Disabled for now. See issue: #1047
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -27,8 +28,8 @@
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"
android:largeHeap="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 +83,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,7 +151,7 @@
</application>
<queries>
<package android:name="me.jmh.authenticatorpro" />
<package android:name="com.stratumauth.app" />
<package android:name="com.authy.authy" />
<package android:name="org.fedorahosted.freeotp" />
<package android:name="org.liberty.android.freeotpplus" />
@@ -155,7 +160,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>
+144
View File
@@ -31,6 +31,150 @@
</head>
<body>
<div></div>
<h3>Version 3.4</h3>
<h4>New</h4>
<ul>
<li>Haptic feedback when an entry is about to expire</li>
<li>Brightness increase is now toggleable in the entry transfer view</li>
<li>Filter on multiple groups simultaneously</li>
<li>Color contrast on hidden codes has been improved</li>
<li>Prompt before the user is about to save an entry with a duplicate name/issuer combination</li>
<li>New languages: Estonian, Korean, Malayalam, Norwegian (Bokmål) and Serbian</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>A crash could occur if an entry with period 7 exists and code expiry indication is enabled</li>
<li>The Portuguese (Brazilian) locale was used even if Portuguese was configured</li>
<li>FreeOTP import would fail if the algorithm or digits field was not specified for an entry</li>
<li>The divider between entries would be missing in certain filter configurations</li>
<li>The snackbar in try entry importing view could obstruct the name of an entry</li>
</ul>
<h4>Miscellaneous</h4>
<ul>
<li>Android 6 or newer is now required the run the app</li>
</ul>
<h3>Version 3.3.4</h3>
<h4>Fixes</h4>
<ul>
<li>Icons are now resized to 512x512 to reduce the size of the vault file and to reduce the chance of encountering out of memory conditions</li>
</ul>
<h3>Version 3.3.3</h3>
<h4>Fixes</h4>
<ul>
<li>Some users ran into out of memory conditions due to large icons in their vault file. We've introduced a temporary measure that should help in most cases, but we'll follow up with a more comprehensive fix soon.</li>
<li>Window insets were not always applied correctly, causing parts of the UI to appear off-screen</li>
<li>The 2FAS importer did not tolerate spaces for secrets and was not always able to extract the issuer</li>
</ul>
<h3>Version 3.3.2</h3>
<h4>New</h4>
<ul>
<li>Find entries by searching in multiple fields simultaneously</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Entries would not actually be added to the Aegis vault in some cases when importing from Google Authenticator export QR codes</li>
<li>The lock button was sometimes shown for unencrypted vaults</li>
<li>The sort category menu item did not always reflect the current sorting</li>
<li>The next code was not always easy to read because its color had low contrast with the background</li>
<li>Entry selection was not cancelled when changing the group filter</li>
</ul>
<h3>Version 3.3.1</h3>
<h4>Fixes</h4>
<ul>
<li>Codes were not shown in case the tiles view mode was combined with hidden account names</li>
</ul>
<h3>Version 3.3</h3>
<h4>New</h4>
<ul>
<li>Significant improvements to group filtering
<ul>
<li>Groups can now be filtered on straight from the main view instead of through a dialog</li>
<li>Ability to assign multiple entries to a group in one go</li>
<li>Support for reordering groups</li>
</ul>
</li>
<li>Codes now change color when they're about to expire</li>
<li>Option to show the next code ahead of time</li>
<li>Support for backing up to a single file (This enables support for more cloud providers, such as Google Drive)</li>
<li>Various minor improvements to make QR code exports easier to scan</li>
<li>Support for importing from Ente Auth</li>
<li>Support for importing FreeOTP 2 backups</li>
<li>Updated translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>QR codes exported for Google Authenticator could not be scanned on iOS</li>
<li>The code would be copied after a single tap in case "Tap to reveal" and "Copy tokens to the clipboard" were enabled simultaneously</li>
<li>Various other minor UI, stability and performance improvements</li>
</ul>
<h3>Version 3.2</h3>
<h4>New</h4>
<ul>
<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>
@@ -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());
}
}
@@ -22,8 +22,6 @@ import com.beemdevelopment.aegis.receivers.VaultLockReceiver;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.mikepenz.iconics.Iconics;
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
import com.topjohnwu.superuser.Shell;
import java.util.Collections;
@@ -48,9 +46,6 @@ public abstract class AegisApplicationBase extends Application {
super.onCreate();
_vaultManager = EarlyEntryPoints.get(this, EntryPoint.class).getVaultManager();
Iconics.init(this);
Iconics.registerFont(new MaterialDesignIconic());
VaultLockReceiver lockReceiver = new VaultLockReceiver();
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
ContextCompat.registerReceiver(this, lockReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
@@ -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,7 @@
package com.beemdevelopment.aegis;
public enum BackupsVersioningStrategy {
UNDEFINED,
MULTIPLE_BACKUPS,
SINGLE_BACKUP
}
@@ -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;
}
}
}
@@ -0,0 +1,20 @@
package com.beemdevelopment.aegis;
public enum GroupPlaceholderType {
ALL,
NEW_GROUP,
NO_GROUP;
public int getStringRes() {
switch (this) {
case ALL:
return R.string.all;
case NEW_GROUP:
return R.string.new_group;
case NO_GROUP:
return R.string.no_group;
default:
throw new IllegalArgumentException("Unexpected placeholder type: " + this);
}
}
}
@@ -5,12 +5,15 @@ import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.provider.DocumentsContractCompat;
import androidx.preference.PreferenceManager;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.TimeUtils;
import com.beemdevelopment.aegis.vault.VaultBackupPermissionException;
import org.json.JSONArray;
import org.json.JSONException;
@@ -20,9 +23,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 +36,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) {
@@ -67,10 +86,18 @@ public class Preferences {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
public boolean isGroupMultiselectEnabled() {
return _prefs.getBoolean("pref_groups_multiselect", false);
}
public boolean isEntryHighlightEnabled() {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean isHapticFeedbackEnabled() {
return _prefs.getBoolean("pref_haptic_feedback", true);
}
public boolean isPauseFocusedEnabled() {
boolean dependenciesEnabled = isTapToRevealEnabled() || isEntryHighlightEnabled();
if (!dependenciesEnabled) return false;
@@ -136,12 +163,24 @@ public class Preferences {
return _prefs.getBoolean("pref_show_icons", true);
}
public boolean getShowNextCode() {
return _prefs.getBoolean("pref_show_next_code", false);
}
public boolean getShowExpirationState() {
return _prefs.getBoolean("pref_expiration_state", true);
}
public CodeGrouping getCodeGroupSize() {
String value = _prefs.getString("pref_code_group_size_string", "GROUPING_THREES");
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);
}
@@ -155,6 +194,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;
}
@@ -195,6 +248,10 @@ public class Preferences {
_prefs.edit().putInt("pref_current_theme", theme.ordinal()).apply();
}
public boolean isDynamicColorsEnabled() {
return _prefs.getBoolean("pref_dynamic_colors", false);
}
public ViewMode getCurrentViewMode() {
return ViewMode.fromInteger(_prefs.getInt("pref_current_view_mode", 0));
}
@@ -224,10 +281,51 @@ public class Preferences {
setUsageCount(usageCounts);
}
public long getLastUsedTimestamp(UUID uuid) {
Map<UUID, Long> timestamps = getLastUsedTimestamps();
if (timestamps != null && timestamps.size() > 0){
Long timestamp = timestamps.get(uuid);
return timestamp != null ? timestamp : 0;
}
return 0;
}
public void clearUsageCount() {
_prefs.edit().remove("pref_usage_count").apply();
}
public Map<UUID, Long> getLastUsedTimestamps() {
Map<UUID, Long> lastUsedTimestamps = new HashMap<>();
String lastUsedTimestamp = _prefs.getString("pref_last_used_timestamps", "");
try {
JSONArray arr = new JSONArray(lastUsedTimestamp);
for (int i = 0; i < arr.length(); i++) {
JSONObject json = arr.getJSONObject(i);
lastUsedTimestamps.put(UUID.fromString(json.getString("uuid")), json.getLong("timestamp"));
}
} catch (JSONException ignored) {
}
return lastUsedTimestamps;
}
public void setLastUsedTimestamps(Map<UUID, Long> lastUsedTimestamps) {
JSONArray lastUsedTimestampJson = new JSONArray();
for (Map.Entry<UUID, Long> entry : lastUsedTimestamps.entrySet()) {
JSONObject entryJson = new JSONObject();
try {
entryJson.put("uuid", entry.getKey());
entryJson.put("timestamp", entry.getValue());
lastUsedTimestampJson.put(entryJson);
} catch (JSONException e) {
e.printStackTrace();
}
}
_prefs.edit().putString("pref_last_used_timestamps", lastUsedTimestampJson.toString()).apply();
}
public Map<UUID, Integer> getUsageCounts() {
Map<UUID, Integer> usageCounts = new HashMap<>();
String usageCount = _prefs.getString("pref_usage_count", "");
@@ -263,8 +361,16 @@ public class Preferences {
return _prefs.getInt("pref_timeout", -1);
}
public String getLanguage() {
return _prefs.getString("pref_lang", "system");
}
public void setLanguage(String lang) {
_prefs.edit().putString("pref_lang", lang).apply();
}
public Locale getLocale() {
String lang = _prefs.getString("pref_lang", "system");
String lang = getLanguage();
if (lang.equals("system")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -478,26 +584,39 @@ public class Preferences {
return _prefs.getBoolean("pref_minimize_on_copy", false);
}
public void setGroupFilter(List<String> groupFilter) {
public void setGroupFilter(Set<UUID> groupFilter) {
JSONArray json = new JSONArray(groupFilter);
_prefs.edit().putString("pref_group_filter", json.toString()).apply();
_prefs.edit().putString("pref_group_filter_uuids", json.toString()).apply();
}
public List<String> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter", null);
public Set<UUID> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter_uuids", null);
if (raw == null || raw.isEmpty()) {
return Collections.emptyList();
return Collections.emptySet();
}
try {
JSONArray json = new JSONArray(raw);
List<String> filter = new ArrayList<>();
Set<UUID> filter = new HashSet<>();
for (int i = 0; i < json.length(); i++) {
filter.add(json.isNull(i) ? null : json.optString(i));
filter.add(json.isNull(i) ? null : UUID.fromString(json.getString(i)));
}
return filter;
} catch (JSONException e) {
return Collections.emptyList();
return Collections.emptySet();
}
}
@NonNull
public BackupsVersioningStrategy getBackupVersioningStrategy() {
Uri uri = getBackupsLocation();
if (uri == null) {
return BackupsVersioningStrategy.UNDEFINED;
}
if (DocumentsContractCompat.isTreeUri(uri)) {
return BackupsVersioningStrategy.MULTIPLE_BACKUPS;
} else {
return BackupsVersioningStrategy.SINGLE_BACKUP;
}
}
@@ -505,14 +624,16 @@ public class Preferences {
private final Date _time;
private boolean _isBuiltIn;
private final String _error;
private final boolean _isPermissionError;
public BackupResult(@Nullable Exception e) {
this(new Date(), e == null ? null : e.toString());
this(new Date(), e == null ? null : e.toString(), e instanceof VaultBackupPermissionException);
}
private BackupResult(Date time, @Nullable String error) {
private BackupResult(Date time, @Nullable String error, boolean isPermissionError) {
_time = time;
_error = error;
_isPermissionError = isPermissionError;
}
@Nullable
@@ -540,12 +661,17 @@ public class Preferences {
_isBuiltIn = isBuiltIn;
}
public boolean isPermissionError() {
return _isPermissionError;
}
public String toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("time", _time.getTime());
obj.put("error", _error == null ? JSONObject.NULL : _error);
obj.put("isPermissionError", _isPermissionError);
} catch (JSONException e) {
throw new RuntimeException(e);
}
@@ -557,7 +683,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,8 @@ 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
);
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.LIGHT, R.style.Theme_Aegis_Light,
Theme.DARK, R.style.Theme_Aegis_Dark,
Theme.AMOLED, R.style.Theme_Aegis_Amoled
);
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis;
import java.util.Arrays;
public class VibrationPatterns {
public static final long[] EXPIRING = {475, 20, 5, 20, 965, 20, 5, 20, 965, 20, 5, 20, 420};
public static final long[] REFRESH_CODE = {0, 100};
public static long getLengthInMillis(long[] pattern) {
return Arrays.stream(pattern).sum();
}
}
@@ -35,19 +35,19 @@ public enum ViewMode {
}
/**
* Retrieves the height (in dp) that the divider between entries should have in this view mode.
* Retrieves the offset (in dp) that should exist between entries in this view mode.
*/
public float getDividerHeight() {
public float getItemOffset() {
if (this == ViewMode.COMPACT) {
return 0;
return 1;
} else if (this == ViewMode.TILES) {
return 4;
}
return 20;
return 8;
}
public int getColumnSpan() {
public int getSpanCount() {
if (this == ViewMode.TILES) {
return 2;
}
@@ -55,14 +55,6 @@ public enum ViewMode {
return 1;
}
public float getDividerWidth() {
if (this == ViewMode.TILES) {
return 4;
}
return 0;
}
public String getFormattedAccountName(String accountName) {
if (this == ViewMode.TILES) {
return accountName;
@@ -1,7 +1,5 @@
package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import com.beemdevelopment.aegis.crypto.bc.SCrypt;
import java.io.ByteArrayOutputStream;
@@ -23,7 +21,6 @@ import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class CryptoUtils {
@@ -66,13 +63,7 @@ public class CryptoUtils {
// generate the nonce if none is given
// we are not allowed to do this ourselves as "setRandomizedEncryptionRequired" is set to true
if (nonce != null) {
AlgorithmParameterSpec spec;
// apparently kitkat doesn't support GCMParameterSpec
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
spec = new IvParameterSpec(nonce);
} else {
spec = new GCMParameterSpec(CRYPTO_AEAD_TAG_SIZE * 8, nonce);
}
AlgorithmParameterSpec spec = new GCMParameterSpec(CRYPTO_AEAD_TAG_SIZE * 8, nonce);
cipher.init(opmode, key, spec);
} else {
cipher.init(opmode, key);
@@ -1,11 +1,8 @@
package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import androidx.annotation.RequiresApi;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -45,10 +42,6 @@ public class KeyStoreHandle {
}
public SecretKey generateKey(String id) throws KeyStoreHandleException {
if (!isSupported()) {
throw new KeyStoreHandleException("Symmetric KeyStore keys are not supported in this version of Android");
}
try {
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME);
generator.init(new KeyGenParameterSpec.Builder(id,
@@ -87,14 +80,13 @@ public class KeyStoreHandle {
throw new KeyStoreHandleException(e);
}
if (isSupported() && isKeyPermanentlyInvalidated(key)) {
if (isKeyPermanentlyInvalidated(key)) {
return null;
}
return key;
}
@RequiresApi(api = Build.VERSION_CODES.M)
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
try {
@@ -127,8 +119,4 @@ public class KeyStoreHandle {
throw new KeyStoreHandleException(e);
}
}
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}
@@ -25,10 +25,10 @@ public class YAOTP {
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period)
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
long seconds = System.currentTimeMillis() / 1000;
return generateOTP(secret, pin, digits, otpAlgo, seconds, period);
return generateOTP(secret, pin, digits, otpAlgo, period, seconds);
}
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long seconds, long period)
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period, long seconds)
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
byte[] pinWithHash;
byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8);
@@ -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);
});
}
}
@@ -1,6 +1,13 @@
package com.beemdevelopment.aegis.helpers;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import java.io.ByteArrayOutputStream;
import java.util.Objects;
public class BitmapHelper {
private BitmapHelper() {
@@ -28,4 +35,29 @@ public class BitmapHelper {
return Bitmap.createScaledBitmap(bitmap, width, height, true);
}
public static boolean isVaultEntryIconOptimized(VaultEntryIcon icon) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(icon.getBytes(), 0, icon.getBytes().length, opts);
return opts.outWidth <= VaultEntryIcon.MAX_DIMENS && opts.outHeight <= VaultEntryIcon.MAX_DIMENS;
}
public static VaultEntryIcon toVaultEntryIcon(Bitmap bitmap, IconType iconType) {
if (bitmap.getWidth() > VaultEntryIcon.MAX_DIMENS
|| bitmap.getHeight() > VaultEntryIcon.MAX_DIMENS) {
bitmap = resize(bitmap, VaultEntryIcon.MAX_DIMENS, VaultEntryIcon.MAX_DIMENS);
}
ByteArrayOutputStream stream = new ByteArrayOutputStream();
if (Objects.equals(iconType, IconType.PNG)) {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
} else {
iconType = IconType.JPEG;
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
}
byte[] data = stream.toByteArray();
return new VaultEntryIcon(data, iconType);
}
}
@@ -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");
}
@@ -56,16 +56,20 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
// It's not clear when this can happen, but sometimes the ViewHolder
// that's passed to this function has a position of -1, leading
// to a crash down the line.
int position = viewHolder.getAdapterPosition();
int position = viewHolder.getBindingAdapterPosition();
if (position == NO_POSITION) {
return 0;
}
int swipeFlags = 0;
EntryAdapter adapter = (EntryAdapter) recyclerView.getAdapter();
if (adapter == null) {
return 0;
}
int swipeFlags = 0;
if (adapter.isPositionFooter(position)
|| adapter.getEntryAt(position) != _selectedEntry
|| adapter.isPositionErrorCard(position)
|| adapter.getEntryAtPosition(position) != _selectedEntry
|| !isLongPressDragEnabled()) {
return makeMovementFlags(0, swipeFlags);
}
@@ -76,12 +80,13 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
if (target.getAdapterPosition() < _adapter.getShownFavoritesCount()){
int targetIndex = _adapter.translateEntryPosToIndex(target.getBindingAdapterPosition());
if (targetIndex < _adapter.getShownFavoritesCount()) {
return false;
}
int firstPosition = viewHolder.getLayoutPosition();
int secondPosition = target.getAdapterPosition();
int secondPosition = target.getBindingAdapterPosition();
_adapter.onItemMove(firstPosition, secondPosition);
_positionChanged = true;
@@ -90,7 +95,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
_adapter.onItemDismiss(viewHolder.getAdapterPosition());
_adapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
}
@Override
@@ -98,7 +103,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
super.clearView(recyclerView, viewHolder);
if (_positionChanged) {
_adapter.onItemDrop(viewHolder.getAdapterPosition());
_adapter.onItemDrop(viewHolder.getBindingAdapterPosition());
_positionChanged = false;
_adapter.refresh(false);
}
@@ -1,23 +1,58 @@
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);
} else if (getConfiguredTheme().equals(Theme.DARK)) {
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Dark);
}
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;
}
}
@@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import com.beemdevelopment.aegis.VibrationPatterns;
public class UiRefresher {
private boolean _running;
private Listener _listener;
@@ -23,7 +25,6 @@ public class UiRefresher {
}
_running = true;
_listener.onRefresh();
_handler.postDelayed(new Runnable() {
@Override
public void run() {
@@ -31,6 +32,27 @@ public class UiRefresher {
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
}
}, _listener.getMillisTillNextRefresh());
_handler.postDelayed(new Runnable() {
@Override
public void run() {
_listener.onExpiring();
_handler.postDelayed(this, getNextRun());
}
}, getInitialRun());
}
private long getInitialRun() {
long sum = _listener.getMillisTillNextRefresh() - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
if (sum < 0) {
return getNextRun();
}
return sum;
}
private long getNextRun() {
return (_listener.getMillisTillNextRefresh() + _listener.getPeriodMillis()) - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
}
public void stop() {
@@ -40,6 +62,8 @@ public class UiRefresher {
public interface Listener {
void onRefresh();
void onExpiring();
long getMillisTillNextRefresh();
long getPeriodMillis();
}
}
@@ -0,0 +1,44 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorManager;
import com.beemdevelopment.aegis.Preferences;
public class VibrationHelper {
private Preferences _preferences;
public VibrationHelper(Context context) {
_preferences = new Preferences(context);
}
public void vibratePattern(Context context, long[] pattern) {
if (!isHapticFeedbackEnabled()) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
VibratorManager vibratorManager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
if (vibratorManager != null) {
Vibrator vibrator = vibratorManager.getDefaultVibrator();
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
vibrator.vibrate(effect);
}
} else {
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
vibrator.vibrate(effect);
}
}
}
}
public boolean isHapticFeedbackEnabled() {
return _preferences.isHapticFeedbackEnabled();
}
}
@@ -0,0 +1,26 @@
package com.beemdevelopment.aegis.helpers;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.android.material.appbar.AppBarLayout;
public class ViewHelper {
private ViewHelper() {
}
public static void setupAppBarInsets(AppBarLayout appBar) {
ViewCompat.setOnApplyWindowInsetsListener(appBar, (targetView, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
targetView.setPadding(
insets.left,
insets.top,
insets.right,
0
);
return WindowInsetsCompat.CONSUMED;
});
}
}
@@ -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());
}
}
@@ -18,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;
@@ -59,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
@@ -162,15 +173,24 @@ 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 {
@@ -188,4 +208,9 @@ public class IconPack {
return new Icon(filename, name, category, issuers);
}
}
private enum MatchType {
NORMAL,
INVERSE
}
}
@@ -42,6 +42,10 @@ public class IconPackManager {
return packs.get(0);
}
public boolean hasIconPack() {
return _iconPacks.size() > 0;
}
public List<IconPack> getIconPacks() {
return new ArrayList<>(_iconPacks);
}
@@ -16,6 +16,7 @@ import com.beemdevelopment.aegis.vault.VaultEntryException;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.topjohnwu.superuser.io.SuFile;
@@ -27,6 +28,7 @@ import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
public class AegisImporter extends DatabaseImporter {
@@ -132,11 +134,31 @@ public class AegisImporter extends DatabaseImporter {
Result result = new Result();
try {
JSONArray array = _obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
JSONObject entryObj = array.getJSONObject(i);
if (_obj.has("groups")) {
JSONArray groupArray = _obj.getJSONArray("groups");
for (int i = 0; i < groupArray.length(); i++) {
JSONObject groupObj = groupArray.getJSONObject(i);
try {
VaultGroup group = convertGroup(groupObj);
if (!result.getGroups().has(group)) {
result.addGroup(group);
}
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
}
JSONArray entryArray = _obj.getJSONArray("entries");
for (int i = 0; i < entryArray.length(); i++) {
JSONObject entryObj = entryArray.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
for (UUID groupUuid : entry.getGroups()) {
if (!result.getGroups().has(groupUuid)) {
entry.getGroups().remove(groupUuid);
}
}
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
@@ -156,5 +178,13 @@ public class AegisImporter extends DatabaseImporter {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
private static VaultGroup convertGroup(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultGroup.fromJson(obj);
} catch (VaultEntryException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -21,6 +21,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
@@ -182,7 +183,7 @@ public class AndOtpImporter extends DatabaseImporter {
context.getResources().getString(R.string.andotp_old_format)
};
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
.setTitle(R.string.choose_andotp_importer)
.setSingleChoiceItems(choices, 0, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
@@ -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,17 +34,18 @@ public abstract class DatabaseImporter {
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
_importers.add(new Definition("Authenticator Pro", AuthenticatorProImporter.class, R.string.importer_help_authenticator_pro, true));
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
_importers.add(new Definition("Battle.net Authenticator", BattleNetImporter.class, R.string.importer_help_battle_net_authenticator, true));
_importers.add(new Definition("Bitwarden", BitwardenImporter.class, R.string.importer_help_bitwarden, false));
_importers.add(new Definition("Duo", DuoImporter.class, R.string.importer_help_duo, true));
_importers.add(new Definition("Ente Auth", EnteAuthImporter.class, R.string.importer_help_ente_auth, false));
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("FreeOTP+ (JSON)", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
_importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true));
_importers.add(new Definition("Stratum (Authenticator Pro)", StratumImporter.class, R.string.importer_help_stratum, true));
_importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
}
@@ -168,12 +170,17 @@ public abstract class DatabaseImporter {
public static class Result {
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private UUIDMap<VaultGroup> _groups = new UUIDMap<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(VaultEntry entry) {
_entries.add(entry);
}
public void addGroup(VaultGroup group) {
_groups.add(group);
}
public void addError(DatabaseImporterEntryException error) {
_errors.add(error);
}
@@ -182,6 +189,10 @@ public abstract class DatabaseImporter {
return _entries;
}
public UUIDMap<VaultGroup> getGroups() {
return _groups;
}
public List<DatabaseImporterEntryException> getErrors() {
return _errors;
}
@@ -0,0 +1,32 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class EnteAuthImporter extends DatabaseImporter {
public EnteAuthImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = IOUtils.readAll(stream);
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(requireContext());
return importer.read(new ByteArrayInputStream(bytes), isInternal);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
}
@@ -4,26 +4,53 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class FreeOtpImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
@@ -40,6 +67,24 @@ public class FreeOtpImporter extends DatabaseImporter {
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try (BufferedInputStream bufInStream = new BufferedInputStream(stream);
DataInputStream dataInStream = new DataInputStream(bufInStream)) {
dataInStream.mark(2);
int magic = dataInStream.readUnsignedShort();
dataInStream.reset();
if (magic == SerializedHashMapParser.MAGIC) {
return readV2(dataInStream);
} else {
return readV1(bufInStream);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
private DecryptedStateV1 readV1(InputStream stream) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
@@ -52,16 +97,184 @@ public class FreeOtpImporter extends DatabaseImporter {
entries.add(new JSONObject(entry.Value));
}
}
return new State(entries);
return new DecryptedStateV1(entries);
} catch (XmlPullParserException | IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private List<JSONObject> _entries;
private EncryptedState readV2(DataInputStream stream) throws DatabaseImporterException {
try {
Map<String, String> entries = SerializedHashMapParser.parse(stream);
JSONObject mkObj = new JSONObject(entries.get("masterKey"));
return new EncryptedState(mkObj, entries);
} catch (IOException | JSONException | SerializedHashMapParser.ParseException e) {
throw new DatabaseImporterException(e);
}
}
public State(List<JSONObject> entries) {
public static class EncryptedState extends State {
private static final int MASTER_KEY_SIZE = 32 * 8;
private final String _mkAlgo;
private final String _mkCipher;
private final byte[] _mkCipherText;
private final byte[] _mkParameters;
private final byte[] _mkToken;
private final byte[] _mkSalt;
private final int _mkIterations;
private final Map<String, String> _entries;
private EncryptedState(JSONObject mkObj, Map<String, String> entries)
throws DatabaseImporterException, JSONException {
super(true);
_mkAlgo = mkObj.getString("mAlgorithm");
if (!_mkAlgo.equals("PBKDF2withHmacSHA1") && !_mkAlgo.equals("PBKDF2withHmacSHA512")) {
throw new DatabaseImporterException(String.format("Unexpected master key KDF: %s", _mkAlgo));
}
JSONObject keyObj = mkObj.getJSONObject("mEncryptedKey");
_mkCipher = keyObj.getString("mCipher");
if (!_mkCipher.equals("AES/GCM/NoPadding")) {
throw new DatabaseImporterException(String.format("Unexpected master key cipher: %s", _mkCipher));
}
_mkCipherText = toBytes(keyObj.getJSONArray("mCipherText"));
_mkParameters = toBytes(keyObj.getJSONArray("mParameters"));
_mkToken = keyObj.getString("mToken").getBytes(StandardCharsets.UTF_8);
_mkSalt = toBytes(mkObj.getJSONArray("mSalt"));
_mkIterations = mkObj.getInt("mIterations");
_entries = entries;
}
public State decrypt(char[] password) throws DatabaseImporterException {
PBKDFTask.Params params = new PBKDFTask.Params(_mkAlgo, MASTER_KEY_SIZE, password, _mkSalt, _mkIterations);
SecretKey passKey = PBKDFTask.deriveKey(params);
return decrypt(passKey);
}
public State decrypt(SecretKey passKey) throws DatabaseImporterException {
byte[] masterKeyBytes;
try {
byte[] nonce = parseNonce(_mkParameters);
IvParameterSpec spec = new IvParameterSpec(nonce);
Cipher cipher = Cipher.getInstance(_mkCipher);
cipher.init(Cipher.DECRYPT_MODE, passKey, spec);
cipher.updateAAD(_mkToken);
masterKeyBytes = cipher.doFinal(_mkCipherText);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException |
IllegalBlockSizeException | InvalidKeyException |
InvalidAlgorithmParameterException | IOException e) {
throw new DatabaseImporterException(e);
}
SecretKey masterKey = new SecretKeySpec(masterKeyBytes, 0, masterKeyBytes.length, "AES");
return new DecryptedStateV2(_entries, masterKey);
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.importer_warning_title_freeotp2)
.setMessage(R.string.importer_warning_message_freeotp2)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, password -> {
PBKDFTask.Params params = getKeyDerivationParams(password, _mkAlgo);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
State state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog1 -> listener.onCanceled());
})
.create());
}
private PBKDFTask.Params getKeyDerivationParams(char[] password, String algo) {
return new PBKDFTask.Params(algo, MASTER_KEY_SIZE, password, _mkSalt, _mkIterations);
}
}
public static class DecryptedStateV2 extends DatabaseImporter.State {
private final Map<String, String> _entries;
private final SecretKey _masterKey;
public DecryptedStateV2(Map<String, String> entries, SecretKey masterKey) {
super(false);
_entries = entries;
_masterKey = masterKey;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
for (Map.Entry<String, String> entry : _entries.entrySet()) {
if (entry.getKey().endsWith("-token") || entry.getKey().equals("masterKey")) {
continue;
}
try {
JSONObject encObj = new JSONObject(entry.getValue());
String tokenKey = String.format("%s-token", entry.getKey());
JSONObject tokenObj = new JSONObject(_entries.get(tokenKey));
VaultEntry vaultEntry = convertEntry(encObj, tokenObj);
result.addEntry(vaultEntry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
} catch (JSONException ignored) {
}
}
return result;
}
private VaultEntry convertEntry(JSONObject encObj, JSONObject tokenObj)
throws DatabaseImporterEntryException {
try {
JSONObject keyObj = new JSONObject(encObj.getString("key"));
String cipherName = keyObj.getString("mCipher");
if (!cipherName.equals("AES/GCM/NoPadding")) {
throw new DatabaseImporterException(String.format("Unexpected cipher: %s", cipherName));
}
byte[] cipherText = toBytes(keyObj.getJSONArray("mCipherText"));
byte[] parameters = toBytes(keyObj.getJSONArray("mParameters"));
byte[] token = keyObj.getString("mToken").getBytes(StandardCharsets.UTF_8);
byte[] nonce = parseNonce(parameters);
IvParameterSpec spec = new IvParameterSpec(nonce);
Cipher cipher = Cipher.getInstance(cipherName);
cipher.init(Cipher.DECRYPT_MODE, _masterKey, spec);
cipher.updateAAD(token);
byte[] secretBytes = cipher.doFinal(cipherText);
JSONArray secretArray = new JSONArray();
for (byte b : secretBytes) {
secretArray.put(b);
}
tokenObj.put("secret", secretArray);
return DecryptedStateV1.convertEntry(tokenObj);
} catch (DatabaseImporterException | JSONException | NoSuchAlgorithmException |
NoSuchPaddingException | InvalidAlgorithmParameterException |
InvalidKeyException | BadPaddingException | IllegalBlockSizeException |
IOException e) {
throw new DatabaseImporterEntryException(e, tokenObj.toString());
}
}
}
public static class DecryptedStateV1 extends DatabaseImporter.State {
private final List<JSONObject> _entries;
public DecryptedStateV1(List<JSONObject> entries) {
super(false);
_entries = entries;
}
@@ -85,8 +298,8 @@ public class FreeOtpImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase(Locale.ROOT);
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
String algo = obj.optString("algo", OtpInfo.DEFAULT_ALGORITHM);
int digits = obj.optInt("digits", OtpInfo.DEFAULT_DIGITS);
byte[] secret = toBytes(obj.getJSONArray("secret"));
String issuer = obj.getString("issuerExt");
@@ -95,7 +308,7 @@ public class FreeOtpImporter extends DatabaseImporter {
OtpInfo info;
switch (type) {
case "totp":
int period = obj.getInt("period");
int period = obj.optInt("period", TotpInfo.DEFAULT_PERIOD);
if (issuer.equals("Steam")) {
info = new SteamInfo(secret, algo, digits, period);
} else {
@@ -116,6 +329,23 @@ public class FreeOtpImporter extends DatabaseImporter {
}
}
private static byte[] parseNonce(byte[] parameters) throws IOException {
ASN1Primitive prim = ASN1Sequence.fromByteArray(parameters);
if (prim instanceof ASN1OctetString) {
return ((ASN1OctetString) prim).getOctets();
}
if (prim instanceof ASN1Sequence) {
for (ASN1Encodable enc : (ASN1Sequence) prim) {
if (enc instanceof ASN1OctetString) {
return ((ASN1OctetString) enc).getOctets();
}
}
}
throw new IOException("Unable to find nonce in parameters");
}
private static byte[] toBytes(JSONArray array) throws JSONException {
byte[] bytes = new byte[array.length()];
for (int i = 0; i < array.length(); i++) {
@@ -123,4 +353,119 @@ public class FreeOtpImporter extends DatabaseImporter {
}
return bytes;
}
private static class SerializedHashMapParser {
private static final int MAGIC = 0xaced;
private static final int VERSION = 5;
private static final long SERIAL_VERSION_UID = 362498820763181265L;
private static final byte TC_NULL = 0x70;
private static final byte TC_CLASSDESC = 0x72;
private static final byte TC_OBJECT = 0x73;
private static final byte TC_STRING = 0x74;
private SerializedHashMapParser() {
}
public static Map<String, String> parse(DataInputStream inStream)
throws IOException, ParseException {
Map<String, String> map = new HashMap<>();
// Read/validate the magic number and version
int magic = inStream.readUnsignedShort();
int version = inStream.readUnsignedShort();
if (magic != MAGIC || version != VERSION) {
throw new ParseException("Not a serialized Java Object");
}
// Read the class descriptor info for HashMap
byte b = inStream.readByte();
if (b != TC_OBJECT) {
throw new ParseException("Expected an object, found: " + b);
}
b = inStream.readByte();
if (b != TC_CLASSDESC) {
throw new ParseException("Expected a class desc, found: " + b);
}
parseClassDescriptor(inStream);
// Not interested in the capacity of the map
inStream.readInt();
// Read the number of elements in the HashMap
int size = inStream.readInt();
// Parse each key-value pair in the map
for (int i = 0; i < size; i++) {
String key = parseStringObject(inStream);
String value = parseStringObject(inStream);
map.put(key, value);
}
return map;
}
private static void parseClassDescriptor(DataInputStream inputStream)
throws IOException, ParseException {
// Check whether we're dealing with a HashMap and a version we support
String className = parseUTF(inputStream);
if (!className.equals(HashMap.class.getName())) {
throw new ParseException(String.format("Unexpected class name: %s", className));
}
long serialVersionUID = inputStream.readLong();
if (serialVersionUID != SERIAL_VERSION_UID) {
throw new ParseException(String.format("Unexpected serial version UID: %d", serialVersionUID));
}
// Read past all of the fields in the class
byte fieldDescriptor = inputStream.readByte();
if (fieldDescriptor == TC_NULL) {
return;
}
int totalFieldSkip = 0;
int fieldCount = inputStream.readUnsignedShort();
for (int i = 0; i < fieldCount; i++) {
char fieldType = (char) inputStream.readByte();
parseUTF(inputStream);
switch (fieldType) {
case 'F': // float (4 bytes)
case 'I': // int (4 bytes)
totalFieldSkip += 4;
break;
default:
throw new ParseException(String.format("Unexpected field type: %s", fieldType));
}
}
inputStream.skipBytes(totalFieldSkip);
// Not sure what these bytes are, just skip them
inputStream.skipBytes(4);
}
private static String parseStringObject(DataInputStream inputStream)
throws IOException, ParseException {
byte objectType = inputStream.readByte();
if (objectType != TC_STRING) {
throw new ParseException(String.format("Expected a string object, found: %d", objectType));
}
int length = inputStream.readUnsignedShort();
byte[] strBytes = new byte[length];
inputStream.readFully(strBytes);
return new String(strBytes, StandardCharsets.UTF_8);
}
private static String parseUTF(DataInputStream inputStream) throws IOException {
int length = inputStream.readUnsignedShort();
byte[] strBytes = new byte[length];
inputStream.readFully(strBytes);
return new String(strBytes, StandardCharsets.UTF_8);
}
private static class ParseException extends Exception {
public ParseException(String message) {
super(message);
}
}
}
}
@@ -46,7 +46,7 @@ public class FreeOtpPlusImporter extends DatabaseImporter {
entries.add(array.getJSONObject(i));
}
state = new FreeOtpImporter.State(entries);
state = new FreeOtpImporter.DecryptedStateV1(entries);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
@@ -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;
@@ -45,11 +45,11 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
public class AuthenticatorProImporter extends DatabaseImporter {
public class StratumImporter extends DatabaseImporter {
private static final String HEADER = "AUTHENTICATORPRO";
private static final String HEADER_LEGACY = "AuthenticatorPro";
private static final String PKG_NAME = "me.jmh.authenticatorpro";
private static final String PKG_DB_PATH = "files/proauth.db3";
private static final String PKG_NAME = "com.stratumauth.app";
private static final String PKG_DB_PATH = "databases/authenticator.db3";
private enum Algorithm {
SHA1,
@@ -57,7 +57,7 @@ public class AuthenticatorProImporter extends DatabaseImporter {
SHA512
}
public AuthenticatorProImporter(Context context) {
public StratumImporter(Context context) {
super(context);
}
@@ -169,7 +169,7 @@ public class AuthenticatorProImporter extends DatabaseImporter {
Argon2Task.Params params = getKeyDerivationParams(password);
Argon2Task task = new Argon2Task(context, key -> {
try {
AuthenticatorProImporter.JsonState state = decrypt(key);
StratumImporter.JsonState state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
@@ -244,7 +244,7 @@ public class AuthenticatorProImporter extends DatabaseImporter {
PBKDFTask.Params params = getKeyDerivationParams(password);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
AuthenticatorProImporter.JsonState state = decrypt(key);
StratumImporter.JsonState state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
@@ -4,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 -> {
@@ -4,17 +4,19 @@ import android.content.Context;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
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;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.base.Strings;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
@@ -60,7 +62,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));
}
@@ -172,9 +174,12 @@ public class TwoFASImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(obj.getString("secret"));
byte[] secret = GoogleAuthInfo.parseSecret(obj.getString("secret"));
JSONObject info = obj.getJSONObject("otp");
String issuer = info.optString("issuer");
String issuer = obj.optString("name");
if (Strings.isNullOrEmpty(issuer)) {
issuer = info.optString("issuer");
}
String name = info.optString("account");
int digits = info.optInt("digits", TotpInfo.DEFAULT_DIGITS);
String algorithm = info.optString("algorithm", TotpInfo.DEFAULT_ALGORITHM);
@@ -187,6 +192,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";
}
}
@@ -30,20 +30,6 @@ public class MotpInfo extends TotpInfo {
setPin(pin);
}
@Override
public String getOtp() {
if (_pin == null) {
throw new IllegalStateException("PIN must be set before generating an OTP");
}
try {
MOTP otp = MOTP.generateOTP(getSecret(), getAlgorithm(false), getDigits(), getPeriod(), getPin());
return otp.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getOtp(long time) {
if (_pin == null) {
@@ -20,11 +20,11 @@ public class SteamInfo extends TotpInfo {
}
@Override
public String getOtp() throws OtpInfoException {
public String getOtp(long time) throws OtpInfoException {
checkSecret();
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
return otp.toSteamString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
@@ -27,17 +27,12 @@ public class TotpInfo extends OtpInfo {
@Override
public String getOtp() throws OtpInfoException {
checkSecret();
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
return otp.toString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
return getOtp(System.currentTimeMillis() / 1000);
}
public String getOtp(long time) {
public String getOtp(long time) throws OtpInfoException {
checkSecret();
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
return otp.toString();
@@ -38,13 +38,13 @@ public class YandexInfo extends TotpInfo {
}
@Override
public String getOtp() {
public String getOtp(long time) {
if (_pin == null) {
throw new IllegalStateException("PIN must be set before generating an OTP");
}
try {
YAOTP otp = YAOTP.generateOTP(getSecret(), getPin(), getDigits(), getAlgorithm(true), getPeriod());
YAOTP otp = YAOTP.generateOTP(getSecret(), getPin(), getDigits(), getAlgorithm(true), getPeriod(), time);
return otp.toString();
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
throw new RuntimeException(e);
@@ -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;
@@ -22,6 +24,7 @@ public class LaunchAppTileService extends TileService {
}
}
@SuppressLint("StartActivityAndCollapseDeprecated")
@Override
public void onClick() {
super.onClick();
@@ -30,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;
@@ -22,6 +24,7 @@ public class LaunchScannerTileService extends TileService {
}
}
@SuppressLint("StartActivityAndCollapseDeprecated")
@Override
public void onClick() {
super.onClick();
@@ -31,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);
}
}
}
}
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.Nullable;
@@ -29,11 +28,7 @@ public class NotificationService extends Service {
@SuppressLint("LaunchActivityFromNotification")
public void serviceMethod() {
int flags = PendingIntent.FLAG_ONE_SHOT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
int flags = PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE;
Intent intent = new Intent(this, VaultLockReceiver.class);
intent.setAction(VaultLockReceiver.ACTION_LOCK_VAULT);
intent.setPackage(BuildConfig.APPLICATION_ID);
@@ -47,7 +42,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,16 @@ import android.widget.Toast;
import androidx.annotation.AttrRes;
import androidx.annotation.StringRes;
import androidx.core.view.LayoutInflaterCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.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.beemdevelopment.aegis.helpers.ViewHelper;
import com.google.android.material.color.MaterialColors;
public class AboutActivity extends AegisActivity {
@@ -40,7 +36,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;
@@ -48,6 +43,7 @@ public class AboutActivity extends AegisActivity {
setContentView(R.layout.activity_about);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -57,12 +53,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,9 +92,20 @@ 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);
});
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.about_scroll_view), (targetView, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
targetView.setPadding(
0,
0,
0,
insets.bottom
);
return WindowInsetsCompat.CONSUMED;
});
}
private static String getCurrentAppVersion() {
@@ -130,30 +140,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.graphics.drawable.Drawable;
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.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.appbar.AppBarLayout;
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,79 @@ 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;
private Drawable _appBarBackground;
private ActionModeStatusGuardHack() {
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;
}
AppBarLayout appBarLayout = findViewById(R.id.app_bar_layout);
if (appBarLayout != null && _appBarBackground == null) {
_appBarBackground = appBarLayout.getBackground();
}
if (fadeAnim == null || actionModeView == null || appBarLayout == null || _appBarBackground == null) {
return;
}
fadeAnim.cancel();
if (visibility == View.VISIBLE) {
actionModeView.setVisibility(visibility);
actionModeView.setAlpha(1f);
int color = MaterialColors.getColor(appBarLayout, com.google.android.material.R.attr.colorSurfaceContainer);
appBarLayout.setBackgroundColor(color);
} else {
actionModeView.setVisibility(visibility);
actionModeView.setAlpha(0f);
appBarLayout.setBackground(_appBarBackground);
}
}
}
/**
* 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,281 @@
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.helpers.ViewHelper;
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));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
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,14 +161,17 @@ public class AuthActivity extends AegisActivity {
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener());
task.execute(getLifecycle(), params);
_decryptButton.setEnabled(false);
});
biometricsButton.setOnClickListener(v -> {
if (_prefs.isPasswordReminderNeeded()) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(getString(R.string.password_reminder_dialog_title))
.setMessage(getString(R.string.password_reminder_dialog_message))
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
showBiometricPrompt();
})
@@ -183,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();
@@ -240,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;
}
@@ -287,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();
}
@@ -302,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());
@@ -345,6 +343,9 @@ public class AuthActivity extends AegisActivity {
finish(result.getKey(), result.isSlotRepaired());
} else {
_decryptButton.setEnabled(true);
_auditLogRepository.addVaultUnlockFailedPasswordEvent();
onInvalidPassword();
}
}
@@ -357,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();
}
}
@@ -15,13 +15,16 @@ 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;
@@ -34,13 +37,14 @@ import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.SafHelper;
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
import com.beemdevelopment.aegis.helpers.SimpleTextWatcher;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
@@ -53,53 +57,63 @@ 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;
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.Iterator;
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 String _pickedMimeType;
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;
@@ -109,21 +123,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;
}
_pickedMimeType = SafHelper.getMimeType(this, data.getData());
if (_pickedMimeType != null && _pickedMimeType.equals(IconType.SVG.toMimeType())) {
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
ImportFileTask task = new ImportFileTask(this, result -> {
if (result.getError() == null) {
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);
@@ -132,12 +169,13 @@ public class EditEntryActivity extends AegisActivity {
}
setContentView(R.layout.activity_edit_entry);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_groups = _vaultManager.getVault().getGroups();
ActionBar bar = getSupportActionBar();
if (bar != null) {
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setHomeAsUpIndicator(R.drawable.ic_outline_close_24);
bar.setDisplayHomeAsUpEnabled(true);
}
@@ -164,6 +202,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);
@@ -173,14 +213,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) {
@@ -209,22 +247,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());
@@ -262,8 +290,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);
@@ -271,11 +309,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);
@@ -327,28 +365,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) {
@@ -365,41 +482,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 * (long) AnimationsHelper.Scale.ANIMATOR.getValue(this));
fadeOut.setDuration((long) (220 * AnimationsHelper.Scale.ANIMATOR.getValue(this)));
_advancedSettingsHeader.startAnimation(fadeOut);
Animation fadeIn = new AlphaAnimation(0, 1);
fadeIn.setInterpolator(new AccelerateInterpolator());
fadeIn.setDuration(250 * (long) AnimationsHelper.Scale.ANIMATOR.getValue(this));
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) {
@@ -437,38 +533,33 @@ public class EditEntryActivity extends AegisActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
discardAndFinish();
break;
case R.id.action_save:
onSave();
break;
case R.id.action_delete:
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry), (dialog, which) -> {
deleteAndFinish(_origEntry);
});
break;
case R.id.action_edit_icon:
startIconSelection();
break;
case R.id.action_reset_usage_count:
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.action_reset_usage_count)
.setMessage(R.string.action_reset_usage_count_dialog)
.setPositiveButton(android.R.string.yes, (dialog, which) -> resetUsageCount())
.setNegativeButton(android.R.string.no, null)
.create());
break;
case R.id.action_default_icon:
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
discardAndFinish();
} else if (itemId == R.id.action_save) {
onSave();
} else if (itemId == R.id.action_delete) {
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry), (dialog, which) -> {
deleteAndFinish(_origEntry);
});
} else if (itemId == R.id.action_edit_icon) {
startIconSelection();
} else if (itemId == R.id.action_reset_usage_count) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this)
.setTitle(R.string.action_reset_usage_count)
.setMessage(R.string.action_reset_usage_count_dialog)
.setPositiveButton(android.R.string.yes, (dialog, which) -> resetUsageCount())
.setNegativeButton(android.R.string.no, null)
.create());
} else if (itemId == R.id.action_default_icon) {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
_selectedIcon = null;
_hasCustomIcon = false;
_hasChangedIcon = true;
default:
return super.onOptionsItemSelected(item);
_selectedIcon = null;
_hasCustomIcon = false;
_hasChangedIcon = true;
} else {
return super.onOptionsItemSelected(item);
}
return true;
@@ -483,7 +574,7 @@ public class EditEntryActivity extends AegisActivity {
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
_vaultManager.startActivityForResult(this, chooserIntent, PICK_IMAGE_REQUEST);
_vaultManager.fireIntentLauncher(this, chooserIntent, pickImageResultLauncher);
}
private void resetUsageCount() {
@@ -500,7 +591,7 @@ public class EditEntryActivity extends AegisActivity {
return;
}
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), new IconAdapter.Listener() {
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), true, new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
selectIcon(icon);
@@ -519,14 +610,7 @@ public class EditEntryActivity extends AegisActivity {
_hasCustomIcon = true;
_hasChangedIcon = true;
IconViewHelper.setLayerType(_iconView, icon.getIconType());
Glide.with(EditEntryActivity.this)
.asDrawable()
.load(icon.getFile())
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
GlideHelper.loadIcon(Glide.with(EditEntryActivity.this), icon, _iconView);
}
private void startEditingIcon(Uri data) {
@@ -596,6 +680,16 @@ public class EditEntryActivity extends AegisActivity {
saveAndFinish(entry, false);
}
private void setLastUsedTimestamp(long timestamp) {
String readableDate = getString(R.string.last_used_never);
if (timestamp != 0) {
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.getDefault());
readableDate = dateFormat.format(new Date(timestamp));
}
_textLastUsed.setText(String.format("%s: %s", getString(R.string.last_used), readableDate));
}
private void deleteAndFinish(VaultEntry entry) {
_vaultManager.getVault().removeEntry(entry);
saveAndFinish(entry, true);
@@ -612,29 +706,6 @@ public class EditEntryActivity extends AegisActivity {
}
}
@Override
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
String fileType = SafHelper.getMimeType(this, data.getData());
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
ImportFileTask task = new ImportFileTask(this, result -> {
if (result.getError() == null) {
CustomSvgIcon icon = new CustomSvgIcon(result.getFile());
selectIcon(icon);
} else {
Dialogs.showErrorDialog(this, R.string.reading_file_error, result.getError());
}
});
task.execute(getLifecycle(), params);
} else {
startEditingIcon(data.getData());
}
}
super.onActivityResult(requestCode, resultCode, data);
}
private int parsePeriod() throws ParseException {
try {
return Integer.parseInt(_textPeriodCounter.getText().toString());
@@ -726,23 +797,23 @@ 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);
IconType iconType = _pickedMimeType == null
? IconType.INVALID : IconType.fromMimeType(_pickedMimeType);
if (iconType == IconType.INVALID) {
iconType = bitmap.hasAlpha() ? IconType.PNG : IconType.JPEG;
}
icon = BitmapHelper.toVaultEntryIcon(bitmap, iconType);
} else {
byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
@@ -750,11 +821,12 @@ public class EditEntryActivity extends AegisActivity {
} catch (IOException e) {
throw new ParseException(e.getMessage());
}
entry.setIcon(iconBytes, _selectedIcon.getIconType());
icon = new VaultEntryIcon(iconBytes, _selectedIcon.getIconType());
}
entry.setIcon(icon);
} else {
entry.setIcon(null, IconType.INVALID);
entry.setIcon(null);
}
}
@@ -762,9 +834,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());
}
@@ -782,10 +855,92 @@ public class EditEntryActivity extends AegisActivity {
return false;
}
if (_isNew) {
for (VaultEntry existing : _vaultManager.getVault().getEntries()) {
if (entry.hasSameNameAndIssuer(existing)) {
showDuplicateBottomSheet(entry);
return false;
}
}
}
addAndFinish(entry);
return true;
}
private void showDuplicateBottomSheet(VaultEntry newEntry) {
BottomSheetDialog dialog = new BottomSheetDialog(this);
View view = getLayoutInflater().inflate(R.layout.dialog_duplicate_entry, null);
dialog.setContentView(view);
dialog.setCancelable(false);
View overwrite = view.findViewById(R.id.overwrite_entry);
View addSuffix = view.findViewById(R.id.create_new_entry);
View cancel = view.findViewById(R.id.cancel_save);
TextView suffixSubtext = view.findViewById(R.id.duplicate_suffix_subtitle);
String baseName = newEntry.getName();
Set<String> existingNames = new HashSet<>();
for (VaultEntry e : _vaultManager.getVault().getEntries()) {
if (e.getIssuer().equals(newEntry.getIssuer())) {
existingNames.add(e.getName());
}
}
int counter = 2;
String newName;
do {
newName = baseName + " #" + counter++;
} while (existingNames.contains(newName));
suffixSubtext.setText(getString(R.string.dialog_duplicate_entry_suffix_subtitle, newName));
overwrite.setOnClickListener(v -> {
List<VaultEntry> duplicates = new ArrayList<>();
for (VaultEntry existing : _vaultManager.getVault().getEntries()) {
if (existing.hasSameNameAndIssuer(newEntry)) {
duplicates.add(existing);
}
}
Resources res = getResources();
String message = res.getQuantityString(
R.plurals.dialog_duplicate_entry_overwrite_dialog_message,
duplicates.size(),
duplicates.size(),
newEntry.getIssuer(),
newEntry.getName()
);
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_duplicate_entry_overwrite_dialog_title)
.setMessage(message)
.setPositiveButton(R.string.action_delete, (d, which) -> {
for (VaultEntry dup : duplicates) {
_vaultManager.getVault().removeEntry(dup);
}
dialog.dismiss();
addAndFinish(newEntry);
})
.setNegativeButton(android.R.string.no, null)
.show();
});
String finalNewName = newName;
addSuffix.setOnClickListener(v -> {
newEntry.setName(finalNewName);
dialog.dismiss();
addAndFinish(newEntry);
});
cancel.setOnClickListener(v -> dialog.dismiss());
Dialogs.showSecureDialog(dialog);
}
private static void setViewEnabled(View view, boolean enabled) {
view.setEnabled(enabled);
@@ -7,24 +7,28 @@ import android.view.View;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.ItemTouchHelper;
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.helpers.ViewHelper;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
private GroupAdapter _adapter;
private HashSet<String> _removedGroups;
private RecyclerView _slotsView;
private HashSet<UUID> _removedGroups;
private RecyclerView _groupsView;
private View _emptyStateView;
private BackPressHandler _backPressHandler;
@@ -36,6 +40,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
}
setContentView(R.layout.activity_groups);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
@@ -43,22 +48,51 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
_backPressHandler = new BackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
_removedGroups = new HashSet<>();
if (savedInstanceState != null) {
List<String> groups = savedInstanceState.getStringArrayList("removedGroups");
_removedGroups = new HashSet<>(Objects.requireNonNull(groups));
} else {
_removedGroups = new HashSet<>();
List<String> removedGroups = savedInstanceState.getStringArrayList("removedGroups");
if (removedGroups != null) {
for (String uuid : removedGroups) {
_removedGroups.add(UUID.fromString(uuid));
}
}
}
_adapter = new GroupAdapter(this);
_slotsView= findViewById(R.id.list_slots);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
_slotsView.setLayoutManager(layoutManager);
_slotsView.setAdapter(_adapter);
_slotsView.setNestedScrollingEnabled(false);
ItemTouchHelper touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(
@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder) {
for (String group : _vaultManager.getVault().getGroups()) {
_adapter.addGroup(group);
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
int draggedItemIndex = viewHolder.getBindingAdapterPosition();
int targetIndex = target.getBindingAdapterPosition();
_adapter.onItemMove(draggedItemIndex, targetIndex);
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { }
});
_adapter = new GroupAdapter(this);
_groupsView = findViewById(R.id.list_groups);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
_groupsView.setLayoutManager(layoutManager);
_groupsView.setAdapter(_adapter);
_groupsView.setNestedScrollingEnabled(false);
touchHelper.attachToRecyclerView(_groupsView);
for (VaultGroup group : _vaultManager.getVault().getGroups()) {
if (!_removedGroups.contains(group.getUUID())) {
_adapter.addGroup(group);
}
}
_emptyStateView = findViewById(R.id.vEmptyList);
@@ -68,16 +102,37 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putStringArrayList("removedGroups", new ArrayList<>(_removedGroups));
ArrayList<String> removed = new ArrayList<>();
for (UUID uuid : _removedGroups) {
removed.add(uuid.toString());
}
outState.putStringArrayList("removedGroups", removed);
}
@Override
public void onRemoveGroup(String group) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
public void onEditGroup(VaultGroup group) {
Dialogs.TextInputListener onEditGroup = text -> {
String newGroupName = new String(text).trim();
if (!newGroupName.isEmpty()) {
VaultGroup newGroup = Cloner.clone(group);
newGroup.setName(newGroupName);
_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,17 +141,36 @@ 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();
}
_vaultManager.getVault().replaceGroups(_adapter.getGroups());
saveAndBackupVault();
finish();
}
@@ -119,15 +193,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 +209,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,5 +1,6 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.Menu;
@@ -10,22 +11,28 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.tasks.IconOptimizationTask;
import com.beemdevelopment.aegis.ui.tasks.RootShellTask;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
@@ -37,14 +44,20 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class ImportEntriesActivity extends AegisActivity {
private View _view;
private Menu _menu;
private RecyclerView _entriesView;
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
private UUIDMap<VaultGroup> _importedGroups;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -53,16 +66,17 @@ public class ImportEntriesActivity extends AegisActivity {
}
setContentView(R.layout.activity_import_entries);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_view = findViewById(R.id.importEntriesRootView);
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setHomeAsUpIndicator(R.drawable.ic_outline_close_24);
bar.setDisplayHomeAsUpEnabled(true);
_adapter = new ImportEntriesAdapter();
RecyclerView entriesView = findViewById(R.id.list_entries);
entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
_entriesView = findViewById(R.id.list_entries);
_entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
@@ -71,9 +85,9 @@ public class ImportEntriesActivity extends AegisActivity {
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
entriesView.setLayoutManager(layoutManager);
entriesView.setAdapter(_adapter);
entriesView.setNestedScrollingEnabled(false);
_entriesView.setLayoutManager(layoutManager);
_entriesView.setAdapter(_adapter);
_entriesView.setNestedScrollingEnabled(false);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> {
@@ -96,10 +110,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);
})
@@ -164,7 +179,7 @@ public class ImportEntriesActivity extends AegisActivity {
state.decrypt(this, new DatabaseImporter.DecryptListener() {
@Override
public void onStateDecrypted(DatabaseImporter.State state) {
importDatabase(state);
processDecryptedImporterState(state);
}
@Override
@@ -179,7 +194,7 @@ public class ImportEntriesActivity extends AegisActivity {
}
});
} else {
importDatabase(state);
processDecryptedImporterState(state);
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
@@ -187,8 +202,7 @@ public class ImportEntriesActivity extends AegisActivity {
}
}
private void importDatabase(DatabaseImporter.State state) {
List<ImportEntry> importEntries = new ArrayList<>();
private void processDecryptedImporterState(DatabaseImporter.State state) {
DatabaseImporter.Result result;
try {
result = state.convert();
@@ -198,17 +212,40 @@ public class ImportEntriesActivity extends AegisActivity {
return;
}
UUIDMap<VaultEntry> entries = result.getEntries();
for (VaultEntry entry : entries.getValues()) {
Map<UUID, VaultEntryIcon> icons = result.getEntries().getValues().stream()
.filter(e -> e.getIcon() != null
&& !e.getIcon().getType().equals(IconType.SVG)
&& !BitmapHelper.isVaultEntryIconOptimized(e.getIcon()))
.collect(Collectors.toMap(VaultEntry::getUUID, VaultEntry::getIcon));
if (!icons.isEmpty()) {
IconOptimizationTask task = new IconOptimizationTask(this, newIcons -> {
for (Map.Entry<UUID, VaultEntryIcon> mapEntry : newIcons.entrySet()) {
VaultEntry entry = result.getEntries().getByUUID(mapEntry.getKey());
entry.setIcon(mapEntry.getValue());
}
processImporterResult(result);
});
task.execute(getLifecycle(), icons);
} else {
processImporterResult(result);
}
}
private void processImporterResult(DatabaseImporter.Result result) {
List<ImportEntry> importEntries = new ArrayList<>();
for (VaultEntry entry : result.getEntries().getValues()) {
ImportEntry importEntry = new ImportEntry(entry);
_adapter.addEntry(importEntry);
importEntries.add(importEntry);
}
_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);
@@ -225,10 +262,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();
@@ -244,8 +314,30 @@ 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();
}
}
}
@@ -267,6 +359,31 @@ public class ImportEntriesActivity extends AegisActivity {
_adapter.setCheckboxStates(duplicateEntries, false);
Snackbar snackbar = Snackbar.make(_view, getResources().getQuantityString(R.plurals.import_duplicate_toast, duplicateEntries.size(), duplicateEntries.size()), Snackbar.LENGTH_INDEFINITE);
snackbar.addCallback(new Snackbar.Callback() {
@Override
public void onShown(Snackbar sb) {
int snackbarHeight = sb.getView().getHeight();
_entriesView.setPadding(
_entriesView.getPaddingLeft(),
_entriesView.getPaddingTop(),
_entriesView.getPaddingRight(),
_entriesView.getPaddingBottom() + snackbarHeight * 2
);
}
@Override
public void onDismissed(Snackbar sb, int event) {
int snackbarHeight = sb.getView().getHeight();
_entriesView.setPadding(
_entriesView.getPaddingLeft(),
_entriesView.getPaddingTop(),
_entriesView.getPaddingRight(),
_entriesView.getPaddingBottom() - snackbarHeight * 2
);
}
});
snackbar.setAction(R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -285,18 +402,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();
}
}
File diff suppressed because it is too large Load Diff
@@ -10,12 +10,15 @@ 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;
import com.beemdevelopment.aegis.helpers.ViewHelper;
public class PreferencesActivity extends AegisActivity implements
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
private Fragment _fragment;
private CharSequence _prefTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -25,6 +28,7 @@ public class PreferencesActivity extends AegisActivity implements
}
setContentView(R.layout.activity_preferences);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
getSupportFragmentManager()
.registerFragmentLifecycleCallbacks(new FragmentResumeListener(), true);
@@ -48,27 +52,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 +83,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 +115,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);
}
}
}
@@ -18,11 +18,11 @@ import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.zxing.Result;
@@ -57,6 +57,7 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
}
setContentView(R.layout.activity_scanner);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_entries = new ArrayList<>();
_lenses = new ArrayList<>();
@@ -95,11 +96,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 +138,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;
}
}
@@ -10,15 +10,17 @@ import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.util.TypedValue;
import android.provider.Settings;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.ColorInt;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
@@ -27,6 +29,9 @@ import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.Transferable;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.imageview.ShapeableImageView;
import com.google.zxing.WriterException;
import java.util.ArrayList;
@@ -34,7 +39,7 @@ import java.util.List;
public class TransferEntriesActivity extends AegisActivity {
private List<Transferable> _authInfos;
private ImageView _qrImage;
private ShapeableImageView _qrImage;
private TextView _description;
private TextView _issuer;
private TextView _accountName;
@@ -43,6 +48,8 @@ public class TransferEntriesActivity extends AegisActivity {
private Button _previousButton;
private Button _copyButton;
private int _currentEntryCount = 1;
private float _deviceBrightness;
private boolean _isMaxBrightnessSet = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -52,6 +59,7 @@ public class TransferEntriesActivity extends AegisActivity {
}
setContentView(R.layout.activity_share_entry);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_qrImage = findViewById(R.id.ivQrCode);
_description = findViewById(R.id.tvDescription);
@@ -62,7 +70,7 @@ public class TransferEntriesActivity extends AegisActivity {
_previousButton = findViewById(R.id.btnPrevious);
_copyButton = findViewById(R.id.btnCopyClipboard);
if (getSupportActionBar() != null){
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
@@ -88,7 +96,7 @@ public class TransferEntriesActivity extends AegisActivity {
});
_previousButton.setOnClickListener(v -> {
if (_currentEntryCount > 1 ) {
if (_currentEntryCount > 1) {
_nextButton.setText(R.string.next);
_currentEntryCount--;
generateQR();
@@ -116,14 +124,59 @@ public class TransferEntriesActivity extends AegisActivity {
if (clipboard != null) {
clipboard.setPrimaryClip(clip);
}
Toast.makeText(this,R.string.uri_copied_to_clipboard, Toast.LENGTH_SHORT).show();
Toast.makeText(this, R.string.uri_copied_to_clipboard, Toast.LENGTH_SHORT).show();
} catch (GoogleAuthInfoException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_copy_uri_to_clipboard, e);
}
});
generateQR();
// Calculate sensible dimensions for the QR code depending on whether we're in landscape
_qrImage.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ConstraintLayout layout = findViewById(R.id.layoutShareEntry);
if (layout.getWidth() > layout.getHeight()) {
int squareSize = (int) (0.5 * layout.getHeight());
ViewGroup.LayoutParams params = _qrImage.getLayoutParams();
params.width = squareSize;
params.height = squareSize;
_qrImage.setLayoutParams(params);
}
generateQR();
_qrImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
_deviceBrightness = getSystemBrightness();
_qrImage.setOnClickListener(v -> {
if (!_isMaxBrightnessSet) {
setBrightness(1f);
_isMaxBrightnessSet = true;
} else {
setBrightness(_deviceBrightness);
_isMaxBrightnessSet = false;
}
});
}
private float getSystemBrightness() {
int brightness = 0;
try {
brightness = Settings.System.getInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS);
} catch (Settings.SettingNotFoundException e) {
e.printStackTrace();
}
return brightness / 255f;
}
private void setBrightness(float brightnessAmount) {
WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.screenBrightness = brightnessAmount;
getWindow().setAttributes(attrs);
}
@Override
@@ -151,16 +204,13 @@ public class TransferEntriesActivity extends AegisActivity {
_entriesCount.setText(getResources().getQuantityString(R.plurals.qr_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
@ColorInt int backgroundColor = Color.WHITE;
if (getConfiguredTheme() == Theme.LIGHT) {
TypedValue typedValue = new TypedValue();
getTheme().resolveAttribute(R.attr.background, typedValue, true);
backgroundColor = typedValue.data;
}
int backgroundColor = _themeHelper.getConfiguredTheme() == Theme.LIGHT
? MaterialColors.getColor(_qrImage, com.google.android.material.R.attr.colorSurfaceContainer)
: Color.WHITE;
Bitmap bitmap;
try {
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), 512, 512, backgroundColor);
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), _qrImage.getWidth(), _qrImage.getWidth(), backgroundColor);
} catch (WriterException | GoogleAuthInfoException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
return;
@@ -18,19 +18,19 @@ import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
import com.beemdevelopment.aegis.R;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
public class DropdownCheckBoxes<T> extends AppCompatAutoCompleteTextView {
private @PluralsRes int _selectedCountPlural = R.plurals.dropdown_checkboxes_default_count;
private boolean _allowFiltering = false;
private final List<String> _items = new ArrayList<>();
private List<String> _visibleItems = new ArrayList<>();
private final Set<String> _checkedItems = new TreeSet<>();
private final List<T> _items = new ArrayList<>();
private List<T> _visibleItems = new ArrayList<>();
private final Set<T> _checkedItems = new HashSet<>();
private CheckboxAdapter _adapter;
@@ -70,7 +70,15 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
}
}
public void addItems(List<String> items, boolean startChecked) {
/**
* Add parameterized items to be displayed as a checkbox in the dropdown view
* the label for the checkbox is determined by the toString() method of the items
* you add.
*
* @param items a list of the items you want to show in the dropdown
* @param startChecked whether the checkbox should be checked initially
*/
public void addItems(List<T> items, boolean startChecked) {
_items.addAll(items);
_visibleItems.addAll(items);
@@ -97,7 +105,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
_selectedCountPlural = resId;
}
public Set<String> getCheckedItems() {
public Set<T> getCheckedItems() {
return _checkedItems;
}
@@ -109,7 +117,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
}
@Override
public String getItem(int i) {
public T getItem(int i) {
return _visibleItems.get(i);
}
@@ -124,19 +132,18 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.dropdown_checkbox, viewGroup, false);
}
String item = _visibleItems.get(i);
T item = _visibleItems.get(i);
CheckBox checkBox = convertView.findViewById(R.id.checkbox_in_dropdown);
checkBox.setText(item);
checkBox.setText(item.toString());
checkBox.setTag(item);
checkBox.setChecked(_checkedItems.contains(item));
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
String label = buttonView.getText().toString();
if (isChecked) {
_checkedItems.add(label);
_checkedItems.add((T) buttonView.getTag());
} else {
_checkedItems.remove(label);
_checkedItems.remove((T) buttonView.getTag());
}
updateCheckedItemsCountText();
@@ -153,9 +160,9 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
FilterResults results = new FilterResults();
results.values = (query == null || query.toString().isEmpty())
? _items
: _items.stream().filter(str -> {
: _items.stream().filter(item -> {
String q = query.toString().toLowerCase();
String strLower = str.toLowerCase();
String strLower = item.toString().toLowerCase();
return strLower.contains(q);
})
@@ -166,7 +173,7 @@ public class DropdownCheckBoxes extends AppCompatAutoCompleteTextView {
@Override
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
_visibleItems = (List<String>) filterResults.values;
_visibleItems = (List<T>) filterResults.values;
notifyDataSetChanged();
}
};
@@ -5,7 +5,7 @@ import android.content.Context;
import com.beemdevelopment.aegis.R;
public class ChangelogDialog extends SimpleWebViewDialog {
private ChangelogDialog() {
public ChangelogDialog() {
super(R.string.changelog);
}
@@ -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;
@@ -21,13 +19,17 @@ import android.widget.EditText;
import android.widget.ListView;
import android.widget.NumberPicker;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.ComponentActivity;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
@@ -39,10 +41,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 +87,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 +109,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 +126,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 +141,7 @@ public class Dialogs {
}
});
AlertDialog dialog = new AlertDialog.Builder(activity)
AlertDialog dialog = new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.set_password)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
@@ -179,13 +183,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 +191,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 +209,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 +239,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 +256,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 +276,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 +310,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 +321,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 +383,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 +419,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 +460,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 +491,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 +530,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,12 +551,13 @@ 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)
.setPositiveButton(context.getString(R.string.import_partial_export_anyway, entries), (dialog, which) -> {
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(context.getResources().getQuantityString(R.plurals.import_partial_export_anyway, entries, entries), (dialog, which) -> {
dismissHandler.onClick(dialog, which);
})
.setNegativeButton(android.R.string.cancel, null);
@@ -562,6 +580,54 @@ public class Dialogs {
showSecureDialog(dialog);
}
public static void showBackupsVersioningStrategy(Context context, BackupsVersioningStrategy currentStrategy, BackupsVersioningStrategyListener listener) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_backups_versioning_strategy, null);
RadioGroup radioGroup = view.findViewById(R.id.radio_group);
RadioButton keepXVersionsButton = view.findViewById(R.id.keep_x_versions_button);
RadioButton singleBackupButton = view.findViewById(R.id.single_backup_button);
TextView warningText = view.findViewById(R.id.warning_text);
CheckBox riskAccept = view.findViewById(R.id.risk_accept);
final AtomicReference<Button> positiveButtonRef = new AtomicReference<>();
radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
Button positiveButton = positiveButtonRef.get();
if (positiveButton != null) {
positiveButton.setEnabled(checkedId == keepXVersionsButton.getId());
}
int visibility = checkedId == singleBackupButton.getId() ? View.VISIBLE : View.GONE;
warningText.setVisibility(visibility);
riskAccept.setVisibility(visibility);
});
riskAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
Button positiveButton = positiveButtonRef.get();
if (positiveButton != null) {
positiveButton.setEnabled(isChecked);
}
});
AlertDialog alertDialog = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.pref_backups_versioning_strategy_dialog_title)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int checkedId = radioGroup.getCheckedRadioButtonId();
if (checkedId == keepXVersionsButton.getId()) {
listener.onStrategySelectionResult(BackupsVersioningStrategy.MULTIPLE_BACKUPS);
} else if (checkedId == singleBackupButton.getId()) {
listener.onStrategySelectionResult(BackupsVersioningStrategy.SINGLE_BACKUP);
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
alertDialog.setOnShowListener(dialog -> {
Button positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
positiveButtonRef.set(positiveButton);
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
radioGroup.check(keepXVersionsButton.getId());
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
radioGroup.check(singleBackupButton.getId());
}
});
showSecureDialog(alertDialog);
}
private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) {
if (isDirect) {
view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName()));
@@ -590,4 +656,8 @@ public class Dialogs {
public interface ImporterListener {
void onImporterSelectionResult(DatabaseImporter.Definition definition);
}
public interface BackupsVersioningStrategyListener {
void onStrategySelectionResult(BackupsVersioningStrategy strategy);
}
}
@@ -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")
@@ -8,11 +8,17 @@ import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import com.beemdevelopment.aegis.AccountNamePosition;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.ui.GroupManagerActivity;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.google.android.material.color.DynamicColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.Arrays;
import java.util.List;
public class AppearancePreferencesFragment extends PreferencesFragment {
private Preference _groupsPreference;
@@ -21,7 +27,6 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_appearance);
_groupsPreference = requirePreference("pref_groups");
@@ -33,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())
@@ -48,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();
@@ -56,7 +61,6 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
dialog.dismiss();
getResult().putExtra("needsRecreate", true);
requireActivity().recreate();
})
.setNegativeButton(android.R.string.cancel, null)
@@ -65,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();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
String[] langs = getResources().getStringArray(R.array.pref_lang_values);
String[] langNames = getResources().getStringArray(R.array.pref_lang_entries);
List<String> langList = Arrays.asList(langs);
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 {
@@ -83,14 +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);
overrideAccountNamePosition(ViewMode.fromInteger(i) == ViewMode.TILES);
refreshAccountNamePositionText();
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
@@ -99,15 +125,27 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
return true;
});
Preference codeDigitGroupingPreference = requirePreference("pref_code_group_size_string");
codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
Preference showExpirationStatePreference = requirePreference("pref_expiration_state");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
showExpirationStatePreference.setSummary(getString(R.string.pref_expiration_state_fallback));
}
Preference onlyShowNecessaryAccountNames = requirePreference("pref_shared_issuer_account_name");
onlyShowNecessaryAccountNames.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", 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.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;
});
@@ -117,13 +155,13 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
_currentAccountNamePositionPreference.setOnPreferenceClickListener(preference -> {
int currentAccountNamePosition1 = _prefs.getAccountNamePosition().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.choose_account_name_position))
.setSingleChoiceItems(R.array.account_name_position_titles, currentAccountNamePosition1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_prefs.setAccountNamePosition(AccountNamePosition.fromInteger(i));
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[i]));
getResult().putExtra("needsRefresh", true);
refreshAccountNamePositionText();
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
@@ -132,21 +170,15 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
return true;
});
Preference showIconsPreference = requirePreference("pref_show_icons");
showIconsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
overrideAccountNamePosition(_prefs.getCurrentViewMode() == ViewMode.TILES);
refreshAccountNamePositionText();
}
private void overrideAccountNamePosition(boolean override) {
private void refreshAccountNamePositionText() {
boolean override = (_prefs.getCurrentViewMode() == ViewMode.TILES && _prefs.getAccountNamePosition() == AccountNamePosition.END);
if (override) {
_currentAccountNamePositionPreference.setEnabled(false);
_currentAccountNamePositionPreference.setSummary(getString(R.string.pref_account_name_position_summary_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.setEnabled(true);
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[_prefs.getAccountNamePosition().ordinal()]));
}
}
@@ -0,0 +1,111 @@
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.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.LinearLayoutManager;
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);
ViewCompat.setOnApplyWindowInsetsListener(_auditLogRecyclerView, (targetView, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
targetView.setPadding(
0,
0,
0,
insets.bottom
);
return WindowInsetsCompat.CONSUMED;
});
entries.observe(getViewLifecycleOwner(), entries1 -> {
_noAuditLogsView.setVisibility(entries1.isEmpty() ? View.VISIBLE : View.GONE);
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,19 +11,25 @@ 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;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.color.MaterialColors;
public class BackupsPreferencesFragment extends PreferencesFragment {
private SwitchPreferenceCompat _androidBackupsPreference;
private SwitchPreferenceCompat _backupsPreference;
private SwitchPreferenceCompat _backupReminderPreference;
private Preference _versioningStrategyPreference;
private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference;
@@ -32,6 +38,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();
@@ -40,7 +55,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");
@@ -64,7 +78,13 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsPreference = requirePreference("pref_backups");
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if ((boolean) newValue) {
selectBackupsLocation();
Dialogs.showBackupsVersioningStrategy(requireContext(), BackupsVersioningStrategy.MULTIPLE_BACKUPS, strategy -> {
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
});
} else {
_prefs.setIsBackupsEnabled(false);
updateBackupPreference();
@@ -89,6 +109,24 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return false;
});
_versioningStrategyPreference = requirePreference("pref_versioning_strategy");
updateBackupsVersioningStrategySummary();
_versioningStrategyPreference.setOnPreferenceClickListener(preference -> {
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
Dialogs.showBackupsVersioningStrategy(requireContext(), currentStrategy, strategy -> {
if (strategy == currentStrategy) {
return;
}
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
});
return true;
});
_androidBackupsPreference = requirePreference("pref_android_backups");
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_prefs.setIsAndroidBackupsEnabled((boolean) newValue);
@@ -99,13 +137,15 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return false;
});
Uri backupLocation = _prefs.getBackupsLocation();
_backupsLocationPreference = requirePreference("pref_backups_location");
if (backupLocation != null) {
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
}
updateBackupsLocationSummary();
_backupsLocationPreference.setOnPreferenceClickListener(preference -> {
selectBackupsLocation();
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
return false;
});
@@ -119,12 +159,11 @@ 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;
});
@@ -137,13 +176,6 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data != null && requestCode == CODE_BACKUPS) {
onSelectBackupsLocationResult(resultCode, data);
}
}
private void onSelectBackupsLocationResult(int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
@@ -155,9 +187,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_prefs.setBackupsLocation(uri);
_prefs.setIsBackupsEnabled(true);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference();
scheduleBackup();
updateBackupsVersioningStrategySummary();
updateBackupsLocationSummary();
}
private void updateBackupPreference() {
@@ -171,9 +204,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsPreference.setChecked(backupEnabled);
_backupsPreference.setEnabled(encrypted);
_backupReminderPreference.setChecked(backupReminderEnabled);
_versioningStrategyPreference.setVisible(backupEnabled);
_backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled && _prefs.getBackupVersioningStrategy() != BackupsVersioningStrategy.SINGLE_BACKUP);
if (backupEnabled) {
updateBackupStatus(_builtinBackupStatusPreference, _prefs.getBuiltInBackupResult());
}
@@ -191,9 +225,9 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
// TODO: Find out why setting the tint of the icon doesn't work
if (backupFailed) {
pref.setIcon(R.drawable.ic_info_outline_black_24dp);
pref.setIcon(R.drawable.ic_outline_error_24);
} else if (res != null) {
pref.setIcon(R.drawable.ic_check_black_24dp);
pref.setIcon(R.drawable.ic_outline_check_24);
} else {
pref.setIcon(null);
}
@@ -201,24 +235,31 @@ 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;
}
private void createBackupFile() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/json")
.putExtra(Intent.EXTRA_TITLE, VaultBackupManager.FILENAME_SINGLE);
_vaultManager.fireIntentLauncher(this, intent, backupsResultLauncher);
}
private void selectBackupsLocation() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
@@ -226,7 +267,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() {
@@ -238,4 +279,38 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
Dialogs.showErrorDialog(requireContext(), R.string.backup_error, e);
}
}
private void updateBackupsVersioningStrategySummary() {
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_keep_x_versions);
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_single_backup);
}
}
private void updateBackupsLocationSummary() {
Uri backupsLocation = _prefs.getBackupsLocation();
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
String text;
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
text = getString(R.string.pref_backups_location_summary);
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
text = getString(R.string.pref_backup_location_summary);
} else {
return;
}
String summary = String.format("%s: %s", text, Uri.decode(backupsLocation.toString()));
_backupsLocationPreference.setSummary(summary);
}
private void updateBackupsVersionsSummary() {
int count = _prefs.getBackupsVersionCount();
if (count == Preferences.BACKUPS_VERSIONS_INFINITE) {
_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,35 +1,80 @@
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 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 AlertDialog.Builder(requireContext())
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.choose_copy_behavior))
.setSingleChoiceItems(R.array.copy_behavior_titles, currentCopyBehavior1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_prefs.setCopyBehavior(CopyBehavior.fromInteger(i));
copyBehaviorPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.copy_behavior_titles)[i]));
getResult().putExtra("needsRefresh", true);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
@@ -38,18 +83,31 @@ public class BehaviorPreferencesFragment extends PreferencesFragment {
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());
}
}

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