Compare commits

...

135 commits
v3.2 ... master

Author SHA1 Message Date
Michael Schättgen
9d32a426f9 Update translations from Crowdin 2025-06-25 03:06:32 +02:00
Michael Schättgen
36bd8f945a Release v3.4 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 2025-05-29 13:48:10 +02:00
Alexander Bakker
af4e3d9566 Update translations from Crowdin 2025-05-29 13:44:23 +02:00
Alexander Bakker
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 Bakker
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 Bakker
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ättgen
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ättgen
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ättgen
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ättgen
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 Bakker
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
mimi89999
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 Bakker
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ättgen
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ättgen
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 Bakker
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 Bakker
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 Bakker
ce29d120a9
Merge pull request #1600 from michaelschattgen/fix/import-entries-padding
Fix obstructing snackbar padding
2025-01-24 16:15:33 +01:00
Alexander Bakker
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 Bakker
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 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ättgen
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ättgen
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 2025-01-02 22:08:00 +01:00
Alexander Bakker
1fb36b0578 Update translations from Crowdin 2025-01-02 22:01:35 +01:00
Alexander Bakker
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ättgen
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 Bakker
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 2024-12-02 22:35:13 +01:00
Alexander Bakker
170f626c9e Update translations from Crowdin 2024-12-02 22:29:48 +01:00
Alexander Bakker
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ättgen
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 Bakker
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 Bakker
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 Bakker
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 Bakker
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 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 2024-11-24 16:42:26 +01:00
Alexander Bakker
79ba822ccf Update translations from Crowdin 2024-11-24 16:40:17 +01:00
Alexander Bakker
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ättgen
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ättgen
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 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 Bakker
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ättgen
eb7b8881a0 Fix progress bar colors when using dynamic 2024-11-15 14:14:11 +01:00
Alexander Bakker
b70654152d
Merge pull request #1439 from r3dh3ck/feature/single_backup
Single backup
2024-11-15 13:53:54 +01:00
r3dh3ck
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ättgen
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ättgen
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ättgen
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ättgen
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ättgen
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 Bakker
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ättgen
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 Bakker
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 Bakker
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 Bakker
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ättgen
7ea2f5c4a5 Change group filter to a single selection 2024-09-24 21:47:40 +02:00
Alexander Bakker
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ättgen
4a9f189897 Add ability to easily assign groups 2024-09-24 21:24:51 +02:00
Alexander Bakker
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ättgen
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 Bakker
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 Bakker
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 Bakker
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 Bakker
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ättgen
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 Bakker
df30e42318
Merge pull request #1481 from michaelschattgen/feature/reorder-groups
Add ability to reorder groups
2024-09-12 22:51:15 +02:00
Alexander Bakker
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 Bakker
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
221 changed files with 8484 additions and 2250 deletions

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%"

View file

@ -35,7 +35,7 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Tests
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d
with:
api-level: 31
arch: x86_64

View file

@ -14,6 +14,7 @@ jobs:
actions: read
contents: read
security-events: write
if: github.event_name != 'schedule' || github.repository == 'beemdevelopment/Aegis'
steps:
- name: Checkout
uses: actions/checkout@v4

View file

@ -13,19 +13,13 @@ jobs:
- uses: actions/checkout@v4
- name: Install crowdin-cli
run: |
wget https://github.com/crowdin/crowdin-cli/releases/download/3.7.2/crowdin-cli.zip
echo "ee9f838b819ccedc33c9b2537055e5ba7d7934561b24df1e1a6274cbd6e27f2d crowdin-cli.zip" | sha256sum -c
wget https://github.com/crowdin/crowdin-cli/releases/download/4.6.1/crowdin-cli.zip
echo "7afd70de3a747ac631a5bad7866008163ae1d50c4606b5773f0b90a5481ffde2 crowdin-cli.zip" | sha256sum -c
unzip crowdin-cli.zip -d crowdin-cli
- name: Upload to Crowdin
env:
CROWDIN_TOKEN: "${{ secrets.CROWDIN_TOKEN }}"
CROWDIN_PERSONAL_TOKEN: "${{ secrets.CROWDIN_TOKEN }}"
run: |
java -jar ./crowdin-cli/3.7.2/crowdin-cli.jar upload sources \
java -jar ./crowdin-cli/4.6.1/crowdin-cli.jar upload sources \
--no-progress \
--token "$CROWDIN_TOKEN" \
--project-id 372633 \
--base-path app/src/main \
--source res/values/strings.xml \
--translation "res/values-%android_code%/%original_file_name%" \
--dest strings.xml \
--branch master

8
FAQ.md
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

View file

@ -123,24 +123,33 @@ documentation](docs/iconpacks.md).
Unofficial monochrome-styled 2FA icons.
[<img width=500 alt="aegis-icons preview"
src="https://raw.githubusercontent.com/aegis-icons/aegis-icons/master/showcase.png">](https://github.com/aegis-icons/aegis-icons)
src="metadata/en-US/images/iconPacks/aegis-icons.png">](https://github.com/aegis-icons/aegis-icons)
- [delta-aegis-icons](https://github.com/Delta-Icons/aegis-icons)
Delta version of the unofficial monochrome-styled 2FA icon pack aegis-icons.
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons) *
[<img width=500 alt="delta-icons preview"
src="metadata/en-US/images/iconPacks/delta-icons.png">](https://github.com/Delta-Icons/aegis-icons)
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons) \*
This project periodically generates an icon pack for Aegis based on [Simple
Icons](https://simpleicons.org/).
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) *
[<img width=500 alt="aegis-simple-icons preview"
src="metadata/en-US/images/iconPacks/aegis-simple-icons.png">](https://github.com/alexbakker/aegis-simple-icons)
- [aegis-simple-icons-outlined](https://github.com/michaelschattgen/aegis-simple-icons-outlined) \*
This is a variant on the aegis-simple-icons pack where the icons contain no solid background and just the outlines are being used.
[<img width=500 alt="aegis-simple-icons-outlined preview"
src="metadata/en-US/images/iconPacks/aegis-simple-icons-outlined.png">](https://github.com/michaelschattgen/aegis-simple-icons-outlined)
\* The icons are automatically generated, so
not all of them are as high quality as the ones you'll find in
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
not all of them are as high quality as the ones you'll find in
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
## Contributing
@ -158,5 +167,6 @@ This project is licensed under the GNU General Public License v3.0. See the
A couple of libraries vendored in Aegis' repository are licensed under a
different license:
- [TextDrawable](app/src/main/java/com/amulyakhare/textdrawable)
- [TrustedIntents](app/src/main/java/info/guardianproject/trustedintents)

View file

@ -20,16 +20,16 @@ def fileProviderAuthority = "${packageName}.fileprovider"
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
android {
compileSdk 34
compileSdk 35
namespace packageName
defaultConfig {
applicationId "${packageName}"
minSdkVersion 21
targetSdkVersion 34
versionCode 71
versionName "3.2"
minSdkVersion 23
targetSdkVersion 35
versionCode 79
versionName "3.4"
multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
@ -93,22 +93,32 @@ android {
}
}
// Required to make the APK reproducible
aaptOptions {
cruncherEnabled = false
}
defaultConfig {
vectorDrawables.generatedDensities = []
}
packagingOptions {
// R8 doesn't remove these resources, so exclude them manually. This reduces APK size by 4MB.
resources {
excludes += ['/org/bouncycastle/pqc/**/*.properties']
excludes += [
'/org/bouncycastle/pqc/**/*.properties',
'META-INF/versions/9/OSGI-INF/MANIFEST.MF'
]
}
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true
}
lint {
abortOnError true
checkDependencies true
disable 'MissingQuantity', 'MissingTranslation'
}
buildFeatures {
buildConfig true
@ -142,32 +152,32 @@ aboutLibraries {
}
dependencies {
def cameraxVersion = '1.3.4'
def cameraxVersion = '1.4.2'
def glideVersion = '4.16.0'
def guavaVersion = '33.2.1'
def hiltVersion = '2.52'
def guavaVersion = '33.4.8'
def hiltVersion = '2.56.2'
def junitVersion = '4.13.2'
def libsuVersion = '6.0.0'
def roomVersion = "2.6.1"
def roomVersion = '2.7.1'
annotationProcessor 'androidx.annotation:annotation:1.8.2'
annotationProcessor 'androidx.annotation:annotation:1.9.1'
annotationProcessor "androidx.room:room-compiler:$roomVersion"
annotationProcessor "com.google.dagger:hilt-compiler:$hiltVersion"
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.activity:activity:1.9.1'
implementation 'androidx.activity:activity:1.10.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.camera:camera-camera2:$cameraxVersion"
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
implementation "androidx.camera:camera-view:$cameraxVersion"
implementation 'androidx.core:core:1.13.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.lifecycle:lifecycle-process:2.8.4'
implementation 'androidx.core:core:1.16.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.documentfile:documentfile:1.1.0'
implementation 'androidx.lifecycle:lifecycle-process:2.9.0'
implementation "androidx.preference:preference:1.2.1"
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation "androidx.room:room-runtime:$roomVersion"
implementation 'androidx.viewpager2:viewpager2:1.1.0'
implementation 'com.caverock:androidsvg-aar:1.4'
@ -182,34 +192,34 @@ dependencies {
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
implementation "com.google.guava:guava:${guavaVersion}-android"
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.google.protobuf:protobuf-javalite:4.27.3'
implementation 'com.google.protobuf:protobuf-javalite:4.31.0'
implementation 'com.google.zxing:core:3.5.3'
implementation('com.mikepenz:aboutlibraries:11.2.2') {
implementation('com.mikepenz:aboutlibraries:11.2.3') {
exclude group: 'com.mikepenz', module: 'aboutlibraries-core'
}
implementation 'com.mikepenz:aboutlibraries-core-android:11.2.2'
implementation 'com.mikepenz:aboutlibraries-core-android:11.2.3'
implementation 'com.nulab-inc:zxcvbn:1.9.0'
implementation 'net.lingala.zip4j:zip4j:2.11.5'
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
implementation 'org.bouncycastle:bcprov-jdk18on:1.80'
implementation 'org.simpleflatmapper:sfm-csv:8.2.3'
androidTestAnnotationProcessor "com.google.dagger:hilt-android-compiler:$hiltVersion"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hiltVersion"
androidTestImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test:runner:1.6.1'
androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
androidTestImplementation "junit:junit:${junitVersion}"
androidTestUtil 'androidx.test:orchestrator:1.5.0'
androidTestUtil 'androidx.test:orchestrator:1.5.1'
testImplementation 'androidx.test:core:1.6.1'
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
testImplementation "junit:junit:${junitVersion}"
testImplementation 'org.json:json:20240303'
testImplementation 'org.robolectric:robolectric:4.13'
testImplementation 'org.json:json:20250517'
testImplementation 'org.robolectric:robolectric:4.14.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}

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>

View file

@ -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;

View file

@ -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());

View file

@ -177,12 +177,10 @@ public class OverallTest extends AegisTest {
}
private void changeGroupFilter(String text) {
onView(withId(R.id.chip_group)).perform(click());
if (text == null) {
onView(withId(R.id.btnClear)).perform(click());
onView(allOf(withText(R.string.no_group), isDescendantOfA(withId(R.id.groupChipGroup)))).perform(click());
} else {
onView(withText(text)).perform(click());
onView(isRoot()).perform(pressBack());
onView(allOf(withText(text), isDescendantOfA(withId(R.id.groupChipGroup)))).perform(click());
}
}

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,6 +28,7 @@
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"
android:largeHeap="true"
android:theme="@style/Theme.Aegis.Launch"
tools:targetApi="tiramisu">
<activity android:name=".ui.TransferEntriesActivity"
@ -149,7 +151,7 @@
</application>
<queries>
<package android:name="me.jmh.authenticatorpro" />
<package android:name="com.stratumauth.app" />
<package android:name="com.authy.authy" />
<package android:name="org.fedorahosted.freeotp" />
<package android:name="org.liberty.android.freeotpplus" />

View file

@ -31,6 +31,82 @@
</head>
<body>
<div></div>
<h3>Version 3.4</h3>
<h4>New</h4>
<ul>
<li>Haptic feedback when an entry is about to expire</li>
<li>Brightness increase is now toggleable in the entry transfer view</li>
<li>Filter on multiple groups simultaneously</li>
<li>Color contrast on hidden codes has been improved</li>
<li>Prompt before the user is about to save an entry with a duplicate name/issuer combination</li>
<li>New languages: Estonian, Korean, Malayalam, Norwegian (Bokmål) and Serbian</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>A crash could occur if an entry with period 7 exists and code expiry indication is enabled</li>
<li>The Portuguese (Brazilian) locale was used even if Portuguese was configured</li>
<li>FreeOTP import would fail if the algorithm or digits field was not specified for an entry</li>
<li>The divider between entries would be missing in certain filter configurations</li>
<li>The snackbar in try entry importing view could obstruct the name of an entry</li>
</ul>
<h4>Miscellaneous</h4>
<ul>
<li>Android 6 or newer is now required the run the app</li>
</ul>
<h3>Version 3.3.4</h3>
<h4>Fixes</h4>
<ul>
<li>Icons are now resized to 512x512 to reduce the size of the vault file and to reduce the chance of encountering out of memory conditions</li>
</ul>
<h3>Version 3.3.3</h3>
<h4>Fixes</h4>
<ul>
<li>Some users ran into out of memory conditions due to large icons in their vault file. We've introduced a temporary measure that should help in most cases, but we'll follow up with a more comprehensive fix soon.</li>
<li>Window insets were not always applied correctly, causing parts of the UI to appear off-screen</li>
<li>The 2FAS importer did not tolerate spaces for secrets and was not always able to extract the issuer</li>
</ul>
<h3>Version 3.3.2</h3>
<h4>New</h4>
<ul>
<li>Find entries by searching in multiple fields simultaneously</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Entries would not actually be added to the Aegis vault in some cases when importing from Google Authenticator export QR codes</li>
<li>The lock button was sometimes shown for unencrypted vaults</li>
<li>The sort category menu item did not always reflect the current sorting</li>
<li>The next code was not always easy to read because its color had low contrast with the background</li>
<li>Entry selection was not cancelled when changing the group filter</li>
</ul>
<h3>Version 3.3.1</h3>
<h4>Fixes</h4>
<ul>
<li>Codes were not shown in case the tiles view mode was combined with hidden account names</li>
</ul>
<h3>Version 3.3</h3>
<h4>New</h4>
<ul>
<li>Significant improvements to group filtering
<ul>
<li>Groups can now be filtered on straight from the main view instead of through a dialog</li>
<li>Ability to assign multiple entries to a group in one go</li>
<li>Support for reordering groups</li>
</ul>
</li>
<li>Codes now change color when they're about to expire</li>
<li>Option to show the next code ahead of time</li>
<li>Support for backing up to a single file (This enables support for more cloud providers, such as Google Drive)</li>
<li>Various minor improvements to make QR code exports easier to scan</li>
<li>Support for importing from Ente Auth</li>
<li>Support for importing FreeOTP 2 backups</li>
<li>Updated translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>QR codes exported for Google Authenticator could not be scanned on iOS</li>
<li>The code would be copied after a single tap in case "Tap to reveal" and "Copy tokens to the clipboard" were enabled simultaneously</li>
<li>Various other minor UI, stability and performance improvements</li>
</ul>
<h3>Version 3.2</h3>
<h4>New</h4>
<ul>

View file

@ -17,23 +17,14 @@ import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.room.Room;
import com.beemdevelopment.aegis.database.AppDatabase;
import com.beemdevelopment.aegis.database.AuditLogEntry;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.receivers.VaultLockReceiver;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.topjohnwu.superuser.Shell;
import org.checkerframework.checker.units.qual.A;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import dagger.hilt.InstallIn;
import dagger.hilt.android.EarlyEntryPoint;

View file

@ -0,0 +1,7 @@
package com.beemdevelopment.aegis;
public enum BackupsVersioningStrategy {
UNDEFINED,
MULTIPLE_BACKUPS,
SINGLE_BACKUP
}

View file

@ -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);
}
}
}

View file

@ -5,9 +5,11 @@ import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.provider.DocumentsContractCompat;
import androidx.preference.PreferenceManager;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.TimeUtils;
@ -84,10 +86,18 @@ public class Preferences {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
public boolean isGroupMultiselectEnabled() {
return _prefs.getBoolean("pref_groups_multiselect", false);
}
public boolean isEntryHighlightEnabled() {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean isHapticFeedbackEnabled() {
return _prefs.getBoolean("pref_haptic_feedback", true);
}
public boolean isPauseFocusedEnabled() {
boolean dependenciesEnabled = isTapToRevealEnabled() || isEntryHighlightEnabled();
if (!dependenciesEnabled) return false;
@ -153,6 +163,14 @@ public class Preferences {
return _prefs.getBoolean("pref_show_icons", true);
}
public boolean getShowNextCode() {
return _prefs.getBoolean("pref_show_next_code", false);
}
public boolean getShowExpirationState() {
return _prefs.getBoolean("pref_expiration_state", true);
}
public CodeGrouping getCodeGroupSize() {
String value = _prefs.getString("pref_code_group_size_string", "GROUPING_THREES");
@ -589,6 +607,19 @@ public class Preferences {
}
}
@NonNull
public BackupsVersioningStrategy getBackupVersioningStrategy() {
Uri uri = getBackupsLocation();
if (uri == null) {
return BackupsVersioningStrategy.UNDEFINED;
}
if (DocumentsContractCompat.isTreeUri(uri)) {
return BackupsVersioningStrategy.MULTIPLE_BACKUPS;
} else {
return BackupsVersioningStrategy.SINGLE_BACKUP;
}
}
public static class BackupResult {
private final Date _time;
private boolean _isBuiltIn;

View file

@ -14,10 +14,4 @@ public class ThemeMap {
Theme.DARK, R.style.Theme_Aegis_Dark,
Theme.AMOLED, R.style.Theme_Aegis_Amoled
);
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light_Fullscreen,
Theme.DARK, R.style.Theme_Aegis_Dark_Fullscreen,
Theme.AMOLED, R.style.Theme_Aegis_Amoled_Fullscreen
);
}

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -69,7 +69,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
int swipeFlags = 0;
if (adapter.isPositionFooter(position)
|| adapter.isPositionErrorCard(position)
|| adapter.getEntryAtPos(position) != _selectedEntry
|| adapter.getEntryAtPosition(position) != _selectedEntry
|| !isLongPressDragEnabled()) {
return makeMovementFlags(0, swipeFlags);
}

View file

@ -33,7 +33,10 @@ public class ThemeHelper {
DynamicColorsOptions.Builder optsBuilder = new DynamicColorsOptions.Builder();
if (getConfiguredTheme().equals(Theme.AMOLED)) {
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Amoled);
} else if (getConfiguredTheme().equals(Theme.DARK)) {
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Dark);
}
DynamicColors.applyToActivityIfAvailable(_activity, optsBuilder.build());
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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;
});
}
}

View file

@ -34,17 +34,18 @@ public abstract class DatabaseImporter {
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
_importers.add(new Definition("Authenticator Pro", AuthenticatorProImporter.class, R.string.importer_help_authenticator_pro, true));
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
_importers.add(new Definition("Battle.net Authenticator", BattleNetImporter.class, R.string.importer_help_battle_net_authenticator, true));
_importers.add(new Definition("Bitwarden", BitwardenImporter.class, R.string.importer_help_bitwarden, false));
_importers.add(new Definition("Duo", DuoImporter.class, R.string.importer_help_duo, true));
_importers.add(new Definition("FreeOTP (1.x)", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("Ente Auth", EnteAuthImporter.class, R.string.importer_help_ente_auth, false));
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+ (JSON)", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
_importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true));
_importers.add(new Definition("Stratum (Authenticator Pro)", StratumImporter.class, R.string.importer_help_stratum, true));
_importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}

View file

@ -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);

View file

@ -4,9 +4,9 @@ import android.content.Context;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@ -16,6 +16,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.base.Strings;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
@ -173,9 +174,12 @@ public class TwoFASImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(obj.getString("secret"));
byte[] secret = GoogleAuthInfo.parseSecret(obj.getString("secret"));
JSONObject info = obj.getJSONObject("otp");
String issuer = info.optString("issuer");
String issuer = obj.optString("name");
if (Strings.isNullOrEmpty(issuer)) {
issuer = info.optString("issuer");
}
String name = info.optString("account");
int digits = info.optInt("digits", TotpInfo.DEFAULT_DIGITS);
String algorithm = info.optString("algorithm", TotpInfo.DEFAULT_ALGORITHM);

View file

@ -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) {

View file

@ -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);

View file

@ -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();

View file

@ -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);

View file

@ -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);

View file

@ -13,11 +13,15 @@ import android.widget.Toast;
import androidx.annotation.AttrRes;
import androidx.annotation.StringRes;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.ChangelogDialog;
import com.beemdevelopment.aegis.ui.dialogs.LicenseDialog;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.google.android.material.color.MaterialColors;
public class AboutActivity extends AegisActivity {
@ -39,6 +43,7 @@ public class AboutActivity extends AegisActivity {
setContentView(R.layout.activity_about);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@ -90,6 +95,17 @@ public class AboutActivity extends AegisActivity {
.setTheme(_themeHelper.getConfiguredTheme())
.show(getSupportFragmentManager(), null);
});
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.about_scroll_view), (targetView, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
targetView.setPadding(
0,
0,
0,
insets.bottom
);
return WindowInsetsCompat.CONSUMED;
});
}
private static String getCurrentAppVersion() {

View file

@ -4,7 +4,7 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
@ -12,7 +12,6 @@ import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
@ -26,6 +25,7 @@ import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.color.MaterialColors;
import java.lang.reflect.Field;
@ -186,13 +186,9 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
private class ActionModeStatusGuardHack {
private Field _fadeAnimField;
private Field _actionModeViewField;
@ColorInt
private final int _statusBarColor;
private Drawable _appBarBackground;
private ActionModeStatusGuardHack() {
_statusBarColor = getWindow().getStatusBarColor();
try {
_fadeAnimField = getDelegate().getClass().getDeclaredField("mFadeAnim");
_fadeAnimField.setAccessible(true);
@ -216,20 +212,26 @@ public abstract class AegisActivity extends AppCompatActivity implements VaultMa
return;
}
if (fadeAnim == null || actionModeView == null) {
AppBarLayout appBarLayout = findViewById(R.id.app_bar_layout);
if (appBarLayout != null && _appBarBackground == null) {
_appBarBackground = appBarLayout.getBackground();
}
if (fadeAnim == null || actionModeView == null || appBarLayout == null || _appBarBackground == null) {
return;
}
fadeAnim.cancel();
actionModeView.setVisibility(visibility);
actionModeView.setAlpha(visibility == View.VISIBLE ? 1f : 0f);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int statusBarColor = visibility == View.VISIBLE
? MaterialColors.getColor(actionModeView, com.google.android.material.R.attr.colorSurfaceContainer)
: _statusBarColor;
getWindow().setStatusBarColor(statusBarColor);
if (visibility == View.VISIBLE) {
actionModeView.setVisibility(visibility);
actionModeView.setAlpha(1f);
int color = MaterialColors.getColor(appBarLayout, com.google.android.material.R.attr.colorSurfaceContainer);
appBarLayout.setBackgroundColor(color);
} else {
actionModeView.setVisibility(visibility);
actionModeView.setAlpha(0f);
appBarLayout.setBackground(_appBarBackground);
}
}
}

View file

@ -25,6 +25,7 @@ import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
import com.beemdevelopment.aegis.ui.views.AssignIconAdapter;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.bumptech.glide.Glide;
@ -61,6 +62,7 @@ public class AssignIconsActivity extends AegisActivity implements AssignIconAdap
setContentView(R.layout.activity_assign_icons);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);

View file

@ -1,6 +1,7 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
@ -27,6 +28,7 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import com.amulyakhare.textdrawable.TextDrawable;
import com.avito.android.krop.KropView;
@ -35,12 +37,14 @@ import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.SafHelper;
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
import com.beemdevelopment.aegis.helpers.SimpleTextWatcher;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
@ -75,7 +79,6 @@ import com.google.android.material.imageview.ShapeableImageView;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@ -86,6 +89,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@ -102,6 +106,7 @@ public class EditEntryActivity extends AegisActivity {
// keep track of icon changes separately as the generated jpeg's are not deterministic
private boolean _hasChangedIcon = false;
private IconPack.Icon _selectedIcon;
private String _pickedMimeType;
private ShapeableImageView _iconView;
private ImageView _saveImageButton;
@ -139,8 +144,8 @@ public class EditEntryActivity extends AegisActivity {
if (activityResult.getResultCode() != RESULT_OK || data == null || data.getData() == null) {
return;
}
String fileType = SafHelper.getMimeType(this, data.getData());
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
_pickedMimeType = SafHelper.getMimeType(this, data.getData());
if (_pickedMimeType != null && _pickedMimeType.equals(IconType.SVG.toMimeType())) {
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
ImportFileTask task = new ImportFileTask(this, result -> {
if (result.getError() == null) {
@ -164,6 +169,7 @@ public class EditEntryActivity extends AegisActivity {
}
setContentView(R.layout.activity_edit_entry);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_groups = _vaultManager.getVault().getGroups();
@ -802,11 +808,12 @@ public class EditEntryActivity extends AegisActivity {
VaultEntryIcon icon;
if (_selectedIcon == null) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// the quality parameter is ignored for PNG
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] data = stream.toByteArray();
icon = new VaultEntryIcon(data, IconType.PNG);
IconType iconType = _pickedMimeType == null
? IconType.INVALID : IconType.fromMimeType(_pickedMimeType);
if (iconType == IconType.INVALID) {
iconType = bitmap.hasAlpha() ? IconType.PNG : IconType.JPEG;
}
icon = BitmapHelper.toVaultEntryIcon(bitmap, iconType);
} else {
byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
@ -848,10 +855,92 @@ public class EditEntryActivity extends AegisActivity {
return false;
}
if (_isNew) {
for (VaultEntry existing : _vaultManager.getVault().getEntries()) {
if (entry.hasSameNameAndIssuer(existing)) {
showDuplicateBottomSheet(entry);
return false;
}
}
}
addAndFinish(entry);
return true;
}
private void showDuplicateBottomSheet(VaultEntry newEntry) {
BottomSheetDialog dialog = new BottomSheetDialog(this);
View view = getLayoutInflater().inflate(R.layout.dialog_duplicate_entry, null);
dialog.setContentView(view);
dialog.setCancelable(false);
View overwrite = view.findViewById(R.id.overwrite_entry);
View addSuffix = view.findViewById(R.id.create_new_entry);
View cancel = view.findViewById(R.id.cancel_save);
TextView suffixSubtext = view.findViewById(R.id.duplicate_suffix_subtitle);
String baseName = newEntry.getName();
Set<String> existingNames = new HashSet<>();
for (VaultEntry e : _vaultManager.getVault().getEntries()) {
if (e.getIssuer().equals(newEntry.getIssuer())) {
existingNames.add(e.getName());
}
}
int counter = 2;
String newName;
do {
newName = baseName + " #" + counter++;
} while (existingNames.contains(newName));
suffixSubtext.setText(getString(R.string.dialog_duplicate_entry_suffix_subtitle, newName));
overwrite.setOnClickListener(v -> {
List<VaultEntry> duplicates = new ArrayList<>();
for (VaultEntry existing : _vaultManager.getVault().getEntries()) {
if (existing.hasSameNameAndIssuer(newEntry)) {
duplicates.add(existing);
}
}
Resources res = getResources();
String message = res.getQuantityString(
R.plurals.dialog_duplicate_entry_overwrite_dialog_message,
duplicates.size(),
duplicates.size(),
newEntry.getIssuer(),
newEntry.getName()
);
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_duplicate_entry_overwrite_dialog_title)
.setMessage(message)
.setPositiveButton(R.string.action_delete, (d, which) -> {
for (VaultEntry dup : duplicates) {
_vaultManager.getVault().removeEntry(dup);
}
dialog.dismiss();
addAndFinish(newEntry);
})
.setNegativeButton(android.R.string.no, null)
.show();
});
String finalNewName = newName;
addSuffix.setOnClickListener(v -> {
newEntry.setName(finalNewName);
dialog.dismiss();
addAndFinish(newEntry);
});
cancel.setOnClickListener(v -> dialog.dismiss());
Dialogs.showSecureDialog(dialog);
}
private static void setViewEnabled(View view, boolean enabled) {
view.setEnabled(enabled);

View file

@ -7,6 +7,7 @@ import android.view.View;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -14,13 +15,10 @@ import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.vault.VaultEntryException;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -30,7 +28,6 @@ import java.util.UUID;
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
private GroupAdapter _adapter;
private HashSet<UUID> _removedGroups;
private HashSet<VaultGroup> _renamedGroups;
private RecyclerView _groupsView;
private View _emptyStateView;
private BackPressHandler _backPressHandler;
@ -43,6 +40,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
}
setContentView(R.layout.activity_groups);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
@ -51,32 +49,45 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
_removedGroups = new HashSet<>();
_renamedGroups = new HashSet<>();
if (savedInstanceState != null) {
List<String> removedGroups = savedInstanceState.getStringArrayList("removedGroups");
List<String> renamedGroups = savedInstanceState.getStringArrayList("renamedGroups");
if (removedGroups != null) {
for (String uuid : removedGroups) {
_removedGroups.add(UUID.fromString(uuid));
}
}
if (renamedGroups != null) {
for (String groupObject : renamedGroups) {
try {
_renamedGroups.add(VaultGroup.fromJson(new JSONObject(groupObject)));
} catch (VaultEntryException | JSONException ignored) {
// This is intentionally ignored since the json object is valid
}
}
}
}
ItemTouchHelper touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(
@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
int draggedItemIndex = viewHolder.getBindingAdapterPosition();
int targetIndex = target.getBindingAdapterPosition();
_adapter.onItemMove(draggedItemIndex, targetIndex);
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { }
});
_adapter = new GroupAdapter(this);
_groupsView = findViewById(R.id.list_groups);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
_groupsView.setLayoutManager(layoutManager);
_groupsView.setAdapter(_adapter);
_groupsView.setNestedScrollingEnabled(false);
touchHelper.attachToRecyclerView(_groupsView);
for (VaultGroup group : _vaultManager.getVault().getGroups()) {
if (!_removedGroups.contains(group.getUUID())) {
@ -84,11 +95,6 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
}
}
for(VaultGroup group: _renamedGroups) {
_adapter.replaceGroup(group.getUUID(), group);
}
_emptyStateView = findViewById(R.id.vEmptyList);
updateEmptyState();
}
@ -100,12 +106,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
for (UUID uuid : _removedGroups) {
removed.add(uuid.toString());
}
ArrayList<String> renamed = new ArrayList<>();
for (VaultGroup group : _renamedGroups) {
renamed.add(group.toJson().toString());
}
outState.putStringArrayList("renamedGroups", renamed);
outState.putStringArrayList("removedGroups", removed);
}
@ -116,7 +117,6 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
if (!newGroupName.isEmpty()) {
VaultGroup newGroup = Cloner.clone(group);
newGroup.setName(newGroupName);
_renamedGroups.add(newGroup);
_adapter.replaceGroup(group.getUUID(), newGroup);
_backPressHandler.setEnabled(true);
}
@ -166,23 +166,16 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
for (UUID uuid : _removedGroups) {
_vaultManager.getVault().removeGroup(uuid);
}
saveAndBackupVault();
}
if (!_renamedGroups.isEmpty()) {
_renamedGroups.removeIf(group -> _removedGroups.contains(group.getUUID()));
for (VaultGroup renamedGroup : _renamedGroups) {
_vaultManager.getVault().renameGroup(renamedGroup);
}
saveAndBackupVault();
}
_vaultManager.getVault().replaceGroups(_adapter.getGroups());
saveAndBackupVault();
finish();
}
private void discardAndFinish() {
if (_removedGroups.isEmpty() && _renamedGroups.isEmpty()) {
if (_removedGroups.isEmpty()) {
finish();
return;
}

View file

@ -15,16 +15,21 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.tasks.IconOptimizationTask;
import com.beemdevelopment.aegis.ui.tasks.RootShellTask;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@ -39,12 +44,15 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class ImportEntriesActivity extends AegisActivity {
private View _view;
private Menu _menu;
private RecyclerView _entriesView;
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
@ -58,6 +66,7 @@ public class ImportEntriesActivity extends AegisActivity {
}
setContentView(R.layout.activity_import_entries);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_view = findViewById(R.id.importEntriesRootView);
@ -66,8 +75,8 @@ public class ImportEntriesActivity extends AegisActivity {
bar.setDisplayHomeAsUpEnabled(true);
_adapter = new ImportEntriesAdapter();
RecyclerView entriesView = findViewById(R.id.list_entries);
entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
_entriesView = findViewById(R.id.list_entries);
_entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
@ -76,9 +85,9 @@ public class ImportEntriesActivity extends AegisActivity {
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
entriesView.setLayoutManager(layoutManager);
entriesView.setAdapter(_adapter);
entriesView.setNestedScrollingEnabled(false);
_entriesView.setLayoutManager(layoutManager);
_entriesView.setAdapter(_adapter);
_entriesView.setNestedScrollingEnabled(false);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> {
@ -170,7 +179,7 @@ public class ImportEntriesActivity extends AegisActivity {
state.decrypt(this, new DatabaseImporter.DecryptListener() {
@Override
public void onStateDecrypted(DatabaseImporter.State state) {
importDatabase(state);
processDecryptedImporterState(state);
}
@Override
@ -185,7 +194,7 @@ public class ImportEntriesActivity extends AegisActivity {
}
});
} else {
importDatabase(state);
processDecryptedImporterState(state);
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
@ -193,8 +202,7 @@ public class ImportEntriesActivity extends AegisActivity {
}
}
private void importDatabase(DatabaseImporter.State state) {
List<ImportEntry> importEntries = new ArrayList<>();
private void processDecryptedImporterState(DatabaseImporter.State state) {
DatabaseImporter.Result result;
try {
result = state.convert();
@ -204,8 +212,29 @@ public class ImportEntriesActivity extends AegisActivity {
return;
}
UUIDMap<VaultEntry> entries = result.getEntries();
for (VaultEntry entry : entries.getValues()) {
Map<UUID, VaultEntryIcon> icons = result.getEntries().getValues().stream()
.filter(e -> e.getIcon() != null
&& !e.getIcon().getType().equals(IconType.SVG)
&& !BitmapHelper.isVaultEntryIconOptimized(e.getIcon()))
.collect(Collectors.toMap(VaultEntry::getUUID, VaultEntry::getIcon));
if (!icons.isEmpty()) {
IconOptimizationTask task = new IconOptimizationTask(this, newIcons -> {
for (Map.Entry<UUID, VaultEntryIcon> mapEntry : newIcons.entrySet()) {
VaultEntry entry = result.getEntries().getByUUID(mapEntry.getKey());
entry.setIcon(mapEntry.getValue());
}
processImporterResult(result);
});
task.execute(getLifecycle(), icons);
} else {
processImporterResult(result);
}
}
private void processImporterResult(DatabaseImporter.Result result) {
List<ImportEntry> importEntries = new ArrayList<>();
for (VaultEntry entry : result.getEntries().getValues()) {
ImportEntry importEntry = new ImportEntry(entry);
_adapter.addEntry(importEntry);
importEntries.add(importEntry);
@ -330,6 +359,31 @@ public class ImportEntriesActivity extends AegisActivity {
_adapter.setCheckboxStates(duplicateEntries, false);
Snackbar snackbar = Snackbar.make(_view, getResources().getQuantityString(R.plurals.import_duplicate_toast, duplicateEntries.size(), duplicateEntries.size()), Snackbar.LENGTH_INDEFINITE);
snackbar.addCallback(new Snackbar.Callback() {
@Override
public void onShown(Snackbar sb) {
int snackbarHeight = sb.getView().getHeight();
_entriesView.setPadding(
_entriesView.getPaddingLeft(),
_entriesView.getPaddingTop(),
_entriesView.getPaddingRight(),
_entriesView.getPaddingBottom() + snackbarHeight * 2
);
}
@Override
public void onDismissed(Snackbar sb, int event) {
int snackbarHeight = sb.getView().getHeight();
_entriesView.setPadding(
_entriesView.getPaddingLeft(),
_entriesView.getPaddingTop(),
_entriesView.getPaddingRight(),
_entriesView.getPaddingBottom() - snackbarHeight * 2
);
}
});
snackbar.setAction(R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View v) {

View file

@ -24,6 +24,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Toast;
@ -32,17 +33,23 @@ import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.beemdevelopment.aegis.GroupPlaceholderType;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@ -50,26 +57,39 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
import com.beemdevelopment.aegis.ui.tasks.IconOptimizationTask;
import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import com.beemdevelopment.aegis.util.TimeUtils;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
public class MainActivity extends AegisActivity implements EntryListView.Listener {
@ -91,6 +111,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private SearchView _searchView;
private EntryListView _entryListView;
private Collection<VaultGroup> _groups;
private ChipGroup _groupChip;
private Set<UUID> _groupFilter;
private Set<UUID> _prefGroupFilter;
private FabScrollHelper _fabScrollHelper;
private ActionMode _actionMode;
@ -129,7 +154,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
return;
}
onAssignEntriesResult(activityResult.getData());
onAssignIconsResult();
});
private final ActivityResultLauncher<Intent> preferenceResultLauncher =
@ -140,7 +165,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
return;
}
onEditEntryResult(activityResult.getData());
onEditEntryResult();
});
private final ActivityResultLauncher<Intent> addEntryResultLauncher =
@ -163,6 +188,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_loaded = false;
_isDPadPressed = false;
_isDoingIntro = false;
@ -187,16 +213,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.setCodeGroupSize(_prefs.getCodeGroupSize());
_entryListView.setAccountNamePosition(_prefs.getAccountNamePosition());
_entryListView.setShowIcon(_prefs.isIconVisible());
_entryListView.setShowExpirationState(_prefs.getShowExpirationState());
_entryListView.setShowNextCode(_prefs.getShowNextCode());
_entryListView.setOnlyShowNecessaryAccountNames(_prefs.onlyShowNecessaryAccountNames());
_entryListView.setHighlightEntry(_prefs.isEntryHighlightEnabled());
_entryListView.setPauseFocused(_prefs.isPauseFocusedEnabled());
_entryListView.setTapToReveal(_prefs.isTapToRevealEnabled());
_entryListView.setTapToRevealTime(_prefs.getTapToRevealTime());
_entryListView.setSortCategory(_prefs.getCurrentSortCategory(), false);
_entryListView.setViewMode(_prefs.getCurrentViewMode());
_entryListView.setSortCategory(_prefs.getCurrentSortCategory(), false);
_entryListView.setCopyBehavior(_prefs.getCopyBehavior());
_entryListView.setSearchBehaviorMask(_prefs.getSearchBehaviorMask());
_entryListView.setPrefGroupFilter(_prefs.getGroupFilter());
_prefGroupFilter = _prefs.getGroupFilter();
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> {
@ -220,10 +248,140 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
Dialogs.showSecureDialog(dialog);
});
_groupChip = findViewById(R.id.groupChipGroup);
_fabScrollHelper = new FabScrollHelper(fab);
_selectedEntries = new ArrayList<>();
}
public void setGroups(Collection<VaultGroup> groups) {
_groups = groups;
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
if (_prefGroupFilter != null) {
Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter);
_prefGroupFilter = null;
if (!groupFilter.isEmpty()) {
_groupFilter = groupFilter;
_entryListView.setGroupFilter(groupFilter);
}
} else if (_groupFilter != null) {
Set<UUID> groupFilter = cleanGroupFilter(_groupFilter);
if (!_groupFilter.equals(groupFilter)) {
_groupFilter = groupFilter;
_entryListView.setGroupFilter(groupFilter);
}
}
_entryListView.setGroups(groups);
initializeGroups();
}
private void initializeGroups() {
_groupChip.removeAllViews();
_groupChip.setSingleSelection(!_prefs.isGroupMultiselectEnabled());
for (VaultGroup group : _groups) {
addChipTo(_groupChip, new VaultGroupModel(group));
}
GroupPlaceholderType placeholderType = GroupPlaceholderType.NO_GROUP;
addChipTo(_groupChip, new VaultGroupModel(this, placeholderType));
addSaveChip(_groupChip);
}
private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) {
Set<UUID> groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet());
return groupFilter.stream()
.filter(g -> g == null || groupUuids.contains(g))
.collect(Collectors.toSet());
}
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) {
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
chip.setText(group.getName());
chip.setCheckable(true);
chip.setCheckedIconVisible(false);
chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID()));
if (group.isPlaceholder()) {
GroupPlaceholderType groupPlaceholderType = group.getPlaceholderType();
chip.setTag(groupPlaceholderType);
if (groupPlaceholderType == GroupPlaceholderType.ALL) {
chip.setChecked(_groupFilter == null);
} else if (groupPlaceholderType == GroupPlaceholderType.NO_GROUP) {
chip.setChecked(_groupFilter != null && _groupFilter.contains(null));
}
} else {
chip.setTag(group);
}
chip.setOnCheckedChangeListener((group1, isChecked) -> {
if (_actionMode != null) {
_actionMode.finish();
}
setSaveChipVisibility(true);
// Reset group filter if last checked group gets unchecked
if (!isChecked && _groupFilter.size() == 1) {
Set<UUID> groupFilter = new HashSet<>();
chipGroup.clearCheck();
_groupFilter = groupFilter;
_entryListView.setGroupFilter(groupFilter);
return;
}
_groupFilter = getGroupFilter(chipGroup);
_entryListView.setGroupFilter(_groupFilter);
});
chipGroup.addView(chip);
}
private void addSaveChip(ChipGroup chipGroup) {
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
chip.setText(getString(R.string.save));
chip.setVisibility(View.GONE);
chip.setChipStrokeWidth(0);
chip.setCheckable(false);
chip.setChipBackgroundColorResource(android.R.color.transparent);
chip.setTextColor(MaterialColors.getColor(chip.getRootView(), com.google.android.material.R.attr.colorSecondary));
chip.setClickable(true);
chip.setCheckedIconVisible(false);
chip.setOnClickListener(v -> {
onSaveGroupFilter(_groupFilter);
setSaveChipVisibility(false);
});
chipGroup.addView(chip);
}
private void setSaveChipVisibility(boolean visible) {
Chip saveChip = (Chip) _groupChip.getChildAt(_groupChip.getChildCount() - 1);
saveChip.setChecked(false);
saveChip.setVisibility(visible ? View.VISIBLE : View.GONE);
}
private static Set<UUID> getGroupFilter(ChipGroup chipGroup) {
return chipGroup.getCheckedChipIds().stream()
.filter(Objects::nonNull)
.map(i -> {
Chip chip = chipGroup.findViewById(i);
if (chip.getTag() instanceof VaultGroupModel) {
VaultGroupModel group = (VaultGroupModel) chip.getTag();
return group.getUUID();
}
return null;
})
.collect(Collectors.toSet());
}
@Override
protected void onDestroy() {
_entryListView.setListener(null);
@ -252,6 +410,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
instance.putString("submittedSearchQuery", _submittedSearchQuery);
instance.putBoolean("isDoingIntro", _isDoingIntro);
instance.putBoolean("isAuthenticating", _isAuthenticating);
if (_groupFilter != null) {
instance.putSerializable("prefGroupFilter", new HashSet<>(_groupFilter));
}
}
@Override
@ -330,6 +492,76 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
assignIconsResultLauncher.launch(assignIconIntent);
}
private void startAssignGroupsDialog() {
View view = LayoutInflater.from(this).inflate(R.layout.dialog_select_group, null);
TextInputLayout groupSelectionLayout = view.findViewById(R.id.group_selection_layout);
AutoCompleteTextView groupsSelection = view.findViewById(R.id.group_selection_dropdown);
TextInputLayout newGroupLayout = view.findViewById(R.id.text_group_name_layout);
TextInputEditText newGroupText = view.findViewById(R.id.text_group_name);
Collection<VaultGroup> groups = _vaultManager.getVault().getUsedGroups();
List<VaultGroupModel> groupModels = new ArrayList<>();
groupModels.add(new VaultGroupModel(this, GroupPlaceholderType.NEW_GROUP));
groupModels.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
DropdownHelper.fillDropdown(this, groupsSelection, groupModels);
AtomicReference<VaultGroupModel> groupModelRef = new AtomicReference<>();
groupsSelection.setOnItemClickListener((parent, view1, position, id) -> {
VaultGroupModel groupModel = (VaultGroupModel) parent.getItemAtPosition(position);
groupModelRef.set(groupModel);
if (groupModel.isPlaceholder()) {
newGroupLayout.setVisibility(View.VISIBLE);
newGroupText.requestFocus();
} else {
newGroupLayout.setVisibility(View.GONE);
}
groupSelectionLayout.setError(null);
});
AlertDialog dialog = new MaterialAlertDialogBuilder(this)
.setTitle(R.string.assign_groups)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setOnShowListener(d -> {
Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
btnPos.setOnClickListener(v -> {
VaultGroupModel groupModel = groupModelRef.get();
if (groupModel == null) {
groupSelectionLayout.setError(getString(R.string.error_required_field));
return;
}
if (groupModel.isPlaceholder()) {
String newGroupName = newGroupText.getText().toString().trim();
if (newGroupName.isEmpty()) {
newGroupLayout.setError(getString(R.string.error_required_field));
return;
}
VaultGroup group = new VaultGroup(newGroupName);
_vaultManager.getVault().addGroup(group);
groupModel = new VaultGroupModel(group);
}
for (VaultEntry selectedEntry : _selectedEntries) {
selectedEntry.addGroup(groupModel.getUUID());
}
dialog.dismiss();
saveAndBackupVault();
_actionMode.finish();
setGroups(_vaultManager.getVault().getUsedGroups());
});
});
Dialogs.showSecureDialog(dialog);
}
private void startIntroActivity() {
if (!_isDoingIntro) {
Intent intro = new Intent(this, IntroActivity.class);
@ -349,31 +581,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
_entryListView.addEntry(entry, true);
_entryListView.setEntries(_vaultManager.getVault().getEntries());
_entryListView.onEntryAdded(entry);
}
}
private void onEditEntryResult(Intent data) {
private void onEditEntryResult() {
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
if (data.getBooleanExtra("delete", false)) {
_entryListView.removeEntry(entryUUID);
} else {
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
_entryListView.replaceEntry(entryUUID, entry);
}
_entryListView.setEntries(_vaultManager.getVault().getEntries());
}
}
private void onAssignEntriesResult(Intent data) {
private void onAssignIconsResult() {
if (_loaded) {
ArrayList<UUID> entryUUIDs = (ArrayList<UUID>) data.getSerializableExtra("entryUUIDs");
for (UUID entryUUID: entryUUIDs) {
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
_entryListView.replaceEntry(entryUUID, entry);
}
_entryListView.setEntries(_vaultManager.getVault().getEntries());
}
}
@ -473,18 +694,21 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
} else if (entries.size() > 1) {
for (VaultEntry entry: entries) {
_vaultManager.getVault().addEntry(entry);
_entryListView.addEntry(entry);
}
if (saveAndBackupVault()) {
Toast.makeText(this, getResources().getQuantityString(R.plurals.added_new_entries, entries.size(), entries.size()), Toast.LENGTH_LONG).show();
}
_entryListView.setEntries(_vaultManager.getVault().getEntries());
}
}
private void updateSortCategoryMenu() {
SortCategory category = _prefs.getCurrentSortCategory();
_menu.findItem(category.getMenuItem()).setChecked(true);
if (_menu != null) {
SortCategory category = _prefs.getCurrentSortCategory();
_menu.findItem(category.getMenuItem()).setChecked(true);
}
}
private void onIntroResult() {
@ -501,6 +725,37 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void checkIconOptimization() {
if (!_vaultManager.getVault().areIconsOptimized()) {
Map<UUID, VaultEntryIcon> oldIcons = _vaultManager.getVault().getEntries().stream()
.filter(e -> e.getIcon() != null
&& !e.getIcon().getType().equals(IconType.SVG)
&& !BitmapHelper.isVaultEntryIconOptimized(e.getIcon()))
.collect(Collectors.toMap(VaultEntry::getUUID, VaultEntry::getIcon));
if (!oldIcons.isEmpty()) {
IconOptimizationTask task = new IconOptimizationTask(this, this::onIconsOptimized);
task.execute(getLifecycle(), oldIcons);
} else {
onIconsOptimized(Collections.emptyMap());
}
}
}
private void onIconsOptimized(Map<UUID, VaultEntryIcon> newIcons) {
for (Map.Entry<UUID, VaultEntryIcon> mapEntry : newIcons.entrySet()) {
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(mapEntry.getKey());
entry.setIcon(mapEntry.getValue());
}
_vaultManager.getVault().setIconsOptimized(true);
saveAndBackupVault();
if (!newIcons.isEmpty()) {
_entryListView.setEntries(_vaultManager.getVault().getEntries());
}
}
private void onDecryptResult() {
_auditLogRepository.addVaultUnlockedEvent();
@ -635,6 +890,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
@Override
protected void onStop() {
super.onStop();
_entryListView.onRefreshStop();
}
@Override
protected void onStart() {
super.onStart();
@ -677,7 +939,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startAuthActivity(false);
} else if (_loaded) {
// update the list of groups in the entry list view so that the chip gets updated
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
setGroups(_vaultManager.getVault().getUsedGroups());
// update the usage counts in case they are edited outside of the EntryListView
_entryListView.setUsageCounts(_prefs.getUsageCounts());
@ -686,43 +948,36 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
// refresh all codes to prevent showing old ones
_entryListView.refresh(false);
_entryListView.onRefreshStart();
} else {
loadEntries();
checkTimeSyncSetting();
checkIconOptimization();
_entryListView.onRefreshStart();
}
_lockBackPressHandler.setEnabled(
_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)
_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)
);
handleIncomingIntent();
updateLockIcon();
updateSortCategoryMenu();
doShortcutActions();
updateErrorCard();
}
private void deleteEntries(List<VaultEntry> entries) {
for (VaultEntry entry: entries) {
VaultEntry oldEntry = _vaultManager.getVault().removeEntry(entry);
_entryListView.removeEntry(oldEntry);
}
saveAndBackupVault();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
getMenuInflater().inflate(R.menu.menu_main, menu);
updateLockIcon();
if (_loaded) {
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
updateSortCategoryMenu();
}
updateSortCategoryMenu();
MenuItem searchViewMenuItem = menu.findItem(R.id.mi_search);
_searchView = (SearchView) searchViewMenuItem.getActionView();
_searchView.setMaxWidth(Integer.MAX_VALUE);
_searchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> {
@ -732,6 +987,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_searchView.setOnCloseListener(() -> {
boolean enabled = _submittedSearchQuery != null;
_searchViewBackPressHandler.setEnabled(enabled);
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
return false;
});
@ -765,6 +1021,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
});
_searchView.setOnSearchClickListener(v -> {
String query = _submittedSearchQuery != null ? _submittedSearchQuery : _pendingSearchQuery;
_groupChip.setVisibility(View.GONE);
_searchView.setQuery(query, false);
});
@ -830,16 +1087,17 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void collapseSearchView() {
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
_searchView.setQuery(null, false);
_searchView.setIconified(true);
}
private void loadEntries() {
if (!_loaded) {
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
setGroups(_vaultManager.getVault().getUsedGroups());
_entryListView.setUsageCounts(_prefs.getUsageCounts());
_entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps());
_entryListView.addEntries(_vaultManager.getVault().getEntries());
_entryListView.setEntries(_vaultManager.getVault().getEntries());
if (!_isRecreated) {
_entryListView.runEntriesAnimation();
}
@ -930,6 +1188,19 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
Dialogs.showSecureDialog(dialog);
}
@Override
public void onRestoreInstanceState(@Nullable Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState == null) {
return;
}
HashSet<UUID> filter = (HashSet<UUID>) savedInstanceState.getSerializable("prefGroupFilter");
if (filter != null) {
_prefGroupFilter = filter;
}
}
@Override
public void onEntryClick(VaultEntry entry) {
if (_actionMode != null) {
@ -1054,6 +1325,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
@Override
protected boolean saveAndBackupVault() {
boolean res = super.saveAndBackupVault();
updateErrorCard();
return res;
}
@SuppressLint("InlinedApi")
private void copyEntryCode(VaultEntry entry) {
String otp;
@ -1150,12 +1428,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
mode.finish();
} else if (itemId == R.id.action_toggle_favorite) {
for (VaultEntry entry : _selectedEntries) {
entry.setIsFavorite(!entry.isFavorite());
_entryListView.replaceEntry(entry.getUUID(), entry);
_vaultManager.getVault().editEntry(entry, newEntry -> {
newEntry.setIsFavorite(!newEntry.isFavorite());
});
}
_entryListView.refresh(true);
saveAndBackupVault();
_entryListView.setEntries(_vaultManager.getVault().getEntries());
mode.finish();
} else if (itemId == R.id.action_share_qr) {
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
@ -1173,8 +1452,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
mode.finish();
} else if (itemId == R.id.action_delete) {
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
deleteEntries(_selectedEntries);
for (VaultEntry entry : _selectedEntries) {
_vaultManager.getVault().removeEntry(entry);
}
saveAndBackupVault();
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
_entryListView.setEntries(_vaultManager.getVault().getEntries());
mode.finish();
});
} else if (itemId == R.id.action_select_all) {
@ -1184,6 +1467,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
} else if (itemId == R.id.action_assign_icons) {
startAssignIconsActivity(_selectedEntries);
mode.finish();
} else if (itemId == R.id.action_assign_groups) {
startAssignGroupsDialog();
} else {
return false;
}

View file

@ -13,6 +13,7 @@ import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.fragments.preferences.AppearancePreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.preferences.MainPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
import com.beemdevelopment.aegis.helpers.ViewHelper;
public class PreferencesActivity extends AegisActivity implements
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
@ -27,6 +28,7 @@ public class PreferencesActivity extends AegisActivity implements
}
setContentView(R.layout.activity_preferences);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
getSupportFragmentManager()
.registerFragmentLifecycleCallbacks(new FragmentResumeListener(), true);

View file

@ -18,11 +18,11 @@ import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.zxing.Result;
@ -57,6 +57,7 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
}
setContentView(R.layout.activity_scanner);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_entries = new ArrayList<>();
_lenses = new ArrayList<>();
@ -87,11 +88,6 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
}, ContextCompat.getMainExecutor(this));
}
@Override
protected void onSetTheme() {
_themeHelper.setTheme(ThemeMap.FULLSCREEN);
}
@Override
protected void onDestroy() {
if (_executor != null) {

View file

@ -10,15 +10,17 @@ import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.util.TypedValue;
import android.provider.Settings;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.ColorInt;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
@ -27,6 +29,9 @@ import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.Transferable;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.imageview.ShapeableImageView;
import com.google.zxing.WriterException;
import java.util.ArrayList;
@ -34,7 +39,7 @@ import java.util.List;
public class TransferEntriesActivity extends AegisActivity {
private List<Transferable> _authInfos;
private ImageView _qrImage;
private ShapeableImageView _qrImage;
private TextView _description;
private TextView _issuer;
private TextView _accountName;
@ -43,6 +48,8 @@ public class TransferEntriesActivity extends AegisActivity {
private Button _previousButton;
private Button _copyButton;
private int _currentEntryCount = 1;
private float _deviceBrightness;
private boolean _isMaxBrightnessSet = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -52,6 +59,7 @@ public class TransferEntriesActivity extends AegisActivity {
}
setContentView(R.layout.activity_share_entry);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_qrImage = findViewById(R.id.ivQrCode);
_description = findViewById(R.id.tvDescription);
@ -62,7 +70,7 @@ public class TransferEntriesActivity extends AegisActivity {
_previousButton = findViewById(R.id.btnPrevious);
_copyButton = findViewById(R.id.btnCopyClipboard);
if (getSupportActionBar() != null){
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
@ -88,7 +96,7 @@ public class TransferEntriesActivity extends AegisActivity {
});
_previousButton.setOnClickListener(v -> {
if (_currentEntryCount > 1 ) {
if (_currentEntryCount > 1) {
_nextButton.setText(R.string.next);
_currentEntryCount--;
generateQR();
@ -116,14 +124,59 @@ public class TransferEntriesActivity extends AegisActivity {
if (clipboard != null) {
clipboard.setPrimaryClip(clip);
}
Toast.makeText(this,R.string.uri_copied_to_clipboard, Toast.LENGTH_SHORT).show();
Toast.makeText(this, R.string.uri_copied_to_clipboard, Toast.LENGTH_SHORT).show();
} catch (GoogleAuthInfoException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_copy_uri_to_clipboard, e);
}
});
generateQR();
// Calculate sensible dimensions for the QR code depending on whether we're in landscape
_qrImage.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ConstraintLayout layout = findViewById(R.id.layoutShareEntry);
if (layout.getWidth() > layout.getHeight()) {
int squareSize = (int) (0.5 * layout.getHeight());
ViewGroup.LayoutParams params = _qrImage.getLayoutParams();
params.width = squareSize;
params.height = squareSize;
_qrImage.setLayoutParams(params);
}
generateQR();
_qrImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
_deviceBrightness = getSystemBrightness();
_qrImage.setOnClickListener(v -> {
if (!_isMaxBrightnessSet) {
setBrightness(1f);
_isMaxBrightnessSet = true;
} else {
setBrightness(_deviceBrightness);
_isMaxBrightnessSet = false;
}
});
}
private float getSystemBrightness() {
int brightness = 0;
try {
brightness = Settings.System.getInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS);
} catch (Settings.SettingNotFoundException e) {
e.printStackTrace();
}
return brightness / 255f;
}
private void setBrightness(float brightnessAmount) {
WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.screenBrightness = brightnessAmount;
getWindow().setAttributes(attrs);
}
@Override
@ -151,16 +204,13 @@ public class TransferEntriesActivity extends AegisActivity {
_entriesCount.setText(getResources().getQuantityString(R.plurals.qr_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
@ColorInt int backgroundColor = Color.WHITE;
if (_themeHelper.getConfiguredTheme() == Theme.LIGHT) {
TypedValue typedValue = new TypedValue();
getTheme().resolveAttribute(androidx.appcompat.R.attr.background, typedValue, true);
backgroundColor = typedValue.data;
}
int backgroundColor = _themeHelper.getConfiguredTheme() == Theme.LIGHT
? MaterialColors.getColor(_qrImage, com.google.android.material.R.attr.colorSurfaceContainer)
: Color.WHITE;
Bitmap bitmap;
try {
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), 512, 512, backgroundColor);
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), _qrImage.getWidth(), _qrImage.getWidth(), backgroundColor);
} catch (WriterException | GoogleAuthInfoException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
return;

View file

@ -19,6 +19,8 @@ import android.widget.EditText;
import android.widget.ListView;
import android.widget.NumberPicker;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
@ -27,6 +29,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
@ -554,7 +557,7 @@ public class Dialogs {
.setView(view)
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(context.getString(R.string.import_partial_export_anyway, entries), (dialog, which) -> {
.setPositiveButton(context.getResources().getQuantityString(R.plurals.import_partial_export_anyway, entries, entries), (dialog, which) -> {
dismissHandler.onClick(dialog, which);
})
.setNegativeButton(android.R.string.cancel, null);
@ -577,6 +580,54 @@ public class Dialogs {
showSecureDialog(dialog);
}
public static void showBackupsVersioningStrategy(Context context, BackupsVersioningStrategy currentStrategy, BackupsVersioningStrategyListener listener) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_backups_versioning_strategy, null);
RadioGroup radioGroup = view.findViewById(R.id.radio_group);
RadioButton keepXVersionsButton = view.findViewById(R.id.keep_x_versions_button);
RadioButton singleBackupButton = view.findViewById(R.id.single_backup_button);
TextView warningText = view.findViewById(R.id.warning_text);
CheckBox riskAccept = view.findViewById(R.id.risk_accept);
final AtomicReference<Button> positiveButtonRef = new AtomicReference<>();
radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
Button positiveButton = positiveButtonRef.get();
if (positiveButton != null) {
positiveButton.setEnabled(checkedId == keepXVersionsButton.getId());
}
int visibility = checkedId == singleBackupButton.getId() ? View.VISIBLE : View.GONE;
warningText.setVisibility(visibility);
riskAccept.setVisibility(visibility);
});
riskAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
Button positiveButton = positiveButtonRef.get();
if (positiveButton != null) {
positiveButton.setEnabled(isChecked);
}
});
AlertDialog alertDialog = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.pref_backups_versioning_strategy_dialog_title)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int checkedId = radioGroup.getCheckedRadioButtonId();
if (checkedId == keepXVersionsButton.getId()) {
listener.onStrategySelectionResult(BackupsVersioningStrategy.MULTIPLE_BACKUPS);
} else if (checkedId == singleBackupButton.getId()) {
listener.onStrategySelectionResult(BackupsVersioningStrategy.SINGLE_BACKUP);
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
alertDialog.setOnShowListener(dialog -> {
Button positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
positiveButtonRef.set(positiveButton);
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
radioGroup.check(keepXVersionsButton.getId());
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
radioGroup.check(singleBackupButton.getId());
}
});
showSecureDialog(alertDialog);
}
private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) {
if (isDirect) {
view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName()));
@ -605,4 +656,8 @@ public class Dialogs {
public interface ImporterListener {
void onImporterSelectionResult(DatabaseImporter.Definition definition);
}
public interface BackupsVersioningStrategyListener {
void onStrategySelectionResult(BackupsVersioningStrategy strategy);
}
}

View file

@ -77,7 +77,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
});
Preference langPreference = requirePreference("pref_lang");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
String[] langs = getResources().getStringArray(R.array.pref_lang_values);
String[] langNames = getResources().getStringArray(R.array.pref_lang_entries);
List<String> langList = Arrays.asList(langs);
@ -125,6 +125,11 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
return true;
});
Preference showExpirationStatePreference = requirePreference("pref_expiration_state");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
showExpirationStatePreference.setSummary(getString(R.string.pref_expiration_state_fallback));
}
String[] codeGroupings = getResources().getStringArray(R.array.pref_code_groupings_values);
String[] codeGroupingNames = getResources().getStringArray(R.array.pref_code_groupings);
Preference codeDigitGroupingPreference = requirePreference("pref_code_group_size_string");

View file

@ -6,6 +6,9 @@ import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.LinearLayoutManager;
@ -58,6 +61,17 @@ public class AuditLogPreferencesFragment extends Fragment {
_auditLogRecyclerView.setAdapter(_adapter);
_auditLogRecyclerView.setNestedScrollingEnabled(false);
ViewCompat.setOnApplyWindowInsetsListener(_auditLogRecyclerView, (targetView, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
targetView.setPadding(
0,
0,
0,
insets.bottom
);
return WindowInsetsCompat.CONSUMED;
});
entries.observe(getViewLifecycleOwner(), entries1 -> {
_noAuditLogsView.setVisibility(entries1.isEmpty() ? View.VISIBLE : View.GONE);

View file

@ -17,9 +17,11 @@ import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.color.MaterialColors;
@ -27,6 +29,7 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
private SwitchPreferenceCompat _androidBackupsPreference;
private SwitchPreferenceCompat _backupsPreference;
private SwitchPreferenceCompat _backupReminderPreference;
private Preference _versioningStrategyPreference;
private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference;
@ -75,7 +78,13 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsPreference = requirePreference("pref_backups");
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if ((boolean) newValue) {
selectBackupsLocation();
Dialogs.showBackupsVersioningStrategy(requireContext(), BackupsVersioningStrategy.MULTIPLE_BACKUPS, strategy -> {
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
});
} else {
_prefs.setIsBackupsEnabled(false);
updateBackupPreference();
@ -100,6 +109,24 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return false;
});
_versioningStrategyPreference = requirePreference("pref_versioning_strategy");
updateBackupsVersioningStrategySummary();
_versioningStrategyPreference.setOnPreferenceClickListener(preference -> {
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
Dialogs.showBackupsVersioningStrategy(requireContext(), currentStrategy, strategy -> {
if (strategy == currentStrategy) {
return;
}
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
});
return true;
});
_androidBackupsPreference = requirePreference("pref_android_backups");
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_prefs.setIsAndroidBackupsEnabled((boolean) newValue);
@ -110,13 +137,15 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return false;
});
Uri backupLocation = _prefs.getBackupsLocation();
_backupsLocationPreference = requirePreference("pref_backups_location");
if (backupLocation != null) {
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
}
updateBackupsLocationSummary();
_backupsLocationPreference.setOnPreferenceClickListener(preference -> {
selectBackupsLocation();
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
return false;
});
@ -158,9 +187,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_prefs.setBackupsLocation(uri);
_prefs.setIsBackupsEnabled(true);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference();
scheduleBackup();
updateBackupsVersioningStrategySummary();
updateBackupsLocationSummary();
}
private void updateBackupPreference() {
@ -174,9 +204,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsPreference.setChecked(backupEnabled);
_backupsPreference.setEnabled(encrypted);
_backupReminderPreference.setChecked(backupReminderEnabled);
_versioningStrategyPreference.setVisible(backupEnabled);
_backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled && _prefs.getBackupVersioningStrategy() != BackupsVersioningStrategy.SINGLE_BACKUP);
if (backupEnabled) {
updateBackupStatus(_builtinBackupStatusPreference, _prefs.getBuiltInBackupResult());
}
@ -221,6 +252,14 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return spannable;
}
private void createBackupFile() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/json")
.putExtra(Intent.EXTRA_TITLE, VaultBackupManager.FILENAME_SINGLE);
_vaultManager.fireIntentLauncher(this, intent, backupsResultLauncher);
}
private void selectBackupsLocation() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
@ -241,6 +280,30 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
}
}
private void updateBackupsVersioningStrategySummary() {
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_keep_x_versions);
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_single_backup);
}
}
private void updateBackupsLocationSummary() {
Uri backupsLocation = _prefs.getBackupsLocation();
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
String text;
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
text = getString(R.string.pref_backups_location_summary);
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
text = getString(R.string.pref_backup_location_summary);
} else {
return;
}
String summary = String.format("%s: %s", text, Uri.decode(backupsLocation.toString()));
_backupsLocationPreference.setSummary(summary);
}
private void updateBackupsVersionsSummary() {
int count = _prefs.getBackupsVersionCount();
if (count == Preferences.BACKUPS_VERSIONS_INFINITE) {

View file

@ -6,6 +6,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.animation.Animation;
import android.widget.LinearLayout;
import android.widget.TextView;
@ -14,6 +15,9 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -67,6 +71,19 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
fab.setOnClickListener(v -> startImportIconPack());
_fabScrollHelper = new FabScrollHelper(fab);
final MarginLayoutParams fabInitialMargin = (MarginLayoutParams) fab.getLayoutParams();
ViewCompat.setOnApplyWindowInsetsListener(fab, (targetView, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
MarginLayoutParams marginParams = (MarginLayoutParams) targetView.getLayoutParams();
marginParams.leftMargin = fabInitialMargin.leftMargin + insets.left;
marginParams.bottomMargin = fabInitialMargin.bottomMargin + insets.bottom;
marginParams.rightMargin = fabInitialMargin.rightMargin + insets.right;
targetView.setLayoutParams(marginParams);
return WindowInsetsCompat.CONSUMED;
});
_noIconPacksView = view.findViewById(R.id.vEmptyList);
((TextView) view.findViewById(R.id.txt_no_icon_packs)).setMovementMethod(LinkMovementMethod.getInstance());
_adapter = new IconPackAdapter(this);
@ -107,15 +124,7 @@ public class IconPacksManagerFragment extends Fragment implements IconPackAdapte
.setMessage(R.string.remove_icon_pack_description)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
try {
_iconPackManager.removeIconPack(pack);
} catch (IconPackException e) {
e.printStackTrace();
Dialogs.showErrorDialog(requireContext(), R.string.icon_pack_delete_error, e);
return;
}
_adapter.removeIconPack(pack);
updateEmptyState();
removeIconPack(pack);
})
.setNegativeButton(android.R.string.no, null)
.create());

View file

@ -22,6 +22,7 @@ import androidx.core.content.FileProvider;
import androidx.preference.Preference;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.GroupPlaceholderType;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
@ -190,7 +191,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
checkBoxExportAllGroups.setVisibility(View.VISIBLE);
ArrayList<VaultGroupModel> groupsArray = new ArrayList<>();
groupsArray.add(new VaultGroupModel(getString(R.string.no_group)));
groupsArray.add(new VaultGroupModel(requireContext(), GroupPlaceholderType.NO_GROUP));
groupsArray.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
groupsSelection.setCheckedItemsCountTextRes(R.plurals.export_groups_selected_count);
@ -295,7 +296,7 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
boolean encrypt = checkBoxEncrypt.isChecked();
try {
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, encrypt);
file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir());
file = new File(getExportCacheDir(), fileInfo.toString());
} catch (IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(requireContext(), R.string.exporting_vault_error, e);
@ -517,11 +518,10 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
file = File.createTempFile(VaultRepository.FILENAME_PREFIX_EXPORT + "-", ".json", getExportCacheDir());
outStream = new FileOutputStream(file);
cb.exportVault(outStream);
new ExportTask(requireContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri));
} catch (VaultRepositoryException | IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(requireContext(), R.string.exporting_vault_error, e);
return;
} finally {
try {
if (outStream != null) {
@ -531,6 +531,8 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
e.printStackTrace();
}
}
new ExportTask(requireContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri));
}, _exportFilter);
_exportFilter = null;
}

View file

@ -1,13 +1,20 @@
package com.beemdevelopment.aegis.ui.fragments.preferences;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.view.animation.Animation;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
@ -61,6 +68,23 @@ public abstract class PreferencesFragment extends PreferenceFragmentCompat {
return super.onCreateAnimation(transit, enter, nextAnim);
}
@NonNull
@Override
public RecyclerView onCreateRecyclerView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent, @Nullable Bundle savedInstanceState) {
RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState);
ViewCompat.setOnApplyWindowInsetsListener(recyclerView, (targetView, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
targetView.setPadding(
0,
0,
0,
insets.bottom
);
return WindowInsetsCompat.CONSUMED;
});
return recyclerView;
}
protected boolean saveAndBackupVault() {
try {
_vaultManager.saveAndBackup();

View file

@ -2,6 +2,10 @@ package com.beemdevelopment.aegis.ui.models;
import android.view.View;
import com.google.common.hash.HashCode;
import java.util.Objects;
public class ErrorCardInfo {
private final String _message;
private final View.OnClickListener _listener;
@ -18,4 +22,23 @@ public class ErrorCardInfo {
public View.OnClickListener getListener() {
return _listener;
}
@Override
public int hashCode() {
return HashCode.fromString(_message).asInt();
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof ErrorCardInfo)) {
return false;
}
// This equality check purposefully ignores the onclick listener
ErrorCardInfo info = (ErrorCardInfo) o;
return Objects.equals(getMessage(), info.getMessage());
}
}

View file

@ -1,23 +1,31 @@
package com.beemdevelopment.aegis.ui.models;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.GroupPlaceholderType;
import com.beemdevelopment.aegis.vault.VaultGroup;
import java.io.Serializable;
import java.util.UUID;
public class VaultGroupModel implements Serializable {
private final VaultGroup _group;
private final String _placeholderName;
private final GroupPlaceholderType _placeholderType;
private final String _placeholderText;
public VaultGroupModel(VaultGroup group) {
_group = group;
_placeholderName = null;
_placeholderText = null;
_placeholderType = null;
}
public VaultGroupModel(String placeholderName) {
public VaultGroupModel(Context context, GroupPlaceholderType placeholderType) {
_group = null;
_placeholderName = placeholderName;
_placeholderType = placeholderType;
_placeholderText = context.getString(placeholderType.getStringRes());
}
public VaultGroup getGroup() {
@ -25,11 +33,15 @@ public class VaultGroupModel implements Serializable {
}
public String getName() {
return _group != null ? _group.getName() : _placeholderName;
return _group != null ? _group.getName() : _placeholderText;
}
public GroupPlaceholderType getPlaceholderType() {
return _placeholderType;
}
public boolean isPlaceholder() {
return _group == null;
return _placeholderType != null;
}
@Nullable

View file

@ -1,17 +1,14 @@
package com.beemdevelopment.aegis.ui.preferences;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import androidx.annotation.RequiresApi;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
public class SwitchPreference extends SwitchPreferenceCompat {
private Preference.OnPreferenceChangeListener _listener;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

View file

@ -34,6 +34,8 @@ public class ExportTask extends ProgressDialogTask<ExportTask.Params, Exception>
return null;
} catch (IOException e) {
return e;
} finally {
boolean ignored = params.getFile().delete();
}
}

View file

@ -0,0 +1,63 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class IconOptimizationTask extends ProgressDialogTask<Map<UUID, VaultEntryIcon>, Map<UUID, VaultEntryIcon>> {
private final Callback _cb;
public IconOptimizationTask(Context context, Callback cb) {
super(context, context.getString(R.string.optimizing_icon));
_cb = cb;
}
@Override
protected Map<UUID, VaultEntryIcon> doInBackground(Map<UUID, VaultEntryIcon>... params) {
Map<UUID, VaultEntryIcon> res = new HashMap<>();
Context context = getDialog().getContext();
int i = 0;
Map<UUID, VaultEntryIcon> icons = params[0];
for (Map.Entry<UUID, VaultEntryIcon> entry : icons.entrySet()) {
if (icons.size() > 1) {
publishProgress(context.getString(R.string.optimizing_icon_multiple, i + 1, icons.size()));
}
i++;
VaultEntryIcon oldIcon = entry.getValue();
if (oldIcon == null || oldIcon.getType().equals(IconType.SVG)) {
continue;
}
if (BitmapHelper.isVaultEntryIconOptimized(oldIcon)) {
continue;
}
Bitmap bitmap = BitmapFactory.decodeByteArray(oldIcon.getBytes(), 0, oldIcon.getBytes().length);
VaultEntryIcon newIcon = BitmapHelper.toVaultEntryIcon(bitmap, oldIcon.getType());
bitmap.recycle();
res.put(entry.getKey(), newIcon);
}
return res;
}
@Override
protected void onPostExecute(Map<UUID, VaultEntryIcon> results) {
super.onPostExecute(results);
_cb.onTaskFinished(results);
}
public interface Callback {
void onTaskFinished(Map<UUID, VaultEntryIcon> results);
}
}

View file

@ -3,6 +3,11 @@ package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
@ -31,6 +36,15 @@ public class PBKDFTask extends ProgressDialogTask<PBKDFTask.Params, SecretKey> {
public static SecretKey deriveKey(Params params) {
try {
// Some older versions of Android (< 26) do not support PBKDF2withHmacSHA512, so use
// BouncyCastle's implementation instead.
if (params.getAlgorithm().equals("PBKDF2withHmacSHA512")) {
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
gen.init(CryptoUtils.toBytes(params.getPassword()), params.getSalt(), params.getIterations());
byte[] key = ((KeyParameter) gen.generateDerivedParameters(params.getKeySize())).getKey();
return new SecretKeySpec(key, "AES");
}
SecretKeyFactory factory = SecretKeyFactory.getInstance(params.getAlgorithm());
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), params.getKeySize());
SecretKey key = factory.generateSecret(spec);

View file

@ -15,6 +15,8 @@ import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.AccountNamePosition;
@ -35,6 +37,7 @@ import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
@ -49,8 +52,7 @@ import java.util.UUID;
public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ItemTouchHelperAdapter {
private EntryListView _view;
private List<VaultEntry> _entries;
private List<VaultEntry> _shownEntries;
private EntryList _entryList;
private List<VaultEntry> _selectedEntries;
private Collection<VaultGroup> _groups;
private Map<UUID, Integer> _usageCounts;
@ -60,6 +62,8 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
private Preferences.CodeGrouping _codeGroupSize;
private AccountNamePosition _accountNamePosition;
private boolean _showIcon;
private boolean _showNextCode;
private boolean _showExpirationState;
private boolean _onlyShowNecessaryAccountNames;
private boolean _highlightEntry;
private boolean _tempHighlightEntry;
@ -76,14 +80,12 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
private Handler _dimHandler;
private Handler _doubleTapHandler;
private boolean _pauseFocused;
private ErrorCardInfo _errorCardInfo;
// keeps track of the EntryHolders that are currently bound
private List<EntryHolder> _holders;
public EntryAdapter(EntryListView view) {
_entries = new ArrayList<>();
_shownEntries = new ArrayList<>();
_entryList = new EntryList();
_selectedEntries = new ArrayList<>();
_groupFilter = new TreeSet<>();
_holders = new ArrayList<>();
@ -115,6 +117,14 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
_showIcon = showIcon;
}
public void setShowNextCode(boolean showNextCode) {
_showNextCode = showNextCode;
}
public void setShowExpirationState(boolean showExpirationState) {
_showExpirationState = showExpirationState;
}
public void setTapToReveal(boolean tapToReveal) {
_tapToReveal = tapToReveal;
}
@ -140,173 +150,45 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
public void setErrorCardInfo(ErrorCardInfo info) {
ErrorCardInfo oldInfo = _errorCardInfo;
_errorCardInfo = info;
if (oldInfo == null && info != null) {
notifyItemInserted(0);
} else if (oldInfo != null && info == null) {
notifyItemRemoved(0);
} else {
notifyItemChanged(0);
if (Objects.equals(info, _entryList.getErrorCardInfo())) {
return;
}
replaceEntryList(new EntryList(
_entryList.getEntries(),
_entryList.getShownEntries(),
info
));
}
public VaultEntry getEntryAtPos(int position) {
return _shownEntries.get(translateEntryPosToIndex(position));
public VaultEntry getEntryAtPosition(int position) {
return _entryList.getShownEntries().get(_entryList.translateEntryPosToIndex(position));
}
public int addEntry(VaultEntry entry) {
_entries.add(entry);
if (isEntryFiltered(entry)) {
return -1;
}
int position = -1;
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
if (comparator != null) {
// insert the entry in the correct order
// note: this assumes that _shownEntries has already been sorted
for (int i = getShownFavoritesCount(); i < _shownEntries.size(); i++) {
if (comparator.compare(_shownEntries.get(i), entry) > 0) {
_shownEntries.add(i, entry);
position = translateEntryIndexToPos(i);
notifyItemInserted(position);
break;
}
}
}
if (position < 0) {
_shownEntries.add(entry);
position = translateEntryIndexToPos(getShownEntriesCount() - 1);
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
_view.onListChange();
checkPeriodUniformity();
updateFooter();
return position;
public int getEntryPosition(VaultEntry entry) {
return _entryList.translateEntryIndexToPos(_entryList.getShownEntries().indexOf(entry));
}
public void addEntries(Collection<VaultEntry> entries) {
for (VaultEntry entry: entries) {
public void setEntries(List<VaultEntry> entries) {
// TODO: Move these fields to separate dedicated model for the UI
for (VaultEntry entry : entries) {
entry.setUsageCount(_usageCounts.containsKey(entry.getUUID()) ? _usageCounts.get(entry.getUUID()) : 0);
entry.setLastUsedTimestamp(_lastUsedTimestamps.containsKey(entry.getUUID()) ? _lastUsedTimestamps.get(entry.getUUID()) : 0);
}
_entries.addAll(entries);
updateShownEntries();
checkPeriodUniformity(true);
}
public void removeEntry(VaultEntry entry) {
_entries.remove(entry);
if (_shownEntries.contains(entry)) {
int index = _shownEntries.indexOf(entry);
_shownEntries.remove(index);
int position = translateEntryIndexToPos(index);
notifyItemRemoved(position);
updateFooter();
}
_view.onListChange();
checkPeriodUniformity();
}
public void removeEntry(UUID uuid) {
VaultEntry entry = getEntryByUUID(uuid);
removeEntry(entry);
replaceEntryList(new EntryList(
entries,
calculateShownEntries(entries),
_entryList.getErrorCardInfo()
));
}
public void clearEntries() {
_entries.clear();
_shownEntries.clear();
notifyDataSetChanged();
checkPeriodUniformity();
replaceEntryList(new EntryList());
}
public void replaceEntry(UUID uuid, VaultEntry newEntry) {
VaultEntry oldEntry = getEntryByUUID(uuid);
_entries.set(_entries.indexOf(oldEntry), newEntry);
if (_shownEntries.contains(oldEntry)) {
int index = _shownEntries.indexOf(oldEntry);
int position = translateEntryIndexToPos(index);
if (isEntryFiltered(newEntry)) {
_shownEntries.remove(index);
notifyItemRemoved(position);
} else {
_shownEntries.set(index, newEntry);
notifyItemChanged(position);
}
sortShownEntries();
int newIndex = _shownEntries.indexOf(newEntry);
int newPosition = translateEntryIndexToPos(newIndex);
if (newPosition != NO_POSITION && position != newPosition) {
notifyItemMoved(position, newPosition);
}
} else if (!isEntryFiltered(newEntry)) {
// NOTE: This logic is wrong, because sorting is not taken into account. This code
// path is currently never hit though, because it is not possible to edit an entry
// that is not shown.
_shownEntries.add(newEntry);
int position = getItemCount() - 1;
notifyItemInserted(position);
}
checkPeriodUniformity();
updateFooter();
}
private VaultEntry getEntryByUUID(UUID uuid) {
for (VaultEntry entry : _entries) {
if (entry.getUUID().equals(uuid)) {
return entry;
}
}
return null;
}
/**
* Translates the given entry position in the recycler view, to its index in the shown entries list.
*/
public int translateEntryPosToIndex(int position) {
if (position == NO_POSITION) {
return NO_POSITION;
}
if (isErrorCardShown()) {
position -= 1;
}
return position;
}
/**
* Translates the given entry index in the shown entries list, to its position in the recycler view.
*/
private int translateEntryIndexToPos(int index) {
if (index == NO_POSITION) {
return NO_POSITION;
}
if (isErrorCardShown()) {
index += 1;
}
return index;
return _entryList.translateEntryPosToIndex(position);
}
private boolean isEntryFiltered(VaultEntry entry) {
@ -315,6 +197,19 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
String name = entry.getName().toLowerCase();
String note = entry.getNote().toLowerCase();
if (_searchFilter != null) {
String[] tokens = _searchFilter.toLowerCase().split("\\s+");
// Return true if not all tokens match at least one of the relevant fields
return !Arrays.stream(tokens)
.allMatch(token ->
((_searchBehaviorMask & Preferences.SEARCH_IN_ISSUER) != 0 && issuer.contains(token)) ||
((_searchBehaviorMask & Preferences.SEARCH_IN_NAME) != 0 && name.contains(token)) ||
((_searchBehaviorMask & Preferences.SEARCH_IN_NOTE) != 0 && note.contains(token)) ||
((_searchBehaviorMask & Preferences.SEARCH_IN_GROUPS) != 0 && doesAnyGroupMatchSearchFilter(groups, token))
);
}
if (!_groupFilter.isEmpty()) {
if (groups.isEmpty() && !_groupFilter.contains(null)) {
return true;
@ -324,14 +219,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
}
if (_searchFilter == null) {
return false;
}
return ((_searchBehaviorMask & Preferences.SEARCH_IN_ISSUER) == 0 || !issuer.contains(_searchFilter))
&& ((_searchBehaviorMask & Preferences.SEARCH_IN_NAME) == 0 || !name.contains(_searchFilter))
&& ((_searchBehaviorMask & Preferences.SEARCH_IN_NOTE) == 0 || !note.contains(_searchFilter))
&& ((_searchBehaviorMask & Preferences.SEARCH_IN_GROUPS) == 0 || !doesAnyGroupMatchSearchFilter(entry.getGroups(), _searchFilter));
return false;
}
private boolean doesAnyGroupMatchSearchFilter(Set<UUID> entryGroupUUIDs, String searchFilter) {
@ -343,7 +231,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
public void refresh(boolean hard) {
if (hard) {
updateShownEntries();
refreshEntryList();
} else {
for (EntryHolder holder : _holders) {
holder.refresh();
@ -358,8 +246,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
_groupFilter = groups;
updateShownEntries();
checkPeriodUniformity();
refreshEntryList();
}
public void setSortCategory(SortCategory category, boolean apply) {
@ -369,7 +256,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
_sortCategory = category;
if (apply) {
updateShownEntries();
refreshEntryList();
}
}
@ -378,25 +265,59 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
public void setSearchFilter(String search) {
_searchFilter = (search != null && !search.isEmpty()) ? search.toLowerCase().trim() : null;
updateShownEntries();
String newSearchFilter = (search != null && !search.isEmpty())
? search.toLowerCase().trim() : null;
if (!Objects.equals(_searchFilter, newSearchFilter)) {
_searchFilter = newSearchFilter;
refreshEntryList();
}
}
private void updateShownEntries() {
// clear the list of shown entries first
_shownEntries.clear();
private void refreshEntryList() {
replaceEntryList(new EntryList(
_entryList.getEntries(),
calculateShownEntries(_entryList.getEntries()),
_entryList.getErrorCardInfo()
));
}
// add entries back that are not filtered out
for (VaultEntry entry : _entries) {
private void replaceEntryList(EntryList newEntryList) {
DiffUtil.DiffResult diffRes = DiffUtil.calculateDiff(new DiffCallback(_entryList, newEntryList));
_entryList = newEntryList;
updatePeriodUniformity();
// This scroll position trick is required in order to not have the recycler view
// jump to some random position after a large change (like resorting entries)
// Related: https://issuetracker.google.com/issues/70149059
int scrollPos = _view.getScrollPosition();
diffRes.dispatchUpdatesTo(this);
_view.scrollToPosition(scrollPos);
_view.onListChange();
}
private List<VaultEntry> calculateShownEntries(List<VaultEntry> entries) {
List<VaultEntry> res = new ArrayList<>();
for (VaultEntry entry : entries) {
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
res.add(entry);
}
}
sortShownEntries();
checkPeriodUniformity();
_view.onListChange();
notifyDataSetChanged();
sortEntries(res, _sortCategory);
return res;
}
private static void sortEntries(List<VaultEntry> entries, SortCategory sortCategory) {
if (sortCategory != null) {
Comparator<VaultEntry> comparator = sortCategory.getComparator();
if (comparator != null) {
Collections.sort(entries, comparator);
}
}
Comparator<VaultEntry> favoriteComparator = new FavoriteComparator();
Collections.sort(entries, favoriteComparator);
}
private boolean isEntryDraggable(VaultEntry entry) {
@ -407,18 +328,6 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
&& _selectedEntries.get(0) == entry;
}
private void sortShownEntries() {
if (_sortCategory != null) {
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
if (comparator != null) {
Collections.sort(_shownEntries, comparator);
}
}
Comparator<VaultEntry> favoriteComparator = new FavoriteComparator();
Collections.sort(_shownEntries, favoriteComparator);
}
public void setViewMode(ViewMode viewMode) {
_viewMode = viewMode;
}
@ -434,7 +343,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
public Map<UUID, Long> getLastUsedTimestamps() { return _lastUsedTimestamps; }
public int getShownFavoritesCount() {
return (int) _shownEntries.stream().filter(VaultEntry::isFavorite).count();
return (int) _entryList.getShownEntries().stream().filter(VaultEntry::isFavorite).count();
}
@Override
@ -446,43 +355,48 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
public void onItemDrop(int position) {
// moving entries is not allowed when a filter is applied
// footer cant be moved, nor can items be moved below it
if (!_groupFilter.isEmpty() || isPositionFooter(position) || isPositionErrorCard(position)) {
if (!_groupFilter.isEmpty() || _entryList.isPositionFooter(position) || _entryList.isPositionErrorCard(position)) {
return;
}
int index = translateEntryPosToIndex(position);
_view.onEntryDrop(_shownEntries.get(index));
int index = _entryList.translateEntryPosToIndex(position);
_view.onEntryDrop(_entryList.getShownEntries().get(index));
}
@Override
public void onItemMove(int firstPosition, int secondPosition) {
// moving entries is not allowed when a filter is applied
// footer cant be moved, nor can items be moved below it
// Moving entries is not allowed when a filter is applied. The footer can't be
// moved, nor can items be moved below it
if (!_groupFilter.isEmpty()
|| isPositionFooter(firstPosition) || isPositionFooter(secondPosition)
|| isPositionErrorCard(firstPosition) || isPositionErrorCard(secondPosition)) {
|| _entryList.isPositionFooter(firstPosition) || _entryList.isPositionFooter(secondPosition)
|| _entryList.isPositionErrorCard(firstPosition) || _entryList.isPositionErrorCard(secondPosition)) {
return;
}
// notify the vault first
int firstIndex = translateEntryPosToIndex(firstPosition);
int secondIndex = translateEntryPosToIndex(secondPosition);
_view.onEntryMove(_entries.get(firstIndex), _entries.get(secondIndex));
// Notify the vault about the entry position change first
int firstIndex = _entryList.translateEntryPosToIndex(firstPosition);
int secondIndex = _entryList.translateEntryPosToIndex(secondPosition);
VaultEntry firstEntry = _entryList.getShownEntries().get(firstIndex);
VaultEntry secondEntry = _entryList.getShownEntries().get(secondIndex);
_view.onEntryMove(firstEntry, secondEntry);
// then update our end
CollectionUtils.move(_entries, firstIndex, secondIndex);
CollectionUtils.move(_shownEntries, firstIndex, secondIndex);
notifyItemMoved(firstPosition, secondPosition);
// Then update the visual end
List<VaultEntry> newEntries = new ArrayList<>(_entryList.getEntries());
CollectionUtils.move(newEntries, newEntries.indexOf(firstEntry), newEntries.indexOf(secondEntry));
replaceEntryList(new EntryList(
newEntries,
calculateShownEntries(newEntries),
_entryList.getErrorCardInfo()
));
}
@Override
public int getItemViewType(int position) {
if (isPositionErrorCard(position)) {
if (_entryList.isPositionErrorCard(position)) {
return R.layout.card_error;
}
if (isPositionFooter(position)) {
if (_entryList.isPositionFooter(position)) {
return R.layout.card_footer;
}
@ -497,7 +411,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
View view = inflater.inflate(viewType, parent, false);
if (viewType == R.layout.card_error) {
holder = new ErrorCardHolder(view, _errorCardInfo);
holder = new ErrorCardHolder(view, Objects.requireNonNull(_entryList.getErrorCardInfo()));
} else if (viewType == R.layout.card_footer) {
holder = new FooterView(view);
} else {
@ -523,23 +437,23 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
if (holder instanceof EntryHolder) {
EntryHolder entryHolder = (EntryHolder) holder;
int index = translateEntryPosToIndex(position);
VaultEntry entry = _shownEntries.get(index);
int index = _entryList.translateEntryPosToIndex(position);
VaultEntry entry = _entryList.getShownEntries().get(index);
boolean hidden = _tapToReveal && entry != _focusedEntry;
boolean paused = _pauseFocused && entry == _focusedEntry;
boolean dimmed = (_highlightEntry || _tempHighlightEntry) && _focusedEntry != null && _focusedEntry != entry;
boolean hidden = _tapToReveal && !entry.equals(_focusedEntry);
boolean paused = _pauseFocused && entry.equals(_focusedEntry);
boolean dimmed = (_highlightEntry || _tempHighlightEntry) && _focusedEntry != null && !_focusedEntry.equals(entry);
boolean showProgress = entry.getInfo() instanceof TotpInfo && ((TotpInfo) entry.getInfo()).getPeriod() != getMostFrequentPeriod();
boolean showAccountName = true;
if (_onlyShowNecessaryAccountNames) {
// Only show account name when there's multiple entries found with the same issuer.
showAccountName = _entries.stream()
showAccountName = _entryList.getEntries().stream()
.filter(x -> x.getIssuer().equals(entry.getIssuer()))
.count() > 1;
}
AccountNamePosition accountNamePosition = showAccountName ? _accountNamePosition : AccountNamePosition.HIDDEN;
entryHolder.setData(entry, _codeGroupSize, _viewMode, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed);
entryHolder.setData(entry, _codeGroupSize, _viewMode, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed, _showExpirationState, _showNextCode);
entryHolder.setFocused(_selectedEntries.contains(entry));
entryHolder.setShowDragHandle(isEntryDraggable(entry));
@ -554,13 +468,13 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
if (_selectedEntries.isEmpty()) {
if (_highlightEntry || _tempHighlightEntry || _tapToReveal) {
if (_focusedEntry == entry) {
if (_focusedEntry != null && _focusedEntry.equals(entry)) {
resetFocus();
// Prevent copying when singletap is set and focus is reset
handled = _copyBehavior == CopyBehavior.SINGLETAP;
} else {
focusEntry(entry, _tapToRevealTime);
// Prevent copying when singletap is set and the entry is being revealed
handled = _copyBehavior == CopyBehavior.SINGLETAP && _tapToReveal;
}
}
@ -611,8 +525,8 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
entryHolder.setFocusedAndAnimate(true);
}
int index = translateEntryPosToIndex(position);
boolean returnVal = _view.onLongEntryClick(_shownEntries.get(index));
int index = _entryList.translateEntryPosToIndex(position);
boolean returnVal = _view.onLongEntryClick(_entryList.getShownEntries().get(index));
if (_selectedEntries.size() == 0 || isEntryDraggable(entry)) {
_view.startDrag(entryHolder);
}
@ -658,15 +572,10 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
}
private void checkPeriodUniformity() {
checkPeriodUniformity(false);
}
private void checkPeriodUniformity(boolean force) {
private void updatePeriodUniformity() {
int mostFrequentPeriod = getMostFrequentPeriod();
boolean uniform = isPeriodUniform();
if (!force && uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) {
if (uniform == _isPeriodUniform && mostFrequentPeriod == _uniformPeriod) {
return;
}
@ -684,7 +593,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
public int getMostFrequentPeriod() {
List<TotpInfo> infos = new ArrayList<>();
for (VaultEntry entry : _shownEntries) {
for (VaultEntry entry : _entryList.getShownEntries()) {
OtpInfo info = entry.getInfo();
if (info instanceof TotpInfo) {
infos.add((TotpInfo) info);
@ -726,7 +635,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
_dimHandler.removeCallbacksAndMessages(null);
for (EntryHolder holder : _holders) {
if (holder.getEntry() != _focusedEntry) {
if (!holder.getEntry().equals(_focusedEntry)) {
if (_highlightEntry || _tempHighlightEntry) {
holder.dim();
}
@ -799,9 +708,9 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
public List<VaultEntry> selectAllEntries() {
_selectedEntries.clear();
for (VaultEntry entry: _shownEntries) {
for (VaultEntry entry: _entryList.getShownEntries()) {
for (EntryHolder holder: _holders) {
if (holder.getEntry() == entry) {
if (holder.getEntry().equals(entry)) {
holder.setFocused(true);
}
}
@ -816,7 +725,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
public void deselectAllEntries() {
for (VaultEntry entry: _selectedEntries) {
for (EntryHolder holder : _holders) {
if (holder.getEntry() == entry) {
if (holder.getEntry().equals(entry)) {
holder.setFocusedAndAnimate(false);
break;
}
@ -853,34 +762,23 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
@Override
public int getItemCount() {
// Always at least one item because of the footer
// Two in case there's also an error card
int baseCount = 1;
if (isErrorCardShown()) {
baseCount++;
}
return baseCount + getShownEntriesCount();
return _entryList.getItemCount();
}
public int getShownEntriesCount() {
return _shownEntries.size();
return _entryList.getShownEntries().size();
}
public boolean isPositionFooter(int position) {
return position == (getItemCount() - 1);
return _entryList.isPositionFooter(position);
}
public boolean isPositionErrorCard(int position) {
return isErrorCardShown() && position == 0;
return _entryList.isPositionErrorCard(position);
}
public boolean isErrorCardShown() {
return _errorCardInfo != null;
}
private void updateFooter() {
notifyItemChanged(getItemCount() - 1);
return _entryList.isErrorCardShown();
}
private class FooterView extends RecyclerView.ViewHolder {
@ -907,6 +805,151 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
}
private static class EntryList {
private final List<VaultEntry> _entries;
private final List<VaultEntry> _shownEntries;
private final ErrorCardInfo _errorCardInfo;
public EntryList() {
this(new ArrayList<>(), new ArrayList<>(), null);
}
public EntryList(
@NonNull List<VaultEntry> entries,
@NonNull List<VaultEntry> shownEntries,
@Nullable ErrorCardInfo errorCardInfo
) {
_entries = entries;
_shownEntries = shownEntries;
_errorCardInfo = errorCardInfo;
}
public List<VaultEntry> getEntries() {
return _entries;
}
public List<VaultEntry> getShownEntries() {
return _shownEntries;
}
public int getItemCount() {
// Always at least one item because of the footer
// Two in case there's also an error card
int baseCount = 1;
if (isErrorCardShown()) {
baseCount++;
}
return baseCount + getShownEntries().size();
}
@Nullable
public ErrorCardInfo getErrorCardInfo() {
return _errorCardInfo;
}
public boolean isErrorCardShown() {
return _errorCardInfo != null;
}
public boolean isPositionErrorCard(int position) {
return isErrorCardShown() && position == 0;
}
public boolean isPositionFooter(int position) {
return position == (getItemCount() - 1);
}
/**
* Translates the given entry position in the recycler view, to its index in the shown entries list.
*/
public int translateEntryPosToIndex(int position) {
if (position == NO_POSITION) {
return NO_POSITION;
}
if (isErrorCardShown()) {
position -= 1;
}
return position;
}
/**
* Translates the given entry index in the shown entries list, to its position in the recycler view.
*/
public int translateEntryIndexToPos(int index) {
if (index == NO_POSITION) {
return NO_POSITION;
}
if (isErrorCardShown()) {
index += 1;
}
return index;
}
}
private static class DiffCallback extends DiffUtil.Callback {
private final EntryList _old;
private final EntryList _new;
public DiffCallback(EntryList oldList, EntryList newList) {
_old = oldList;
_new = newList;
}
@Override
public int getOldListSize() {
return _old.getItemCount();
}
@Override
public int getNewListSize() {
return _new.getItemCount();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
if (_old.isPositionErrorCard(oldItemPosition) != _new.isPositionErrorCard(newItemPosition)
|| _old.isPositionFooter(oldItemPosition) != _new.isPositionFooter(newItemPosition)) {
return false;
}
if ((_old.isPositionFooter(oldItemPosition) && _new.isPositionFooter(newItemPosition))
|| (_old.isPositionErrorCard(oldItemPosition) && _new.isPositionErrorCard(newItemPosition))) {
return true;
}
int oldEntryIndex = _old.translateEntryPosToIndex(oldItemPosition);
int newEntryIndex = _new.translateEntryPosToIndex(newItemPosition);
if (oldEntryIndex < 0 || newEntryIndex < 0) {
return false;
}
return _old.getShownEntries().get(oldEntryIndex).getUUID()
.equals(_new.getShownEntries().get(newEntryIndex).getUUID());
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
if (_old.isPositionFooter(oldItemPosition) && _new.isPositionFooter(newItemPosition)) {
return _old.getShownEntries().size() == _new.getShownEntries().size();
}
if (_old.isPositionErrorCard(oldItemPosition) && _new.isPositionErrorCard(newItemPosition)) {
return Objects.equals(_old.getErrorCardInfo(), _new.getErrorCardInfo());
}
int oldEntryIndex = _old.translateEntryPosToIndex(oldItemPosition);
int newEntryIndex = _new.translateEntryPosToIndex(newItemPosition);
return _old.getShownEntries().get(oldEntryIndex)
.equals(_new.getShownEntries().get(newEntryIndex));
}
}
public interface Listener {
void onEntryClick(VaultEntry entry);
boolean onLongEntryClick(VaultEntry entry);

View file

@ -1,7 +1,12 @@
package com.beemdevelopment.aegis.ui.views;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.text.Spannable;
import android.text.SpannableString;
@ -35,6 +40,7 @@ import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Glide;
import com.google.android.material.card.MaterialCardView;
import com.google.android.material.color.MaterialColors;
public class EntryHolder extends RecyclerView.ViewHolder {
private static final float DEFAULT_ALPHA = 1.0f;
@ -44,6 +50,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private View _favoriteIndicator;
private TextView _profileName;
private TextView _profileCode;
private TextView _nextProfileCode;
private TextView _profileIssuer;
private TextView _profileCopied;
private ImageView _profileDrawable;
@ -52,21 +59,23 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private RelativeLayout _description;
private ImageView _dragHandle;
private ViewMode _viewMode;
private final ImageView _selected;
private final Handler _selectedHandler;
private Preferences.CodeGrouping _codeGrouping = Preferences.CodeGrouping.NO_GROUPING;
private AccountNamePosition _accountNamePosition = AccountNamePosition.HIDDEN;
private boolean _hidden;
private boolean _paused;
private TotpProgressBar _progressBar;
private MaterialCardView _view;
private UiRefresher _refresher;
private Handler _animationHandler;
private Handler _copyAnimationHandler;
private Handler _expirationHandler;
private AnimatorSet _expirationAnimSet;
private boolean _showNextCode;
private boolean _showExpirationState;
private Animation _scaleIn;
private Animation _scaleOut;
@ -77,6 +86,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_view = (MaterialCardView) view;
_profileName = view.findViewById(R.id.profile_account_name);
_profileCode = view.findViewById(R.id.profile_code);
_nextProfileCode = view.findViewById(R.id.next_profile_code);
_profileIssuer = view.findViewById(R.id.profile_issuer);
_profileCopied = view.findViewById(R.id.profile_copied);
_description = view.findViewById(R.id.description);
@ -86,8 +96,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_dragHandle = view.findViewById(R.id.drag_handle);
_favoriteIndicator = view.findViewById(R.id.favorite_indicator);
_selectedHandler = new Handler();
_animationHandler = new Handler();
_copyAnimationHandler = new Handler();
_expirationHandler = new Handler();
_progressBar = view.findViewById(R.id.progressBar);
@ -97,24 +107,31 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
public void onRefresh() {
if (!_hidden && !_paused) {
refreshCode();
}
refreshCode();
}
@Override
public void onExpiring() { }
@Override
public long getMillisTillNextRefresh() {
return ((TotpInfo) _entry.getInfo()).getMillisTillNextRotation();
}
@Override
public long getPeriodMillis() {
return ((TotpInfo) _entry.getInfo()).getPeriod() * 1000L;
}
});
}
public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed) {
public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean nonUniform, boolean hidden, boolean paused, boolean dimmed, boolean showExpirationState, boolean showNextCode) {
_entry = entry;
_hidden = hidden;
_paused = paused;
_codeGrouping = groupSize;
_viewMode = viewMode;
_accountNamePosition = accountNamePosition;
if (viewMode.equals(ViewMode.TILES) && _accountNamePosition == AccountNamePosition.END) {
_accountNamePosition = AccountNamePosition.BELOW;
@ -122,16 +139,19 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_selected.clearAnimation();
_selected.setVisibility(View.GONE);
_selectedHandler.removeCallbacksAndMessages(null);
_animationHandler.removeCallbacksAndMessages(null);
_copyAnimationHandler.removeCallbacksAndMessages(null);
_expirationHandler.removeCallbacksAndMessages(null);
_showNextCode = entry.getInfo() instanceof TotpInfo && showNextCode;
_showExpirationState = _entry.getInfo() instanceof TotpInfo && showExpirationState;
_favoriteIndicator.setVisibility(_entry.isFavorite() ? View.VISIBLE : View.INVISIBLE);
// only show the progress bar if there is no uniform period and the entry type is TotpInfo
setShowProgress(showProgress);
setShowProgress(nonUniform);
// only show the button if this entry is of type HotpInfo
_buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE);
_nextProfileCode.setVisibility(_showNextCode ? View.VISIBLE : View.GONE);
String profileIssuer = entry.getIssuer();
String profileName = entry.getName();
@ -149,7 +169,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
showIcon(showIcon);
itemView.setAlpha(dimmed ? DIMMED_ALPHA : DEFAULT_ALPHA);
}
@ -277,15 +296,24 @@ public class EntryHolder extends RecyclerView.ViewHolder {
public void refreshCode() {
if (!_hidden && !_paused) {
updateCode();
updateCodes();
startExpirationAnimation();
}
}
private void updateCode() {
private void updateCodes() {
_profileCode.setText(getOtp());
if (_showNextCode) {
_nextProfileCode.setText(getOtp(1));
}
}
private String getOtp() {
return getOtp(0);
}
private String getOtp(int offset) {
OtpInfo info = _entry.getInfo();
// In previous versions of Aegis, it was possible to import entries with an empty
@ -294,7 +322,12 @@ public class EntryHolder extends RecyclerView.ViewHolder {
// the OTP, instead of crashing.
String otp;
try {
otp = info.getOtp();
if (info instanceof TotpInfo) {
otp = ((TotpInfo)info).getOtp((System.currentTimeMillis() / 1000) + ((long) (offset) * ((TotpInfo) _entry.getInfo()).getPeriod()));
} else {
otp = info.getOtp();
}
if (!(info instanceof SteamInfo || info instanceof YandexInfo)) {
otp = formatCode(otp);
}
@ -335,14 +368,18 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
public void revealCode() {
updateCode();
updateCodes();
startExpirationAnimation();
_hidden = false;
}
public void hideCode() {
String code = getOtp();
String hiddenText = code.replaceAll("\\S", Character.toString(HIDDEN_CHAR));
stopExpirationAnimation();
updateTextViewWithDots(_profileCode, hiddenText, code);
updateTextViewWithDots(_nextProfileCode, hiddenText, code);
_hidden = true;
}
@ -356,6 +393,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
float dotsWidth = paint.measureText(hiddenCode);
float scaleFactor = codeWidth / dotsWidth;
scaleFactor = (float)(Math.round(scaleFactor * 10.0) / 10.0);
textView.setTextColor(MaterialColors.getColor(textView, R.attr.colorCodeHidden));
// If scale is higher or equal to 0.8, do nothing and proceed with the normal text rendering
if (scaleFactor >= 0.8) {
@ -387,6 +425,73 @@ public class EntryHolder extends RecyclerView.ViewHolder {
textView.setText(dotsString);
}
public void startExpirationAnimation() {
stopExpirationAnimation();
if (!_showExpirationState) {
return;
}
final int totalStateDuration = 7000;
TotpInfo info = (TotpInfo) _entry.getInfo();
if (info.getPeriod() * 1000 <= totalStateDuration) {
_profileCode.setTextColor(MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError));
return;
}
// Workaround for when animations are disabled or Android version being too old
float durationScale = AnimationsHelper.Scale.ANIMATOR.getValue(itemView.getContext());
if (durationScale == 0.0 || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
int color = MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError);
if (info.getMillisTillNextRotation() < totalStateDuration) {
_profileCode.setTextColor(color);
} else {
_expirationHandler.postDelayed(() -> {
_profileCode.setTextColor(color);
}, info.getMillisTillNextRotation() - totalStateDuration);
}
return;
}
final int colorShiftDuration = 300;
long delayAnimDuration = info.getPeriod() * 1000L - totalStateDuration - colorShiftDuration;
ValueAnimator delayAnim = ValueAnimator.ofFloat(0f, 0f);
delayAnim.setDuration((long) (delayAnimDuration / durationScale));
int colorFrom = _profileCode.getCurrentTextColor();
int colorTo = MaterialColors.getColor(_profileCode, com.google.android.material.R.attr.colorError);
ValueAnimator colorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
colorAnim.setDuration((long) (colorShiftDuration / durationScale));
colorAnim.addUpdateListener(a -> _profileCode.setTextColor((int) a.getAnimatedValue()));
final int blinkDuration = 3000;
ValueAnimator delayAnim2 = ValueAnimator.ofFloat(0f, 0f);
delayAnim2.setDuration((long) ((totalStateDuration - blinkDuration) / durationScale));
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(_profileCode, "alpha", 1f, .5f);
alphaAnim.setDuration((long) (500 / durationScale));
alphaAnim.setRepeatCount(blinkDuration / 500 - 1);
alphaAnim.setRepeatMode(ValueAnimator.REVERSE);
_expirationAnimSet = new AnimatorSet();
_expirationAnimSet.playSequentially(delayAnim, colorAnim, delayAnim2, alphaAnim);
_expirationAnimSet.start();
long currentPlayTime = (info.getPeriod() * 1000L) - info.getMillisTillNextRotation();
_expirationAnimSet.setCurrentPlayTime((long) (currentPlayTime / durationScale));
}
private void stopExpirationAnimation() {
_expirationHandler.removeCallbacksAndMessages(null);
if (_expirationAnimSet != null) {
_expirationAnimSet.cancel();
_expirationAnimSet = null;
}
int colorTo = MaterialColors.getColor(_profileCode, R.attr.colorCode);
_profileCode.setTextColor(colorTo);
_profileCode.setAlpha(1f);
}
public void showIcon(boolean show) {
if (show) {
_profileDrawable.setVisibility(View.VISIBLE);
@ -402,8 +507,11 @@ public class EntryHolder extends RecyclerView.ViewHolder {
public void setPaused(boolean paused) {
_paused = paused;
if (!_hidden && !_paused) {
updateCode();
if (_paused) {
stopExpirationAnimation();
} else if (!_hidden) {
updateCodes();
startExpirationAnimation();
}
}
@ -416,7 +524,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
public void animateCopyText() {
_animationHandler.removeCallbacksAndMessages(null);
_copyAnimationHandler.removeCallbacksAndMessages(null);
Animation slideDownFadeIn = AnimationsHelper.loadScaledAnimation(itemView.getContext(), R.anim.slide_down_fade_in);
Animation slideDownFadeOut = AnimationsHelper.loadScaledAnimation(itemView.getContext(), R.anim.slide_down_fade_out);
@ -429,7 +537,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
View fadeOutView = (_accountNamePosition == AccountNamePosition.BELOW) ? _profileName : _description;
fadeOutView.startAnimation(slideDownFadeOut);
_animationHandler.postDelayed(() -> {
_copyAnimationHandler.postDelayed(() -> {
_profileCopied.startAnimation(fadeOut);
fadeOutView.startAnimation(fadeIn);
}, 3000);
@ -439,7 +547,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_profileCopied.startAnimation(fadeIn);
visibleProfileText.startAnimation(fadeOut);
_animationHandler.postDelayed(() -> {
_copyAnimationHandler.postDelayed(() -> {
_profileCopied.startAnimation(fadeOut);
visibleProfileText.startAnimation(fadeIn);
}, 3000);

View file

@ -13,15 +13,15 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.LayoutAnimationController;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.widget.NestedScrollView;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.ItemTouchHelper;
@ -33,17 +33,16 @@ import com.beemdevelopment.aegis.CopyBehavior;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.VibrationPatterns;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.helpers.VibrationHelper;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.bumptech.glide.Glide;
@ -51,29 +50,25 @@ import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.card.MaterialCardView;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.shape.CornerFamily;
import com.google.android.material.shape.ShapeAppearanceModel;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class EntryListView extends Fragment implements EntryAdapter.Listener {
private EntryAdapter _adapter;
private Listener _listener;
private SimpleItemTouchHelperCallback _touchCallback;
private ItemTouchHelper _touchHelper;
private VibrationHelper _vibrationHelper;
private RecyclerView _recyclerView;
private RecyclerView.ItemDecoration _itemDecoration;
@ -81,11 +76,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private TotpProgressBar _progressBar;
private boolean _showProgress;
private ViewMode _viewMode;
private Collection<VaultGroup> _groups;
private LinearLayout _emptyStateView;
private Chip _groupChip;
private Set<UUID> _groupFilter;
private Set<UUID> _prefGroupFilter;
private UiRefresher _refresher;
@ -106,8 +97,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
_progressBar = view.findViewById(R.id.progressBar);
_groupChip = view.findViewById(R.id.chip_group);
initializeGroupChip();
_vibrationHelper = new VibrationHelper(getContext());
// set up the recycler view
_recyclerView = view.findViewById(R.id.rvKeyProfiles);
@ -157,12 +147,40 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
@Override
public void onRefresh() {
refresh(false);
_vibrationHelper.vibratePattern(getContext(), VibrationPatterns.REFRESH_CODE);
}
@Override
public void onExpiring() {
_vibrationHelper.vibratePattern(getContext(), VibrationPatterns.EXPIRING);
}
@Override
public long getMillisTillNextRefresh() {
return TotpInfo.getMillisTillNextRotation(_adapter.getMostFrequentPeriod());
}
@Override
public long getPeriodMillis() {
return _adapter.getMostFrequentPeriod() * 1000L;
}
});
final int rvInitialPaddingLeft = _recyclerView.getPaddingLeft();
final int rvInitialPaddingTop = _recyclerView.getPaddingTop();
final int rvInitialPaddingRight = _recyclerView.getPaddingRight();
final int rvInitialPaddingBottom = _recyclerView.getPaddingBottom();
ViewCompat.setOnApplyWindowInsetsListener(_recyclerView, (targetView, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
// left and right padding seems to be handled by fitsSystemWindows="true" on the CoordinatorLayout in activity_main.xml
targetView.setPadding(
rvInitialPaddingLeft,
rvInitialPaddingTop,
rvInitialPaddingRight,
rvInitialPaddingBottom + insets.bottom
);
return WindowInsetsCompat.CONSUMED;
});
_emptyStateView = view.findViewById(R.id.vEmptyList);
@ -173,27 +191,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_preloadSizeProvider.setView(view);
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState == null) {
return;
}
HashSet<UUID> filter = (HashSet<UUID>) savedInstanceState.getSerializable("prefGroupFilter");
if (filter != null) {
_prefGroupFilter = filter;
}
public int getScrollPosition() {
return ((LinearLayoutManager) _recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
// user can apply _groupFilter without saving
// restore _groupFilter as _prefGroupFilter in order to reapply correct filter after screen rotate
if (_groupFilter != null) {
outState.putSerializable("prefGroupFilter", new HashSet<>(_groupFilter));
}
public void scrollToPosition(int position) {
_recyclerView.getLayoutManager().scrollToPosition(position);
}
@Override
@ -202,16 +205,26 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
super.onDestroyView();
}
public void setGroupFilter(Set<UUID> groups, boolean animate) {
_groupFilter = groups;
public void onRefreshStop() {
_refresher.stop();
}
public void onRefreshStart() {
if (_adapter.getMostFrequentPeriod() != -1){
_refresher.start();
}
}
public void setGroups(Collection<VaultGroup> groups) {
_adapter.setGroups(groups);
updateDividerDecoration();
}
public void setGroupFilter(Set<UUID> groups) {
_adapter.setGroupFilter(groups);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
updateEmptyState();
updateGroupChip();
if (animate) {
runEntriesAnimation();
}
updateDividerDecoration();
}
public void setIsLongPressDragEnabled(boolean enabled) {
@ -244,10 +257,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
public void setSortCategory(SortCategory sortCategory, boolean apply) {
_adapter.setSortCategory(sortCategory, apply);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
if (apply) {
runEntriesAnimation();
}
updateDividerDecoration();
}
public void setUsageCounts(Map<UUID, Integer> usageCounts) {
@ -269,8 +279,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
public void setSearchFilter(String search) {
_adapter.setSearchFilter(search);
_touchCallback.setIsLongPressDragEnabled(_adapter.isDragAndDropAllowed());
updateEmptyState();
updateDividerDecoration();
}
public void setSelectedEntry(VaultEntry entry) {
@ -369,11 +379,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_progressBar.setVisibility(View.VISIBLE);
_progressBar.setPeriod(period);
_progressBar.start();
_refresher.start();
onRefreshStart();
} else {
_progressBar.setVisibility(View.GONE);
_progressBar.stop();
_refresher.stop();
onRefreshStop();
}
}
@ -384,10 +394,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
}
public void setPrefGroupFilter(Set<UUID> groupFilter) {
_prefGroupFilter = groupFilter;
}
public void setCodeGroupSize(Preferences.CodeGrouping codeGrouping) {
_adapter.setCodeGroupSize(codeGrouping);
}
@ -404,6 +410,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_adapter.setShowIcon(showIcon);
}
public void setShowNextCode(boolean showNextCode) {
_adapter.setShowNextCode(showNextCode);
}
public void setShowExpirationState(boolean showExpirationState) {
_adapter.setShowExpirationState(showExpirationState);
}
public void setHighlightEntry(boolean highlightEntry) {
_adapter.setHighlightEntry(highlightEntry);
}
@ -424,61 +438,57 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_adapter.setErrorCardInfo(info);
}
public void addEntry(VaultEntry entry) {
addEntry(entry, false);
}
@SuppressLint("ClickableViewAccessibility")
public void addEntry(VaultEntry entry, boolean focusEntry) {
int position = _adapter.addEntry(entry);
updateEmptyState();
public void onEntryAdded(VaultEntry entry) {
int position = _adapter.getEntryPosition(entry);
if (position < 0) {
return;
}
LinearLayoutManager layoutManager = (LinearLayoutManager) _recyclerView.getLayoutManager();
if (focusEntry && position >= 0) {
if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition())
|| (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) {
boolean smoothScroll = !AnimationsHelper.Scale.TRANSITION.isZero(requireContext());
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
private void handleScroll() {
_recyclerView.removeOnScrollListener(this);
_recyclerView.setOnTouchListener(null);
tempHighlightEntry(entry);
}
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (smoothScroll && newState == RecyclerView.SCROLL_STATE_IDLE) {
handleScroll();
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (!smoothScroll) {
handleScroll();
}
}
};
_recyclerView.addOnScrollListener(scrollListener);
_recyclerView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
_recyclerView.removeOnScrollListener(scrollListener);
_recyclerView.stopScroll();
_recyclerView.setOnTouchListener(null);
}
return false;
});
// We can't easily control the speed of the smooth scroll animation, but we
// can at least disable it if animations are disabled
if (smoothScroll) {
_recyclerView.smoothScrollToPosition(position);
} else {
_recyclerView.scrollToPosition(position);
if ((_recyclerView.canScrollVertically(1) && position > layoutManager.findLastCompletelyVisibleItemPosition())
|| (_recyclerView.canScrollVertically(-1) && position < layoutManager.findFirstCompletelyVisibleItemPosition())) {
boolean smoothScroll = !AnimationsHelper.Scale.TRANSITION.isZero(requireContext());
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
private void handleScroll() {
_recyclerView.removeOnScrollListener(this);
_recyclerView.setOnTouchListener(null);
tempHighlightEntry(entry);
}
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (smoothScroll && newState == RecyclerView.SCROLL_STATE_IDLE) {
handleScroll();
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (!smoothScroll) {
handleScroll();
}
}
};
_recyclerView.addOnScrollListener(scrollListener);
_recyclerView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
_recyclerView.removeOnScrollListener(scrollListener);
_recyclerView.stopScroll();
_recyclerView.setOnTouchListener(null);
}
return false;
});
// We can't easily control the speed of the smooth scroll animation, but we
// can at least disable it if animations are disabled
if (smoothScroll) {
_recyclerView.smoothScrollToPosition(position);
} else {
tempHighlightEntry(entry);
_recyclerView.scrollToPosition(position);
}
} else {
tempHighlightEntry(entry);
}
}
@ -489,27 +499,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_adapter.focusEntry(entry, secondsToFocus);
}
public void addEntries(Collection<VaultEntry> entries) {
_adapter.addEntries(entries);
updateEmptyState();
}
public void removeEntry(VaultEntry entry) {
_adapter.removeEntry(entry);
updateEmptyState();
}
public void removeEntry(UUID uuid) {
_adapter.removeEntry(uuid);
public void setEntries(Collection<VaultEntry> entries) {
_adapter.setEntries(new ArrayList<>(entries));
updateEmptyState();
}
public void clearEntries() {
_adapter.clearEntries();
}
public void replaceEntry(UUID uuid, VaultEntry newEntry) {
_adapter.replaceEntry(uuid, newEntry);
updateEmptyState();
}
public void runEntriesAnimation() {
@ -519,116 +516,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_recyclerView.scheduleLayoutAnimation();
}
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) {
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
chip.setText(group.getName());
chip.setCheckable(true);
chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID()));
chip.setCheckedIconVisible(true);
chip.setOnCheckedChangeListener((group1, checkedId) -> {
Set<UUID> groupFilter = getGroupFilter(chipGroup);
setGroupFilter(groupFilter, true);
});
chip.setTag(group);
chipGroup.addView(chip);
}
private void initializeGroupChip() {
View view = getLayoutInflater().inflate(R.layout.dialog_select_groups, null);
BottomSheetDialog dialog = new BottomSheetDialog(requireContext());
NestedScrollView scrollView = view.findViewById(R.id.scrollView);
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) scrollView.getLayoutParams();
layoutParams.matchConstraintMaxHeight = getResources().getConfiguration().screenHeightDp;
dialog.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED);
dialog.getBehavior().setSkipCollapsed(false);
dialog.setContentView(view);
ChipGroup chipGroup = view.findViewById(R.id.groupChipGroup);
Button clearButton = view.findViewById(R.id.btnClear);
Button saveButton = view.findViewById(R.id.btnSave);
clearButton.setOnClickListener(v -> {
chipGroup.clearCheck();
Set<UUID> groupFilter = Collections.emptySet();
if (_listener != null) {
_listener.onSaveGroupFilter(groupFilter);
}
setGroupFilter(groupFilter, true);
dialog.dismiss();
});
saveButton.setOnClickListener(v -> {
Set<UUID> groupFilter = getGroupFilter(chipGroup);
if (_listener != null) {
_listener.onSaveGroupFilter(groupFilter);
}
setGroupFilter(groupFilter, true);
dialog.dismiss();
});
_groupChip.setOnClickListener(v -> {
chipGroup.removeAllViews();
for (VaultGroup group : _groups) {
addChipTo(chipGroup, new VaultGroupModel(group));
}
addChipTo(chipGroup, new VaultGroupModel(getString(R.string.no_group)));
Dialogs.showSecureDialog(dialog);
});
}
private static Set<UUID> getGroupFilter(ChipGroup chipGroup) {
return chipGroup.getCheckedChipIds().stream()
.map(i -> {
Chip chip = chipGroup.findViewById(i);
VaultGroupModel group = (VaultGroupModel) chip.getTag();
return group.getUUID();
})
.collect(Collectors.toSet());
}
private void updateGroupChip() {
if (_groupFilter.isEmpty()) {
_groupChip.setText(R.string.groups);
} else {
_groupChip.setText(String.format("%s (%d)", getString(R.string.groups), _groupFilter.size()));
}
}
private void setShowProgress(boolean showProgress) {
_showProgress = showProgress;
updateDividerDecoration();
}
public void setGroups(Collection<VaultGroup> groups) {
_groups = groups;
_adapter.setGroups(groups);
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
updateDividerDecoration();
if (_prefGroupFilter != null) {
Set<UUID> groupFilter = cleanGroupFilter(_prefGroupFilter);
_prefGroupFilter = null;
if (!groupFilter.isEmpty()) {
setGroupFilter(groupFilter, false);
}
} else if (_groupFilter != null) {
Set<UUID> groupFilter = cleanGroupFilter(_groupFilter);
if (!_groupFilter.equals(groupFilter)) {
setGroupFilter(groupFilter, true);
}
}
}
private Set<UUID> cleanGroupFilter(Set<UUID> groupFilter) {
Set<UUID> groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet());
return groupFilter.stream()
.filter(g -> g == null || groupUuids.contains(g))
.collect(Collectors.toSet());
}
private void updateDividerDecoration() {
if (_itemDecoration != null) {
_recyclerView.removeItemDecoration(_itemDecoration);
@ -705,15 +597,15 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
int entryIndex = _adapter.translateEntryPosToIndex(adapterPosition);
// The first entry should have a top margin, but only if the group chip is not shown and the error card is not shown
if (entryIndex == 0 && (_groups == null || _groups.isEmpty()) && !_adapter.isErrorCardShown()) {
// The first entry should have a top margin, but only if the error card is not shown
if (entryIndex == 0 && !_adapter.isErrorCardShown()) {
outRect.top = _offset;
}
// Only non-favorite entries have a bottom margin, except for the final favorite entry
int totalFavorites = _adapter.getShownFavoritesCount();
if (totalFavorites == 0
|| (entryIndex < _adapter.getShownEntriesCount() && !_adapter.getEntryAtPos(adapterPosition).isFavorite())
|| (entryIndex < _adapter.getShownEntriesCount() && !_adapter.getEntryAtPosition(adapterPosition).isFavorite())
|| totalFavorites == entryIndex + 1) {
outRect.bottom = _offset;
}
@ -806,7 +698,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
return Collections.emptyList();
}
VaultEntry entry = _adapter.getEntryAtPos(position);
VaultEntry entry = _adapter.getEntryAtPosition(position);
if (!entry.hasIcon()) {
return Collections.emptyList();
}

View file

@ -7,12 +7,14 @@ import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
import com.beemdevelopment.aegis.util.CollectionUtils;
import com.beemdevelopment.aegis.vault.VaultGroup;
import java.util.ArrayList;
import java.util.UUID;
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> implements ItemTouchHelperAdapter {
private GroupAdapter.Listener _listener;
private ArrayList<VaultGroup> _groups;
@ -32,6 +34,10 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
}
}
public ArrayList<VaultGroup> getGroups() {
return _groups;
}
public void replaceGroup(UUID uuid, VaultGroup newGroup) {
VaultGroup oldGroup = getGroupByUUID(uuid);
int position = _groups.indexOf(oldGroup);
@ -64,6 +70,18 @@ public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
});
}
@Override
public void onItemMove(int firstPosition, int secondPosition) {
CollectionUtils.move(_groups, firstPosition, secondPosition);
notifyItemMoved(firstPosition, secondPosition);
}
@Override
public void onItemDismiss(int position) { }
@Override
public void onItemDrop(int position) { }
private VaultGroup getGroupByUUID(UUID uuid) {
for (VaultGroup group : _groups) {
if (group.getUUID().equals(uuid)) {

View file

@ -35,8 +35,10 @@ public class IconPackAdapter extends RecyclerView.Adapter<IconPackHolder> {
public void removeIconPack(IconPack pack) {
int position = _iconPacks.indexOf(pack);
_iconPacks.remove(position);
notifyItemRemoved(position);
if (position >= 0) {
_iconPacks.remove(position);
notifyItemRemoved(position);
}
}
@NonNull

View file

@ -2,14 +2,11 @@ package com.beemdevelopment.aegis.ui.views;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.animation.LinearInterpolator;
import android.widget.ProgressBar;
import androidx.annotation.RequiresApi;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.otp.TotpInfo;
@ -30,7 +27,6 @@ public class TotpProgressBar extends ProgressBar {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TotpProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

View file

@ -15,6 +15,7 @@ public class Vault {
private static final int VERSION = 3;
private final UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private final UUIDMap<VaultGroup> _groups = new UUIDMap<>();
private boolean _iconsOptimized = true;
// Whether we've migrated the group list to the new format while parsing the vault
private boolean _isGroupsMigrationFresh = false;
@ -42,6 +43,7 @@ public class Vault {
obj.put("version", VERSION);
obj.put("entries", entriesArray);
obj.put("groups", groupsArray);
obj.put("icons_optimized", _iconsOptimized);
return obj;
} catch (JSONException e) {
@ -86,6 +88,10 @@ public class Vault {
entries.add(entry);
}
if (!obj.optBoolean("icons_optimized")) {
vault.setIconsOptimized(false);
}
} catch (VaultEntryException | JSONException e) {
throw new VaultException(e);
}
@ -101,6 +107,14 @@ public class Vault {
return _isGroupsMigrationFresh;
}
public void setIconsOptimized(boolean optimized) {
_iconsOptimized = optimized;
}
public boolean areIconsOptimized() {
return _iconsOptimized;
}
public boolean migrateOldGroup(VaultEntry entry) {
if (entry.getOldGroup() != null) {
Optional<VaultGroup> optGroup = getGroups().getValues()

View file

@ -1,5 +1,6 @@
package com.beemdevelopment.aegis.vault;
import android.content.ContentResolver;
import android.content.Context;
import android.content.UriPermission;
import android.net.Uri;
@ -9,6 +10,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.util.IOUtils;
@ -38,6 +40,7 @@ public class VaultBackupManager {
new StrictDateFormat("yyyyMMdd-HHmmss", Locale.ENGLISH);
public static final String FILENAME_PREFIX = "aegis-backup";
public static final String FILENAME_SINGLE = String.format("%s.json", FILENAME_PREFIX);
private final Context _context;
private final Preferences _prefs;
@ -51,10 +54,10 @@ public class VaultBackupManager {
_auditLogRepository = auditLogRepository;
}
public void scheduleBackup(File tempFile, Uri dirUri, int versionsToKeep) {
public void scheduleBackup(File tempFile, BackupsVersioningStrategy strategy, Uri uri, int versionsToKeep) {
_executor.execute(() -> {
try {
createBackup(tempFile, dirUri, versionsToKeep);
createBackup(tempFile, strategy, uri, versionsToKeep);
_auditLogRepository.addBackupCreatedEvent();
_prefs.setBuiltInBackupResult(new Preferences.BackupResult(null));
} catch (VaultRepositoryException | VaultBackupPermissionException e) {
@ -64,6 +67,46 @@ public class VaultBackupManager {
});
}
private void createBackup(File tempFile, BackupsVersioningStrategy strategy, Uri uri, int versionsToKeep)
throws VaultRepositoryException, VaultBackupPermissionException {
if (uri == null) {
throw new VaultRepositoryException("getBackupsLocation returned null");
}
if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackup(tempFile, uri);
} else if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
createBackup(tempFile, uri, versionsToKeep);
} else {
throw new VaultRepositoryException("Invalid backups versioning strategy");
}
}
private void createBackup(File tempFile, Uri fileUri)
throws VaultRepositoryException, VaultBackupPermissionException {
Log.i(TAG, String.format("Creating backup at %s", fileUri));
try {
if (!hasPermissionsAt(fileUri)) {
throw new VaultBackupPermissionException("No persisted URI permissions");
}
ContentResolver resolver = _context.getContentResolver();
try (FileInputStream inStream = new FileInputStream(tempFile);
OutputStream outStream = resolver.openOutputStream(fileUri, "wt")
) {
if (outStream == null) {
throw new IOException("openOutputStream returned null");
}
IOUtils.copy(inStream, outStream);
} catch (IOException exception) {
throw new VaultRepositoryException(exception);
}
} catch (VaultRepositoryException | VaultBackupPermissionException exception) {
Log.e(TAG, String.format("Unable to create backup: %s", exception));
throw exception;
} finally {
tempFile.delete();
}
}
private void createBackup(File tempFile, Uri dirUri, int versionsToKeep)
throws VaultRepositoryException, VaultBackupPermissionException {
FileInfo fileInfo = new FileInfo(FILENAME_PREFIX);

View file

@ -103,8 +103,14 @@ public class VaultEntry extends UUIDMap.Value {
entry.setOldGroup(JsonUtils.optString(obj, "group"));
}
VaultEntryIcon icon = VaultEntryIcon.fromJson(obj);
entry.setIcon(icon);
// Silently ignore any errors that occur when trying to parse the icon of an
// entry. This allows us to introduce new icon types in the future (e.g. WebP)
// without breaking compatibility with older versions of Aegis.
try {
VaultEntryIcon icon = VaultEntryIcon.fromJson(obj);
entry.setIcon(icon);
} catch (VaultEntryIconException ignored) {
}
return entry;
} catch (OtpInfoException | JSONException e) {
@ -233,6 +239,10 @@ public class VaultEntry extends UUIDMap.Value {
&& getGroups().equals(entry.getGroups());
}
public boolean hasSameNameAndIssuer(VaultEntry entry) {
return getName().equals(entry.getName()) && getIssuer().equals(entry.getIssuer());
}
/**
* Reports whether this entry has its values set to the defaults.
*/

View file

@ -1,5 +1,8 @@
package com.beemdevelopment.aegis.vault;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
@ -7,8 +10,6 @@ import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.google.common.hash.HashCode;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
@ -23,21 +24,23 @@ public class VaultEntryIcon implements Serializable {
private final byte[] _hash;
private final IconType _type;
public VaultEntryIcon(byte @NonNull [] bytes, @NonNull IconType type) {
public static final int MAX_DIMENS = 512;
public VaultEntryIcon(@NonNull byte[] bytes, @NonNull IconType type) {
this(bytes, type, generateHash(bytes, type));
}
VaultEntryIcon(byte @NonNull [] bytes, @NonNull IconType type, byte @NonNull [] hash) {
VaultEntryIcon(@NonNull byte[] bytes, @NonNull IconType type, @NonNull byte[] hash) {
_bytes = bytes;
_hash = hash;
_type = type;
}
public byte @NonNull [] getBytes() {
public @NonNull byte[] getBytes() {
return _bytes;
}
public byte @NonNull [] getHash() {
public @NonNull byte[] getHash() {
return _hash;
}
@ -70,7 +73,7 @@ public class VaultEntryIcon implements Serializable {
}
@Nullable
static VaultEntryIcon fromJson(@NonNull JSONObject obj) throws VaultEntryException {
static VaultEntryIcon fromJson(@NonNull JSONObject obj) throws VaultEntryIconException {
try {
Object icon = obj.get("icon");
if (icon == JSONObject.NULL) {
@ -80,7 +83,7 @@ public class VaultEntryIcon implements Serializable {
String mime = JsonUtils.optString(obj, "icon_mime");
IconType iconType = mime == null ? IconType.JPEG : IconType.fromMimeType(mime);
if (iconType == IconType.INVALID) {
throw new VaultEntryException(String.format("Bad icon MIME type: %s", mime));
throw new VaultEntryIconException(String.format("Bad icon MIME type: %s", mime));
}
byte[] iconBytes = Base64.decode((String) icon);
@ -92,11 +95,11 @@ public class VaultEntryIcon implements Serializable {
return new VaultEntryIcon(iconBytes, iconType);
} catch (JSONException | EncodingException e) {
throw new VaultEntryException(e);
throw new VaultEntryIconException(e);
}
}
private static byte @NonNull [] generateHash(byte @NonNull [] bytes, @NonNull IconType type) {
private static @NonNull byte[] generateHash(@NonNull byte[] bytes, @NonNull IconType type) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(type.toMimeType().getBytes(StandardCharsets.UTF_8));

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.vault;
public class VaultEntryIconException extends Exception {
public VaultEntryIconException(Throwable cause) {
super(cause);
}
public VaultEntryIconException(String message) {
super(message);
}
}

View file

@ -5,12 +5,14 @@ import android.app.backup.BackupManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
@ -172,8 +174,11 @@ public class VaultManager {
try (OutputStream outStream = new FileOutputStream(tempFile)) {
_repo.export(outStream);
}
BackupsVersioningStrategy strategy = _prefs.getBackupVersioningStrategy();
Uri uri = _prefs.getBackupsLocation();
int versionsToKeep = _prefs.getBackupsVersionCount();
_backups.scheduleBackup(tempFile, _prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
_backups.scheduleBackup(tempFile, strategy, uri, versionsToKeep);
} catch (IOException e) {
throw new VaultRepositoryException(e);
}

View file

@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
import androidx.core.util.AtomicFile;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.util.IOUtils;
import com.google.zxing.WriterException;
@ -249,6 +250,13 @@ public class VaultRepository {
return _vault.getEntries().replace(entry);
}
public VaultEntry editEntry(VaultEntry entry, EntryEditor editor) {
VaultEntry newEntry = Cloner.clone(entry);
editor.edit(newEntry);
replaceEntry(newEntry);
return newEntry;
}
/**
* Moves entry1 to the position of entry2.
*/
@ -291,8 +299,11 @@ public class VaultRepository {
removeGroup(group);
}
public void renameGroup(VaultGroup renamedGroup) {
_vault.getGroups().replace(renamedGroup);
public void replaceGroups(Collection<VaultGroup> groups) {
_vault.getGroups().wipe();
for (VaultGroup group : groups) {
_vault.getGroups().add(group);
}
}
public void removeGroup(VaultGroup group) {
@ -322,6 +333,14 @@ public class VaultRepository {
return _vault.isGroupsMigrationFresh();
}
public boolean areIconsOptimized() {
return _vault.areIconsOptimized();
}
public void setIconsOptimized(boolean optimized) {
_vault.setIconsOptimized(optimized);
}
public VaultFileCredentials getCredentials() {
return _creds == null ? null : _creds.clone();
}
@ -341,4 +360,8 @@ public class VaultRepository {
return getCredentials().getSlots().findBackupPasswordSlots().size() > 0;
}
public interface EntryEditor {
void edit(VaultEntry entry);
}
}

View file

@ -37,6 +37,6 @@ message MigrationPayload {
repeated OtpParameters otp_parameters = 1;
int32 version = 2;
int32 batch_size = 3;
int32 batch_index = 4;
optional int32 batch_index = 4;
int32 batch_id = 5;
}

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorSecondary" android:state_enabled="true"/>
<item android:alpha="0.38" android:color="?attr/colorSecondary"/>
</selector>

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="@dimen/mtrl_low_ripple_pressed_alpha" android:color="?attr/colorSecondary" android:state_pressed="true"/>
<item android:alpha="@dimen/mtrl_low_ripple_focused_alpha" android:color="?attr/colorSecondary" android:state_focused="true" android:state_hovered="true"/>
<item android:alpha="@dimen/mtrl_low_ripple_focused_alpha" android:color="?attr/colorSecondary" android:state_focused="true"/>
<item android:alpha="@dimen/mtrl_low_ripple_hovered_alpha" android:color="?attr/colorSecondary" android:state_hovered="true"/>
<item android:alpha="@dimen/mtrl_low_ripple_default_alpha" android:color="?attr/colorSecondary"/>
</selector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M400,720L400,640L560,640L560,720L400,720ZM240,520L240,440L720,440L720,520L240,520ZM120,320L120,240L840,240L840,320L120,320Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m240,800 l40,-160L120,640l20,-80h160l40,-160L180,400l20,-80h160l40,-160h80l-40,160h160l40,-160h80l-40,160h160l-20,80L660,400l-40,160h160l-20,80L600,640l-40,160h-80l40,-160L360,640l-40,160h-80ZM380,560h160l40,-160L420,400l-40,160Z"
android:fillColor="#e3e3e3"/>
</vector>

View file

@ -1,9 +1,14 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/progress">
<clip>
<scale android:scaleWidth="100%">
<shape>
<solid android:color="?attr/colorPrimaryAlternative"/>
<solid android:color="?attr/colorProgressbar"/>
<corners
android:topLeftRadius="0dp"
android:topRightRadius="2dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="2dp" />
</shape>
</clip>
</scale>
</item>
</layer-list>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?attr/colorSurfaceContainerLow"/>
<corners android:radius="4dp"/>
<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp"/>
</shape>

View file

@ -7,6 +7,7 @@
android:fitsSystemWindows="true"
tools:context="com.beemdevelopment.aegis.ui.AboutActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
@ -16,6 +17,7 @@
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:id="@+id/about_scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"

View file

@ -8,6 +8,7 @@
tools:context="com.beemdevelopment.aegis.ui.AssignIconsActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">

View file

@ -5,6 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:fitsSystemWindows="true"
tools:context="com.beemdevelopment.aegis.ui.AuthActivity">
<LinearLayout
android:layout_width="match_parent"

View file

@ -8,6 +8,7 @@
android:fitsSystemWindows="true"
tools:context="com.beemdevelopment.aegis.ui.EditEntryActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
@ -98,7 +99,7 @@
android:id="@+id/text_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"/>
android:inputType="textCapSentences"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
@ -117,7 +118,7 @@
android:maxLines="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"/>
android:inputType="textCapSentences"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
@ -171,7 +172,7 @@
android:id="@+id/text_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text|textMultiLine"/>
android:inputType="text|textMultiLine|textCapSentences"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
@ -201,7 +202,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/secret"
android:inputType="textPassword"/>
android:inputType="textPassword|textMultiLine"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout

View file

@ -8,6 +8,7 @@
tools:context="com.beemdevelopment.aegis.ui.GroupManagerActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">

View file

@ -9,6 +9,7 @@
tools:context="com.beemdevelopment.aegis.ui.ImportEntriesActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
@ -22,7 +23,6 @@
android:id="@+id/list_entries"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="60dp"
android:clipToPadding="false"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -15,30 +14,48 @@
android:fitsSystemWindows="true"
app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@+id/rvKeyProfiles">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="0dp"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:scrollbars="none">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<com.google.android.material.chip.ChipGroup
android:id="@+id/groupChipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:selectionRequired="true"/>
</LinearLayout>
</HorizontalScrollView>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:orientation="vertical">
<com.google.android.material.chip.ChipGroup
android:id="@+id/groupChipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.google.android.material.chip.ChipGroup>
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<fragment
android:name="com.beemdevelopment.aegis.ui.views.EntryListView"
android:id="@+id/key_profiles"
android:layout_height="fill_parent"
android:name="com.beemdevelopment.aegis.ui.views.EntryListView"
android:layout_width="match_parent"
android:layout_height="fill_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>
@ -48,6 +65,6 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_outline_add_24" />
android:src="@drawable/ic_outline_add_24" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -6,6 +6,7 @@
android:orientation="vertical"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">

View file

@ -9,7 +9,7 @@
tools:context="com.beemdevelopment.aegis.ui.ScannerActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_layout"
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"

View file

@ -9,7 +9,7 @@
tools:context="com.beemdevelopment.aegis.ui.TransferEntriesActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_layout"
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
@ -22,62 +22,56 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layoutShareEntry"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/ivQrCode"
android:layout_width="250dp"
android:layout_height="250dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTransfer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/transfer_entry"
android:textSize="22sp"
android:textStyle="bold"
android:layout_marginBottom="5dp"
app:layout_constraintBottom_toTopOf="@+id/tvDescription"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:paddingHorizontal="30dp">
<TextView
android:id="@+id/tvDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dp"
android:text="@string/transfer_entry_description"
app:layout_constraintBottom_toTopOf="@+id/ivQrCode"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivQrCode"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.3"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Aegis.ImageView.Rounded" />
<TextView
android:id="@+id/tvIssuer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="Issuer"
android:layout_marginTop="20dp"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ivQrCode" />
app:layout_constraintTop_toBottomOf="@+id/ivQrCode"
tools:text="Issuer" />
<TextView
android:id="@+id/tvAccountName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="Accountname"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvIssuer" />
app:layout_constraintTop_toBottomOf="@+id/tvIssuer"
tools:text="Accountname" />
<Button
android:id="@+id/btnCopyClipboard"
@ -86,18 +80,19 @@
android:text="@string/copy_uri"
android:textAllCaps="false"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/tvEntriesCount"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAccountName"
app:layout_constraintBottom_toTopOf="@id/tvEntriesCount" />
app:layout_constraintVertical_bias="0.134" />
<Button
android:id="@+id/btnNext"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
style="@style/Widget.Material3.Button.TextButton"
android:text="@string/next"
android:textAllCaps="false"
app:layout_constraintBottom_toBottomOf="parent"
@ -105,11 +100,11 @@
<Button
android:id="@+id/btnPrevious"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
style="@style/Widget.Material3.Button.TextButton"
android:text="@string/previous"
android:textAllCaps="false"
android:visibility="invisible"
@ -123,9 +118,18 @@
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/btnNext"
app:layout_constraintHorizontal_bias="0.506"
app:layout_constraintStart_toStartOf="@+id/btnPrevious" />
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/transfer_entry_brightness"
app:layout_constraintBottom_toTopOf="@+id/tvEntriesCount"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnCopyClipboard" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -71,7 +71,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/description"
android:layout_alignStart="@+id/profile_code">
android:layout_alignStart="@+id/profile_codes_layout">
<TextView
android:layout_width="wrap_content"
@ -109,23 +109,47 @@
android:textSize="16sp"
android:visibility="invisible" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layoutDirection="ltr"
<LinearLayout
android:orientation="vertical"
android:layout_marginBottom="0dp"
android:paddingBottom="0dp"
android:id="@+id/profile_codes_layout"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
android:textSize="34sp"
android:textColor="?attr/colorCode"
android:layout_marginStart="6dp"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:textStyle="normal|bold"/>
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layoutDirection="ltr"
android:textSize="34sp"
android:layout_below="@id/description"
android:textColor="?attr/colorCode"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:textStyle="normal|bold"/>
<TextView
android:id="@+id/next_profile_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:paddingTop="0dp"
android:paddingStart="2dp"
android:layout_alignParentStart="true"
android:textColor="?attr/colorOnSurfaceDim"
android:textSize="20sp"
android:textStyle="normal|bold"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
tools:text="412 643"/>
</LinearLayout>
</RelativeLayout>

View file

@ -85,8 +85,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/description"
android:layout_alignStart="@+id/profile_code">
android:layout_alignStart="@+id/profile_codes_layout">
<TextView
android:layout_width="wrap_content"
@ -95,6 +94,7 @@
android:text="@string/issuer"
android:textStyle="bold"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
android:textSize="13sp"
android:ellipsize="end"
android:maxLines="1"/>
@ -112,23 +112,50 @@
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layoutDirection="ltr"
<LinearLayout
android:orientation="vertical"
android:layout_marginBottom="0dp"
android:paddingBottom="0dp"
android:id="@+id/profile_codes_layout"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
android:textSize="26sp"
android:textColor="?attr/colorCode"
android:layout_marginStart="6dp"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:textStyle="normal|bold"/>
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layoutDirection="ltr"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
android:textSize="26sp"
android:textColor="?attr/colorCode"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:textStyle="normal|bold"/>
<TextView
android:id="@+id/next_profile_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:letterSpacing="-0.01"
android:layout_marginTop="0dp"
android:paddingTop="0dp"
android:paddingStart="2dp"
android:textColor="?attr/colorOnSurfaceDim"
android:textSize="16sp"
android:textStyle="normal|bold"
android:fallbackLineSpacing="false"
android:includeFontPadding="false"
tools:text="412 643"/>
</LinearLayout>
</RelativeLayout>

View file

@ -85,7 +85,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/description"
android:layout_alignStart="@+id/profile_code">
android:layout_alignStart="@+id/profile_codes_layout">
<TextView
android:layout_width="wrap_content"
@ -110,23 +110,48 @@
tools:text=" - AccountName" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layoutDirection="ltr"
<LinearLayout
android:orientation="vertical"
android:layout_marginBottom="0dp"
android:paddingBottom="0dp"
android:id="@+id/profile_codes_layout"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
android:textSize="26sp"
android:textColor="?attr/colorCode"
android:layout_marginStart="6dp"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:textStyle="normal|bold"/>
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layoutDirection="ltr"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
android:textSize="26sp"
android:textColor="?attr/colorCode"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:textStyle="normal|bold"/>
<TextView
android:id="@+id/next_profile_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:letterSpacing="-0.01"
android:layout_marginTop="0dp"
android:paddingTop="0dp"
android:paddingStart="2dp"
android:textColor="?attr/colorOnSurfaceDim"
android:textSize="16sp"
android:textStyle="normal|bold"
android:fallbackLineSpacing="false"
android:includeFontPadding="false"
tools:text="412 643"/>
</LinearLayout>
</RelativeLayout>

View file

@ -104,22 +104,47 @@
tools:text=" - AccountName" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layoutDirection="ltr"
<LinearLayout
android:orientation="vertical"
android:layout_marginBottom="0dp"
android:paddingBottom="0dp"
android:id="@+id/profile_codes_layout"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
android:textSize="26sp"
android:textColor="?attr/colorCode"
android:layout_alignParentStart="true"
android:layout_marginTop="10dp"
android:textStyle="normal|bold"/>
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:layoutDirection="ltr"
android:layout_below="@id/description"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
android:textSize="26sp"
android:textColor="?attr/colorCode"
android:layout_alignParentStart="true"
android:layout_marginTop="10dp"
android:textStyle="normal|bold"/>
<TextView
android:id="@+id/next_profile_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:letterSpacing="-0.01"
android:layout_marginTop="0dp"
android:paddingTop="0dp"
android:paddingStart="2dp"
android:textColor="?attr/colorOnSurfaceDim"
android:textSize="16sp"
android:textStyle="normal|bold"
android:includeFontPadding="false"
android:fallbackLineSpacing="false"
tools:text="412 643"/>
</LinearLayout>
</RelativeLayout>

View file

@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/colorBackground"
android:orientation="horizontal">
<LinearLayout

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="20dp"
android:paddingTop="20dp"
android:paddingEnd="20dp"
android:orientation="vertical">
<RadioGroup
android:id="@+id/radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/keep_x_versions_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_backups_versioning_strategy_keep_x_versions" />
<RadioButton
android:id="@+id/single_backup_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_backups_versioning_strategy_single_backup" />
</RadioGroup>
<TextView
android:id="@+id/warning_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:text="@string/pref_backups_versioning_strategy_single_backup_warning"
android:textColor="?attr/colorError" />
<CheckBox
android:id="@+id/risk_accept"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/understand_risk_accept" />
</LinearLayout>

View file

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="16dp"
android:text="@string/dialog_duplicate_entry_title"
android:textSize="20sp" />
<TextView
android:id="@+id/duplicate_warning_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_duplicate_entry_message"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"
android:paddingTop="10dp"
android:paddingBottom="20dp" />
<LinearLayout
android:id="@+id/overwrite_entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingVertical="15dp"
android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_outline_brush_24"
app:tint="?attr/colorOnSurfaceVariant" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialog_duplicate_entry_overwrite_title"
android:textSize="17sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialog_duplicate_entry_overwrite_subtitle"
android:textSize="14sp"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/create_new_entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingVertical="15dp"
android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_tag_24"
app:tint="?attr/colorOnSurfaceVariant" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialog_duplicate_entry_suffix_title"
android:textSize="17sp"
android:textStyle="bold" />
<TextView
android:id="@+id/duplicate_suffix_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialog_duplicate_entry_suffix_subtitle"
android:textSize="14sp"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/cancel_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingVertical="15dp"
android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_outline_close_24"
app:tint="?attr/colorOnSurfaceVariant" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialog_duplicate_entry_cancel_title"
android:textSize="17sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialog_duplicate_entry_cancel_subtitle"
android:textSize="14sp"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="10dp"
android:paddingTop="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="25dp"
android:layout_marginEnd="25dp"
android:text="@string/assign_groups_dialog_summary" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/group_selection_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="25dp"
android:layout_marginEnd="25dp"
android:layout_marginTop="15dp"
android:hint="@string/assign_groups_dialog_dropdown"
style="?attr/dropdownStyle">
<AutoCompleteTextView
android:id="@+id/group_selection_dropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_group_name_layout"
android:hint="@string/group_name_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="25dp"
android:layout_marginEnd="25dp"
android:layout_marginTop="10dp"
android:visibility="gone">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_group_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapSentences"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>

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