Compare commits

..
120 Commits
Author SHA1 Message Date
Alexander Bakker d0baeef064 Release v1.1-beta1 2019-12-24 15:01:39 +01:00
Michael SchättgenandGitHub 44b83c888c Merge pull request #255 from alexbakker/hightlight-and-reveal
Make the behavior of highlighting and revealing entries consistent
2019-12-16 20:35:03 +01:00
Michael SchättgenandGitHub da45fa1969 Merge pull request #240 from alexbakker/biometric
Replace the custom fingerprint auth UI with BiometricPrompt
2019-12-16 17:53:57 +01:00
Alexander BakkerandGitHub 5ad3a10251 Merge pull request #260 from Dissem/master
Fix exception when importing an old export
2019-12-12 12:07:51 +01:00
Christian Basler 9ec94a472f Fix exception when importing an old export 2019-12-11 17:03:22 +01:00
Alexander Bakker 3be9aecb88 Replace the custom fingerprint auth UI with BiometricPrompt
This patch replaces the usage of the deprecated FingerprintManager API with
BiometricPrompt. This uses the Android X library, so we get the native biometric
prompt on recent versions of Android and a Google-made one on older versions. By
not working with custom prompts for biometric authentication like we do now, we
can be sure that any issues like #70, #81, #237 are not actually our fault.
Here's what it looks like:

![](https://alexbakker.me/u/b2rmf3x0b9.jpeg)

As a nice aside, this also adds support for the new facial recognition as an
authentication method on Pixel 4 phones.

This is still a draft, but early feedback is welcome.
2019-11-27 16:50:33 +01:00
Alexander Bakker a6a5af781e Make the behavior of highlighting and revealing entries consistent
I think this makes the highlight/reveal functionality feel a bit more
consistent, by not allowing multiple entries to be revealed at the same time,
just like you can't have multiple highlighted entries. Here's video of what it
looks like: https://alexbakker.me/u/3a9dhplrj2.mp4.
2019-11-27 16:39:46 +01:00
Michael SchättgenandGitHub a93ced6e34 Merge pull request #254 from alexbakker/fdroid-icon
Update the icon of the F-Droid listing
2019-11-26 22:17:53 +01:00
Alexander BakkerandGitHub 82135c8b9d Merge pull request #238 from michaelschattgen/feature-togglepassword
Add checkbox to toggle visibility in password fields
2019-11-26 21:55:45 +01:00
Michael Schättgen 9f248e0802 Add checkbox to toggle visibility in password fields
Make string translatable
2019-11-26 21:53:29 +01:00
Alexander Bakker 65aa95504b Update the icon of the F-Droid listing 2019-11-26 21:19:14 +01:00
Alexander BakkerandGitHub def7c67614 Merge pull request #239 from michaelschattgen/dialog-text-improvement
Add minor text improvement
2019-10-16 13:18:47 +02:00
Michael Schättgen be3f30e666 Add minor text improvement 2019-10-14 23:18:44 +02:00
Alexander Bakker b920fe4b8c Update README.md 2019-10-14 21:16:25 +02:00
Michael SchättgenandGitHub 4b6b10526d Merge pull request #236 from alexbakker/screenshots
Update screenshots
2019-10-14 21:07:07 +02:00
Michael SchättgenandGitHub 74b2f8f398 Merge pull request #235 from alexbakker/bug-template-device
Ask users for their device model in the bug report template
2019-10-14 21:02:36 +02:00
Michael SchättgenandGitHub 34ef4b3fb4 Merge pull request #232 from alexbakker/chromebook
Explicitly state in AndroidManifest that a camera is not required
2019-10-14 21:02:26 +02:00
Michael SchättgenandGitHub 63b0f6bb99 Merge pull request #220 from alexbakker/highlight-entry
Add the option to highlight entries when tapped
2019-10-14 20:55:53 +02:00
Michael SchättgenandGitHub 36973669ea Merge pull request #219 from alexbakker/new-andotp-format
Add support for the new andOTP backup format
2019-10-14 19:49:06 +02:00
Alexander Bakker 2e301c5389 Update screenshots
This updates the screenshots for F-Droid and the README to the ones shown in our
Play Store listing. I also removed the old icon attribution notice
2019-10-12 13:23:55 +02:00
Alexander Bakker 6a4b0f7bc5 Ask users for their device model in the bug report template 2019-10-12 11:53:14 +02:00
Alexander Bakker 8b063815ab Explicitly state in AndroidManifest that a camera is not required
This hopefully fixes #226. I tested this on a Chromebook emulator, but it let me
install the app even without this patch, so we'll have to wait and see if this
fixes it when we release a new version.
2019-10-09 15:24:03 +02:00
Alexander BakkerandGitHub 75b79d3d4a Merge pull request #203 from michaelschattgen/feature-winauthimport
Add WinAuth importer
2019-09-25 21:54:34 +02:00
Michael Schättgen b61b73685f Add WinAuth importer
Fix WinAuthImporter
2019-09-25 20:36:10 +02:00
Alexander Bakker 864e3bd6f4 Merge pull request #218 from michaelschattgen/feature-translation 2019-09-25 20:00:37 +02:00
Michael SchättgenandGitHub 013e4ae28b Merge pull request #206 from alexbakker/target-29
Target API 29, update gradle and update the dependencies
2019-09-23 22:55:37 +02:00
Alexander Bakker 51a0a16afb Add the option to highlight entries when tapped
This adds an option to highlight tapped entries, so that it's easier to
distinguisch between the one you're trying to enter into a website and the other
ones.

Only one entry can be highlighted at a time. Perhaps it would make sense to
change our tap to reveal functionality to behave the same, so that the two
features are nicely in sync. I can address that in a separate PR if we decide to
do so.
2019-09-21 12:58:35 +02:00
Alexander Bakker a5165e2345 Add support for the new andOTP backup format
This patch adds support for the new backup file format of andOTP. andOTP has
improved their security by switching from SHA-256 to PBKDF2 to derive the key
for encrypted backups.

Glad to see this has been addressed now. Awesome work, @flocke!

See: andOTP/andOTP@d96b037.
2019-09-20 22:45:11 +02:00
Alexander Bakker c41a337791 Fix the Travis CI build by accepting the Android SDK licenses 2019-09-18 21:56:47 +02:00
Alexander Bakker e58dec35c0 Target API 29, update gradle and update the dependencies
This also fixes an issue with the use of the Iconics library where it was
initialized twice. I also removed the dependency to [AndroidX Preference
eXtended](https://github.com/takisoft/preferencex-android), as there don't seem
to be any issues with using the vanilla AndroidX preference library anymore.
2019-09-18 21:56:47 +02:00
Alexander BakkerandGitHub 5e7697039a Merge pull request #214 from michaelschattgen/feature-ungrouped-filter
Add ungrouped filter
2019-09-18 21:55:22 +02:00
Michael Schättgen 45de9cea3a Add Crowdin support 2019-09-17 23:12:27 +02:00
Michael Schättgen 9f075e7bc6 Add ungrouped filter 2019-09-16 23:02:48 +02:00
Michael SchättgenandGitHub a88d1cf11e Merge pull request #211 from alexbakker/show-account-default
Set pref_account_name to true by default
2019-09-16 21:33:15 +02:00
Michael SchättgenandGitHub a6eec88dcb Merge pull request #195 from alexbakker/about-version-git
Add git commit hash and branch information to AboutActivity for debug builds
2019-09-16 20:40:43 +02:00
Michael SchättgenandGitHub 3c4afe36e8 Merge pull request #202 from simonporte/patch-1
Update french translation
2019-09-16 20:36:38 +02:00
simonporteandmdmk 5b71df37ec Update french translation.
Change a single word in english : change log --> changelog
2019-09-16 19:40:40 +02:00
Alexander Bakker d7d3d52a30 Set pref_account_name to true by default
This changes the default settings so that account names are shown by default
2019-09-15 21:04:36 +02:00
Alexander BakkerandGitHub ac222c4946 Merge pull request #201 from michaelschattgen/feature-usernamesearch
Add ability to search in account names
2019-09-14 16:03:45 +01:00
奔跑的梅洛斯andAlexander Bakker 0200287203 Add Simplified Chinese translation (#198) 2019-09-13 18:56:24 +01:00
Alexander Bakker 5677bd4112 Copy the version to the clipboard when tapped in AboutActivity 2019-09-13 19:40:39 +02:00
Alexander Bakker cbe3697d68 Add git commit hash and branch information to AboutActivity for debug builds
This will help us differentiate between custom/debug builds and release builds
of Aegis.
2019-09-13 19:37:47 +02:00
Michael Schättgen 445410fcd7 Add ability to search in account names 2019-09-11 20:51:02 +02:00
Michael Schättgen 1d513441c6 Release v1.0.2 2019-09-10 23:43:10 +02:00
Michael Schättgen 0a31103f91 Disable automatic backups through Google Play
Backups from the Google Play Store had some unintended consequences...
2019-09-10 23:11:22 +02:00
Michael Schättgen 85bf4adc72 Enable and fix search feature again
For some reason we used the old SearchView. Changed it to androidx.appcompat.widget.SearchView and it works on Huawei devices again.
2019-09-10 23:10:11 +02:00
Alexander Bakker cc55a6dacb Release v1.0.1 2019-09-10 00:15:50 +02:00
Alexander Bakker e099e2cacd Add missing null check in onBackPressed for SearchView 2019-09-10 00:14:42 +02:00
Michael Schättgen 658946ff70 Temporarily disable search feature when unavailable
Which isn't working on Huawei devices
2019-09-09 23:51:13 +02:00
Alexander Bakker b6a31ed789 Release v1.0 2019-09-08 14:22:33 +02:00
Alexander BakkerandGitHub 9c272a0d4d Merge pull request #190 from michaelschattgen/feature-about
Add AboutActivity
2019-09-07 12:50:11 +02:00
Michael Schättgen 3e626a37db Add AboutActivity
Add DialogStyles for different themes

Add review fixes

Remove unused usings
2019-09-07 12:49:31 +02:00
Alexander BakkerandGitHub e2150e3823 Merge pull request #194 from michaelschattgen/feature-empty-dataset
Add placeholder for empty recyclerview
2019-09-07 11:55:49 +02:00
Michael Schättgen 6480fef315 Add placeholder for empty recyclerview 2019-09-07 11:48:38 +02:00
Alexander BakkerandGitHub 95df2ceb42 Merge pull request #193 from michaelschattgen/fix-icon-onboarding
Fix app icon in IntroActivity
2019-09-07 10:41:02 +02:00
Michael SchättgenandGitHub 189698dddb Merge pull request #189 from alexbakker/entry-defaults
Set a default value for period and digits for new entries
2019-09-07 00:03:55 +02:00
Michael Schättgen 438d5581aa Fix app icon in IntroActivity 2019-09-06 23:58:04 +02:00
Alexander BakkerandGitHub 7af78b11f5 Merge pull request #192 from michaelschattgen/feature-fixbackpress
Add temporary backpress fix
2019-09-06 22:02:10 +02:00
Michael SchättgenandGitHub 5d68211ce9 Merge pull request #191 from alexbakker/travis-test
Add Travis CI configuration
2019-09-06 21:49:44 +02:00
Michael Schättgen a7e1a88dc8 Add temporary backpress fix 2019-09-06 21:48:17 +02:00
Alexander Bakker 3054032a37 Add Travis CI configuration
This doesn't spin up an emulator yet, but we don't have any tests that require
one anyway
2019-09-05 01:12:30 +02:00
Alexander Bakker 79279630ba Fix all linter errors
Well almost all, just ignore missing translation errors
2019-09-05 01:09:16 +02:00
Alexander Bakker e019fb6db2 Set a default value for period and digits for new entries
This is also in preparation for #24, but I haven't fully figured out how to do
that nicely yet, so that'll come later.
2019-09-04 23:48:30 +02:00
Alexander Bakker 85bdecc573 Fix syntax error in the XML file of the German translation 2019-09-04 23:47:25 +02:00
Andrey SaksonovandAlexander Bakker 21f3d93295 Update Russian localization (#170) 2019-09-03 10:14:08 +02:00
xopezandAlexander Bakker 888e9d0e4a Added German Translation (#188)
* Added German Translation

* Update arrays.xml
2019-09-03 10:10:22 +02:00
Alexander Bakker e6d84e6859 Release v1.0-beta1 2019-08-31 14:05:53 +02:00
Alexander BakkerandGitHub 65371f18d4 Merge pull request #184 from michaelschattgen/feature-setgroupautomatically
Set group on new entries automatically when group filter is active
2019-08-31 13:48:50 +02:00
Michael Schättgen 1b5ab33c25 Add way to pass selected group to edit entry activity 2019-08-31 13:46:18 +02:00
Michael SchättgenandGitHub 396dd0b169 Merge pull request #187 from alexbakker/contributing
Add basic contribution guidelines, issue templates and a guide for ADB
2019-08-29 21:39:52 +02:00
Alexander Bakker eb40369634 Add basic contribution guidelines, issue templates and a guide for ADB 2019-08-29 21:12:17 +02:00
Alexander BakkerandGitHub 0c87f06972 Merge pull request #183 from michaelschattgen/feature-applockdependency
Change auto lock preference visibility behaviour
2019-08-29 11:47:51 +02:00
Michael Schättgen c774796422 Change auto lock preference visibility behaviour 2019-08-28 23:38:12 +02:00
Alexander BakkerandGitHub c12c6ab107 Merge pull request #173 from michaelschattgen/feature-tapholdswipe
Overhaul entry interaction
2019-08-28 23:27:14 +02:00
Michael SchättgenandGitHub e3b9b67fee Merge pull request #181 from alexbakker/fix-152
Check for the possibility of an overflow when parsing OTP period
2019-08-28 23:12:51 +02:00
Michael Schättgen 2407b4b780 Overhaul entry interaction
Add support for different card background colors
Add minor bug fixes
Add minor fixes

Fix minor bugs where action mode kept active
2019-08-28 23:09:52 +02:00
Michael SchättgenandGitHub e962d91c77 Merge pull request #177 from alexbakker/fix-175
Correctly display codes with an uneven number of digits
2019-08-28 22:24:59 +02:00
Michael Schättgen d0afa2f61f Fix app icon 2019-08-28 22:08:06 +02:00
Alexander Bakker 58d13ba9e3 Check for the possibility of an overflow when parsing OTP period
The conversion of the OTP period value to milliseconds may overflow for large
values, causing the result to wrap around to Integer.MIN_VALUE. This
subsequently caused a crash when calling ObjectAnimator.setDuration.
2019-08-28 19:46:47 +02:00
Alexander Bakker 631ca76136 Correctly display codes with an uneven number of digits 2019-08-14 22:12:36 +02:00
Michael SchättgenandGitHub 9cb9d47857 Merge pull request #136 from alexbakker/uuid-map
Introduce UUIDMap for storing objects that are keyed by a UUID
2019-08-04 19:41:27 +02:00
Alexander Bakker 2323d89938 Introduce UUIDMap for storing objects that are keyed by a UUID
This patch introduces the new ``UUIDMap`` type, reducing code duplication and
making UUID lookups faster. We currently already use UUIDs as the identifier for
the ``DatabaseEntry`` and ``Slot`` types, but the way lookups by UUID work are
kind of ugly, as we simply iterate over the list until we find a match. As we're
probably going to have more types like this soon (groups and icons, for
example), I figured it'd be good to abstract this away into a separate type and
make it a map instead of a list.

The only thing that has gotten slower is the ``swap`` method. The internal
``LinkedHashMap`` retains insertion order with a linked list, but does not know
about the position of the values, so we basically have to copy the entire map to
simply swap two values. I don't think it's too big of a deal, because swap
operations still take less than a millisecond even with large vaults, but
suggestions for improving this are welcome.

I had to update gradle and JUnit to be able to use the new ``assertThrows``
assertion method, so this patch includes that as well.
2019-08-03 17:50:25 +02:00
Michael SchättgenandGitHub 6769fefd00 Merge pull request #106 from alexbakker/feature-import-freeotp+
Add support for importing from FreeOTP+
2019-08-03 17:26:30 +02:00
Alexander BakkerandGitHub 5dbf1c7ce2 Merge pull request #172 from michaelschattgen/feature-otpauthuri
Add support for deeplinking otpauth uris
2019-08-03 17:16:13 +02:00
Michael Schättgen 8d3c113475 Add support for deeplinking otpauth uris
Fix bug
2019-08-03 17:15:35 +02:00
Alexander BakkerandGitHub 087bf08d44 Merge pull request #169 from michaelschattgen/feature-showpassword
Add ability to toggle password visibility
2019-08-03 16:13:02 +02:00
Michael SchättgenandGitHub 32b2ce280c Merge pull request #144 from alexbakker/fix-lag-part1
Unregister any listeners when destroying EntryListView
2019-08-02 21:16:42 +02:00
Michael SchättgenandGitHub 37c9b033c5 Merge pull request #141 from alexbakker/pref-lang
Add a language option to the preference menu
2019-08-01 21:17:30 +02:00
Alexander BakkerandGitHub f8d806999d Merge pull request #157 from BM835/master
Updated Russian translation
2019-08-01 11:26:08 +02:00
Michael SchättgenandGitHub 86c4011797 Merge pull request #158 from alexbakker/fix-150
Get external storage read permissions before selecting an image to scan
2019-08-01 00:00:35 +02:00
Michael Schättgen bbc4c1d61c Add ability to toggle password visibility 2019-07-31 23:53:38 +02:00
Alexander BakkerandGitHub 951e908d4b Merge pull request #148 from michaelschattgen/feature-notification
Add notification to warn user about the unlock state
2019-07-31 18:58:06 +02:00
Alexander BakkerandGitHub 536929ff0e Merge pull request #168 from michaelschattgen/feature-icon
Add new icon
2019-07-31 18:54:14 +02:00
Michael Schättgen 1f5a46b883 Add notification to warn user about the unlock state
Clean up code

Add minor changes
2019-07-31 00:37:48 +02:00
Michael Schättgen 4cab47235b Add new icon 2019-07-30 23:30:13 +02:00
Alexander BakkerandGitHub 5524a77736 Merge pull request #163 from wvffle/patch-1
Fix #162
2019-07-30 10:14:18 +02:00
Kasper SewerynandGitHub a3cdfe693d Fix #162 2019-07-11 13:41:38 +02:00
Alexander Bakker 9e4ad1495d Get external storage read permissions before selecting an image to scan
Fixes #150
2019-07-03 18:57:03 +02:00
BM835 914da94c01 Updated Russian translation 2019-07-03 13:14:09 +10:00
Alexander Bakker 950dcce803 Unregister any listeners when destroying EntryListView
We were leaking some resources by not unregistering listeners when destroying
the entry list view. The code refresh loop of the leaked view started running in
a tight infinite loop, which causes a lot of lag in the main activity.
2019-06-25 22:57:17 +02:00
Michael SchättgenandGitHub f82e480c32 Merge pull request #143 from alexbakker/fix-lag-part2
Use glide to load and cache icons
2019-06-25 22:50:31 +02:00
Alexander Bakker 9c5726cbca Use glide to load and cache icons
This patch adds a dependency to glide to handle the loading and caching of
icons. In my testing it eliminated the lag previously experienced in the main
activity when quickly scrolling through a large list of entries. It does add an
extra 1MB to the APK size, but I think that's acceptable for the amount of
complexity it handles for us.
2019-06-25 21:01:50 +02:00
Michael SchättgenandGitHub 550e53f225 Merge pull request #140 from alexbakker/debug-flag-secure
Disable screen security by default for debug builds
2019-06-25 00:15:15 +02:00
Michael SchättgenandGitHub 6e04ca054d Merge pull request #135 from alexbakker/fix-divider
Move entry divider logic to EntryListView
2019-06-25 00:14:19 +02:00
Alexander Bakker b014d95005 Add a language option to the preference menu 2019-06-22 09:58:35 +02:00
Alexander Bakker 0123b98e47 Disable screen security by default for debug builds
I've lost count of how many times I've tried to record a demo video with
screen security enabled.
2019-06-19 14:47:02 +02:00
Alexander Bakker d7093487dd Add support for importing from FreeOTP+
This also includes some other small changes:
- Make FreeOtpImporter more reusable
- Refactor preference file parsing logic into a separate class
- Add support for importing Steam tokens from FreeOTP(+)
- Make FileReader a bit leaner
- Add some missing @Override annotations
2019-06-19 13:09:34 +02:00
Alexander Bakker 02c9a0cb1c Move entry divider logic to EntryListView
This patch makes EntryListView responsible for providing the divider between
entries, instead of setting a margin on every entry like we do now. It also
fixes a couple of miscellaneous issues, like use of the old package name.
2019-06-09 22:41:50 +02:00
Michael Schättgen eb29be587f Fix bug where searchview did not reset properly 2019-06-07 20:57:57 +02:00
Alexander Bakker f64a23d98f Release v0.4.3 2019-06-06 22:27:59 +02:00
Michael SchättgenandGitHub 8bbadb9e57 Merge pull request #130 from alexbakker/fix-import
Limit the amount of entry info passed to SelectEntriesActivitiy
2019-06-06 21:56:28 +02:00
Alexander Bakker 4066cd83cc Limit the amount of entry info passed to SelectEntriesActivitiy
This horrid patch changes the vault import logic to pass an ImportEntry list to
SelectEntriesActivity, instead of a DatabaseEntry list. Previously, a crash
would occur when importing a vault with lots of icons, because the maximum
Parcel size was exceeded.

Storing icons in the vault file was a bad idea.
2019-06-06 01:25:39 +02:00
Alexander BakkerandGitHub cca35bd5e5 Merge pull request #109 from michaelschattgen/feature-entrysearch
Add ability to search for entries in vault
2019-06-02 16:58:53 +02:00
Michael Schättgen 187adfa738 Add ability to search for entries
Fix bug where new entries did not get filtered

Fix IsEntryFiltered
2019-06-02 16:52:32 +02:00
Alexander BakkerandGitHub 246c03dea1 Merge pull request #107 from michaelschattgen/feature-importauthy
Add support for importing from Authy
2019-05-31 10:17:59 +02:00
Alexander BakkerandGitHub e68571a7a6 Merge pull request #108 from michaelschattgen/feature-qrshortcuticon
Add new QR shortcut icon which follows material guidelines
2019-05-31 09:54:36 +02:00
Michael SchättgenandGitHub 128c85fc0c Merge pull request #111 from alexbakker/fix-intro-decrypt
Don't ignore the result of IntroActivity when the vault is locked
2019-05-30 19:42:17 +02:00
Michael Schättgen c75b25c4b6 Add new QR shortcut icon which follows material guidelines
Remove superfluous drawable files
2019-05-30 19:40:47 +02:00
Michael Schättgen 6abcfcea75 Add ability to import from Authy
Fix importer and appImporter lists
2019-05-30 19:35:50 +02:00
Alexander Bakker b300b72626 Don't ignore the result of IntroActivity when the vault is locked
This fixes a bug where AuthActivity would be shown after finishing the intro.
2019-05-30 12:50:40 +02:00
185 changed files with 4637 additions and 1539 deletions
+34
View File
@@ -0,0 +1,34 @@
---
name: "Bug report"
about: "Create a report to help us fix a bug"
labels: bug
---
<!-- Please read the [bug reports section of the contribution
guidelines](/CONTRIBUTING.md#bug-reports) before submitting an issue. -->
###### Info
* __Version__:
* __Source__: (Google Play/GitHub/F-Droid/?)
* __Vault encrypted:__ Yes (with biometric unlock)/Yes/No
* __Device:__
* __Android version and ROM:__
###### Steps to reproduce
A detailed list of reproduction steps.
###### What do you expect to happen?
...
###### What happens instead?
...
###### Log
```
If applicable, paste the debug log here.
```
+6
View File
@@ -0,0 +1,6 @@
---
name: "Feature request"
about: "Suggest a new feature for this project"
labels: proposal
---
+15
View File
@@ -0,0 +1,15 @@
language: android
env:
global:
- ANDROID_HOME=/usr/local/android-sdk
android:
components:
- android-29
- build-tools-29.0.2
before_install:
- mkdir -p $ANDROID_HOME/licenses
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > $ANDROID_HOME/licenses/android-sdk-license
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> $ANDROID_HOME/licenses/android-sdk-license
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> $ANDROID_HOME/licenses/android-sdk-license
script:
- ./gradlew build test
+106
View File
@@ -0,0 +1,106 @@
# Contributing
There are a couple of ways to contribute to Aegis. This document contains some
general guidelines for each type of contribution.
## Pull requests
If you're planning on adding a new feature or making other large changes, please
discuss it with us first through a proposal on GitHub. Discussing your idea with
us first ensures that everyone is on the same page before you start working on
your change. We don't like rejecting pull requests.
## Bug reports
We use GitHub's issue tracker to track bugs. Bug reports must follow [the
template](.github/ISSUE_TEMPLATE/bug.md). If a bug report does not follow the
template and does not contain enough information, it will be closed without a
response. Duplicate bug reports receive the same treatment.
Please consider trying to find the root cause yourself first and perhaps even
send us a patch that fixes the issue! We're happy to help you if you get stuck
along the way.
### Capturing a log with ADB
In some cases, we ask our users to obtain a debug log from their device. This is
typically only necessary if Aegis:
- Is unable to recover from an error and crashes.
- Only shows a generic error to the user, but writes a more detailed one to the
log.
Capturing a log with the Android Debug Bridge (ADB) allows us to see the stack
trace and the exception that occurred.
#### Preparation
Before you can capture a log, you first need to go through a one-time setup
process on your Android device and computer.
##### Prerequisites
- Your Android device.
- A computer with Windows, Mac or Linux.
- A USB cable to connect your Android device to your computer.
##### Setup
__On your Android device__:
1. Navigate to ``Settings -> About``, scroll down and start tapping on the build
number until developer options are enabled.
2. Navigate to ``Settings -> System -> Developer options`` and enable ``USB
debugging``.
These navigation steps may differ slightly across Android versions and ROMs.
__On your computer__:
3. Download and extract the SDK platform tools for Android:
https://developer.android.com/studio/releases/platform-tools.
4. Start your terminal emulator (If you're on Windows, start PowerShell) and
navigate to the folder where platform-tools was extracted.
5. Execute ``adb devices``.
__On your Android device__:
6. A prompt will appear. Select "Always allow from this computer" and accept the
connection.
#### Capturing a log
__On your Android device__:
1. Start Aegis.
__On your PC__:
2. Start your terminal emulator (If you're on Windows, start PowerShell) and
navigate to the folder where platform-tools was extracted.
3. Start a log capture by executing the following commands.
```
adb logcat -c
adb logcat -f debug.log
```
The logcat command captures the full system log by default, which may expose
some sensitive information. While this information can sometimes help with
finding the root cause of the issue, it is not always necessary. To only
capture the log output of Aegis, replace the last logcat command with the
one below:
```sh
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) -f debug.log
```
_If you are using a debug APK, replace ``com.beemdevelopment.aegis`` with
``com.beemdevelopment.aegis.debug``._
__On your Android device__:
4. Reproduce the issue.
__On your PC__:
5. Stop the log capture with Ctrl+C.
6. Attach the ``debug.log`` file to your issue on GitHub.
+28 -17
View File
@@ -2,7 +2,8 @@
Aegis is a free, secure and open source 2FA app for Android.
Aegis' security design and vault format is described in detail in [this document](docs/vault.md).
Aegis' security design and vault format is described in detail in [this
document](docs/vault.md).
# Features
@@ -10,7 +11,7 @@ Aegis' security design and vault format is described in detail in [this document
- Secure
- Encryption (AES-256)
- Password (scrypt)
- Fingerprint (Android Keystore)
- Biometrics (Android Keystore)
- Screen capture prevention
- Tap to reveal ability
- Multiple ways to add new entries
@@ -42,17 +43,22 @@ Aegis' security design and vault format is described in detail in [this document
## Screenshots
[<img width=200 alt="Main Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true)
[<img width=200 alt="Settings Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true)
[<img width=200 alt="Edit Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true)
[<img width=200 alt="Screenshot 1"
src="metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true)
[<img width=200 alt="Screenshot 2"
src="metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true)
[<img width=200 alt="Screenshot 3"
src="metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true)
[<img width=200 alt="Main Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true)
[<img width=200 alt="Main Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true)
[<img width=200 alt="Screenshot 4"
src="metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true)
[<img width=200 alt="Screenshot 5"
src="metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true)
[<img width=200 alt="Screenshot 6"
src="metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true)
[<img width=200 alt="Screenshot 7"
src="metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true)
## Downloads
@@ -96,6 +102,16 @@ Certificate fingerprints:
SHA256: C6:DB:80:A8:E1:4E:52:30:C1:DE:84:15:EF:82:0D:13:DC:90:1D:8F:E3:3C:F3:AC:B5:7B:68:62:D8:58:A8:23
```
### Community
- [aegis-icons](https://github.com/krisu5/aegis-icons)
While we're still working on better icon support in Aegis, krisu5 has started
a third-party repository for icons that you may want to check out.
[<img width=500 alt="Aegis-icons preview"
src="https://raw.githubusercontent.com/krisu5/aegis-icons/master/showcase.png">](https://github.com/krisu5/aegis-icons)
## Libraries
- [TextDrawable](https://github.com/amulyakhare/TextDrawable) by Amulya Khare
@@ -104,7 +120,6 @@ Certificate fingerprints:
- [AppIntro](https://github.com/AppIntro/AppIntro) by Paolo Rotolo
- [Krop](https://github.com/avito-tech/krop) by Avito Technology
- [SpongyCastle](https://github.com/rtyley/spongycastle) by Roberto Tyley
- [Swirl](https://github.com/mattprecious/swirl) by Matthew Precious
- [CircleImageView](https://github.com/hdodenhof/CircleImageView) by Henning
Dodenhof
- [barcodescanner](https://github.com/dm77/barcodescanner) by Dushyanth
@@ -114,7 +129,3 @@ Certificate fingerprints:
This project is licensed under the GNU General Public License v3.0. See the
[LICENSE](LICENSE) file for details.
## Attribution
The icon is largely based on a design by Freepik from Flaticon.
+59 -16
View File
@@ -1,27 +1,61 @@
apply plugin: 'com.android.application'
def getCmdOutput = { cmd ->
def stdout = new ByteArrayOutputStream()
exec {
commandLine cmd
standardOutput = stdout
}
return stdout.toString().trim()
}
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
android {
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
targetSdkVersion 28
versionCode 12
versionName "0.4.2"
targetSdkVersion 29
versionCode 23
versionName "1.1-beta1"
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
}
lintOptions {
abortOnError true
disable "MissingTranslation"
}
testOptions {
unitTests.all {
useJUnitPlatform()
ignoreFailures false
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
}
}
}
buildTypes {
debug {
minifyEnabled false
applicationIdSuffix ".debug"
manifestPlaceholders = [title:"AegisDev", iconName:"ic_launcher_debug"]
manifestPlaceholders = [title: "AegisDev", iconName: "ic_launcher_debug"]
resValue "bool", "pref_secure_screen_default", "false"
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
manifestPlaceholders = [title:"Aegis", iconName:"ic_launcher"]
manifestPlaceholders = [title: "Aegis", iconName: "ic_launcher"]
resValue "bool", "pref_secure_screen_default", "true"
}
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
@@ -29,24 +63,33 @@ android {
}
dependencies {
def libsuVersion = '2.3.2'
def libsuVersion = '2.5.1'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation "androidx.biometric:biometric:1.0.0"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'com.takisoft.preferencex:preferencex:1.0.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.getbase:floatingactionbutton:1.10.1'
implementation 'com.github.apl-devs:appintro:v4.2.2'
implementation 'com.github.apl-devs:appintro:5.1.0'
implementation 'com.github.avito-tech:krop:0.44'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.mattprecious.swirl:swirl:1.0.0'
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'me.dm7.barcodescanner:zxing:1.9'
implementation 'de.hdodenhof:circleimageview:3.0.1'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
testImplementation 'junit:junit:4.12'
implementation 'com.github.bumptech.glide:annotations:4.9.0'
implementation 'com.github.bumptech.glide:glide:4.9.0'
implementation "com.mikepenz:iconics-core:3.2.5"
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
implementation("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
transitive = false
}
annotationProcessor 'androidx.annotation:annotation:1.1.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
}
@@ -1,13 +0,0 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}
+19 -2
View File
@@ -4,19 +4,25 @@
package="com.beemdevelopment.aegis">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<application
android:name=".AegisApplication"
android:allowBackup="true"
android:allowBackup="false"
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:theme">
<activity
android:name=".ui.AboutActivity"
android:label="@string/title_activity_about"></activity>
<activity
android:name=".ui.SelectEntriesActivity"
android:label="Select entries" />
@@ -26,8 +32,17 @@
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" />
</intent-filter>
</activity>
<activity
android:name=".ui.ScannerActivity"
@@ -48,6 +63,8 @@
<activity
android:name=".ui.GroupManagerActivity"
android:label="Manage groups" />
<service android:name=".services.NotificationService" />
</application>
</manifest>
+85
View File
@@ -0,0 +1,85 @@
<html>
<head>
<style type="text/css">
* {
word-wrap: break-word;
}
body {
background-color: #%1$s;
color: #%2$s;
}
ul {
list-style-position: inside;
padding: 0;
padding-left: 5px;
margin-bottom: 5px;
}
li {
padding-bottom: 8px;
list-style-position: outside;
margin-left: 1em;
}
h4 {
margin-top: 0px;
margin-bottom: 0px;
}
h3 {
margin-bottom: 7px;
padding-top: 7px;
}
</style>
</head>
<body>
<div></div>
<h3>Version 1.1</h3>
<h4>New</h4>
<ul>
<li>Support for other types of biometric authentication (i.e. Pixel 4 face unlock)</li>
<li>Support for importing from WinAuth</li>
<li>Support for Chromebooks</li>
<li>Option to highlight entries when tapped</li>
<li>Filter for ungrouped tokens</li>
<li>Ability to search for token account names</li>
<li>Simplified Chinese translation (thanks RunningMelos!)</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The behavior of highlighting and revealing entries was inconsistent</li>
</ul>
<h3>Version 1.0.3</h3>
<h4>New</h4>
<ul>
<li>Support for andOTP's new backup file format</li>
</ul>
<h3>Version 1.0.2</h3>
<h4>Fixes</h4>
<ul>
<li>Search feature on Huawei devices</li>
</ul>
<h4>Notes</h4>
<ul>
<li>Disabled automatic backups through the Google Play Store</li>
</ul>
<h3>Version 1.0.1</h3>
<h4>Notes</h4>
<ul>
<li>Temporarily disabled search feature on Huawei devices</li>
</ul>
<h3>Version 1.0</h3>
<h4>New</h4>
<ul>
<li>New icon</li>
<li>Overhaul of interaction with the entry list</li>
<li>Persistent notification while the vault is unlocked</li>
<li>Language override option</li>
<li>Support for importing from FreeOTP+</li>
<li>Ability to toggle password visibility during unlock</li>
<li>Support for deeplinking otpauth URIs</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Bad overall performance and high battery usage</li>
<li>Codes with an uneven number of digits are displayed incorrectly</li>
<li>Crash when entering a large value for OTP period</li>
</ul>
</body>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

@@ -1,6 +1,8 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -11,7 +13,10 @@ import android.graphics.drawable.Icon;
import android.os.Build;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.mikepenz.iconics.Iconics;
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
import java.util.ArrayList;
import java.util.Collections;
@@ -24,6 +29,9 @@ public class AegisApplication extends Application {
private Preferences _prefs;
private List<LockListener> _lockListeners;
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private static final String CODE_LOCK_DATABASE_ACTION = "lock_database";
@Override
public void onCreate() {
super.onCreate();
@@ -31,13 +39,23 @@ public class AegisApplication extends Application {
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
Iconics.init(this);
Iconics.registerFont(new MaterialDesignIconic());
// listen for SCREEN_OFF events
ScreenOffReceiver receiver = new ScreenOffReceiver();
registerReceiver(receiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
intentFilter.addAction(CODE_LOCK_DATABASE_ACTION);
registerReceiver(receiver, intentFilter);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
initAppShortcuts();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
initNotificationChannels();
}
}
public DatabaseManager getDatabaseManager() {
@@ -65,6 +83,8 @@ public class AegisApplication extends Application {
for (LockListener listener : _lockListeners) {
listener.onLocked();
}
stopService(new Intent(AegisApplication.this, NotificationService.class));
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
@@ -81,14 +101,28 @@ public class AegisApplication extends Application {
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
.setShortLabel(getString(R.string.new_profile))
.setLongLabel(getString(R.string.add_new_profile))
.setIcon(Icon.createWithResource(this, R.drawable.qr_scanner))
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
.setIntent(intent)
.build();
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
}
private class ScreenOffReceiver extends BroadcastReceiver {
private void initNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.channel_name_lock_status);
String description = getString(R.string.channel_description_lock_status);
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(CODE_LOCK_STATUS_ID, name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
public class ScreenOffReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (isAutoLockEnabled()) {
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis;
public enum CancelAction {
KILL,
CLOSE
}
@@ -2,8 +2,12 @@ package com.beemdevelopment.aegis;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Build;
import android.preference.PreferenceManager;
import java.util.Locale;
public class Preferences {
private SharedPreferences _prefs;
@@ -15,12 +19,21 @@ public class Preferences {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
public boolean isSearchAccountNameEnabled() {
return _prefs.getBoolean("pref_search_names", false);
}
public boolean isEntryHighlightEnabled() {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean isSecureScreenEnabled() {
return _prefs.getBoolean("pref_secure_screen", true);
// screen security should be enabled by default, but not for debug builds
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
}
public boolean isAccountNameVisible() {
return _prefs.getBoolean("pref_account_name", false);
return _prefs.getBoolean("pref_account_name", true);
}
public boolean isIntroDone() {
@@ -70,4 +83,18 @@ public class Preferences {
public int getTimeout() {
return _prefs.getInt("pref_timeout", -1);
}
public Locale getLocale() {
String lang = _prefs.getString("pref_lang", "system");
if (lang.equals("system")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Resources.getSystem().getConfiguration().getLocales().get(0);
} else {
return Resources.getSystem().getConfiguration().locale;
}
}
return new Locale(lang);
}
}
@@ -30,4 +30,15 @@ public enum ViewMode {
return R.layout.card_entry;
}
}
/**
* Retrieves the height (in dp) that the divider between entries should have in this view mode.
*/
public float getDividerHeight() {
if (this == ViewMode.COMPACT) {
return 0;
}
return 20;
}
}
@@ -2,17 +2,15 @@ package com.beemdevelopment.aegis.db;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;
import java.util.UUID;
public class Database {
private static final int VERSION = 1;
private DatabaseEntryList _entries = new DatabaseEntryList();
private UUIDMap<DatabaseEntry> _entries = new UUIDMap<>();
public JSONObject toJson() {
try {
@@ -32,6 +30,7 @@ public class Database {
public static Database fromJson(JSONObject obj) throws DatabaseException {
Database db = new Database();
UUIDMap<DatabaseEntry> entries = db.getEntries();
try {
int ver = obj.getInt("version");
@@ -42,7 +41,7 @@ public class Database {
JSONArray array = obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
DatabaseEntry entry = DatabaseEntry.fromJson(array.getJSONObject(i));
db.addEntry(entry);
entries.add(entry);
}
} catch (Base64Exception | OtpInfoException | JSONException e) {
throw new DatabaseException(e);
@@ -51,27 +50,7 @@ public class Database {
return db;
}
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
}
public void removeEntry(DatabaseEntry entry) {
_entries.remove(entry);
}
public void replaceEntry(DatabaseEntry newEntry) {
_entries.replace(newEntry);
}
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
_entries.swap(entry1, entry2);
}
public List<DatabaseEntry> getEntries() {
return _entries.getList();
}
public DatabaseEntry getEntryByUUID(UUID uuid) {
return _entries.getByUUID(uuid);
public UUIDMap<DatabaseEntry> getEntries() {
return _entries;
}
}
@@ -5,17 +5,17 @@ import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
public class DatabaseEntry implements Serializable {
private UUID _uuid;
public class DatabaseEntry extends UUIDMap.Value {
private String _name = "";
private String _issuer = "";
private String _group;
@@ -23,12 +23,13 @@ public class DatabaseEntry implements Serializable {
private byte[] _icon;
private DatabaseEntry(UUID uuid, OtpInfo info) {
_uuid = uuid;
super(uuid);
_info = info;
}
public DatabaseEntry(OtpInfo info) {
this(UUID.randomUUID(), info);
super();
_info = info;
}
public DatabaseEntry(OtpInfo info, String name, String issuer) {
@@ -46,7 +47,7 @@ public class DatabaseEntry implements Serializable {
try {
obj.put("type", _info.getType());
obj.put("uuid", _uuid.toString());
obj.put("uuid", getUUID().toString());
obj.put("name", _name);
obj.put("issuer", _issuer);
obj.put("group", _group);
@@ -82,14 +83,6 @@ public class DatabaseEntry implements Serializable {
return entry;
}
public void resetUUID() {
_uuid = UUID.randomUUID();
}
public UUID getUUID() {
return _uuid;
}
public String getName() {
return _name;
}
@@ -136,19 +129,39 @@ public class DatabaseEntry implements Serializable {
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof DatabaseEntry)) {
return false;
}
DatabaseEntry entry = (DatabaseEntry) o;
return getUUID().equals(entry.getUUID())
&& getName().equals(entry.getName())
return super.equals(entry) && equivalates(entry);
}
/**
* Reports whether this entry is equivalent to the given entry. The UUIDs of these
* entries are ignored during the comparison, so they are not necessarily the same
* instance.
*/
public boolean equivalates(DatabaseEntry entry) {
return getName().equals(entry.getName())
&& getIssuer().equals(entry.getIssuer())
&& Objects.equals(getGroup(), entry.getGroup())
&& getInfo().equals(entry.getInfo())
&& Arrays.equals(getIcon(), entry.getIcon());
}
/**
* Reports whether this entry has its values set to the defaults.
*/
public boolean isDefault() {
return equivalates(getDefault());
}
public static DatabaseEntry getDefault() {
try {
return new DatabaseEntry(new TotpInfo(null));
} catch (OtpInfoException e) {
throw new RuntimeException(e);
}
}
}
@@ -1,62 +0,0 @@
package com.beemdevelopment.aegis.db;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import androidx.annotation.NonNull;
public class DatabaseEntryList implements Iterable<DatabaseEntry>, Serializable {
private List<DatabaseEntry> _entries = new ArrayList<>();
@NonNull
@Override
public Iterator<DatabaseEntry> iterator() {
return _entries.iterator();
}
public void add(DatabaseEntry entry) {
if (getByUUID(entry.getUUID()) != null) {
throw new AssertionError("entry found with the same uuid");
}
_entries.add(entry);
}
public void remove(DatabaseEntry entry) {
entry = mustGetByUUID(entry.getUUID());
_entries.remove(entry);
}
public void replace(DatabaseEntry newEntry) {
DatabaseEntry oldEntry = mustGetByUUID(newEntry.getUUID());
_entries.set(_entries.indexOf(oldEntry), newEntry);
}
public void swap(DatabaseEntry entry1, DatabaseEntry entry2) {
Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2));
}
public List<DatabaseEntry> getList() {
return Collections.unmodifiableList(_entries);
}
public DatabaseEntry getByUUID(UUID uuid) {
for (DatabaseEntry entry : _entries) {
if (entry.getUUID().equals(uuid)) {
return entry;
}
}
return null;
}
private DatabaseEntry mustGetByUUID(UUID uuid) {
DatabaseEntry entry = getByUUID(uuid);
if (entry == null) {
throw new AssertionError("no entry found with the same uuid");
}
return entry;
}
}
@@ -1,10 +1,12 @@
package com.beemdevelopment.aegis.db;
import android.content.Context;
import android.content.Intent;
import android.os.Environment;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.services.NotificationService;
import org.json.JSONObject;
@@ -14,7 +16,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.Collator;
import java.util.List;
import java.util.Collection;
import java.util.TreeSet;
import java.util.UUID;
@@ -72,6 +74,7 @@ public class DatabaseManager {
JSONObject obj = _file.getContent(creds);
_db = Database.fromJson(obj);
_creds = creds;
_context.startService(new Intent(_context, NotificationService.class));
} catch (DatabaseFileException | DatabaseException e) {
throw new DatabaseManagerException(e);
}
@@ -133,32 +136,32 @@ public class DatabaseManager {
public void addEntry(DatabaseEntry entry) {
assertState(false, true);
_db.addEntry(entry);
_db.getEntries().add(entry);
}
public void removeEntry(DatabaseEntry entry) {
public DatabaseEntry removeEntry(DatabaseEntry entry) {
assertState(false, true);
_db.removeEntry(entry);
return _db.getEntries().remove(entry);
}
public void replaceEntry(DatabaseEntry entry) {
public DatabaseEntry replaceEntry(DatabaseEntry entry) {
assertState(false, true);
_db.replaceEntry(entry);
return _db.getEntries().replace(entry);
}
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
assertState(false, true);
_db.swapEntries(entry1, entry2);
_db.getEntries().swap(entry1, entry2);
}
public List<DatabaseEntry> getEntries() {
public boolean isEntryDuplicate(DatabaseEntry entry) {
assertState(false, true);
return _db.getEntries();
return _db.getEntries().has(entry);
}
public DatabaseEntry getEntryByUUID(UUID uuid) {
public Collection<DatabaseEntry> getEntries() {
assertState(false, true);
return _db.getEntryByUUID(uuid);
return _db.getEntries().getValues();
}
public TreeSet<String> getGroups() {
@@ -4,17 +4,17 @@ import com.beemdevelopment.aegis.crypto.CryptParameters;
import java.util.UUID;
public class FingerprintSlot extends RawSlot {
public FingerprintSlot() {
public class BiometricSlot extends RawSlot {
public BiometricSlot() {
super();
}
FingerprintSlot(UUID uuid, byte[] key, CryptParameters keyParams) {
BiometricSlot(UUID uuid, byte[] key, CryptParameters keyParams) {
super(uuid, key, keyParams);
}
@Override
public byte getType() {
return TYPE_FINGERPRINT;
return TYPE_BIOMETRIC;
}
}
@@ -7,12 +7,12 @@ import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.Serializable;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@@ -25,26 +25,29 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public abstract class Slot implements Serializable {
public abstract class Slot extends UUIDMap.Value {
public final static byte TYPE_RAW = 0x00;
public final static byte TYPE_DERIVED = 0x01;
public final static byte TYPE_FINGERPRINT = 0x02;
public final static byte TYPE_BIOMETRIC = 0x02;
private UUID _uuid;
private byte[] _encryptedMasterKey;
private CryptParameters _encryptedMasterKeyParams;
protected Slot() {
_uuid = UUID.randomUUID();
super();
}
protected Slot(UUID uuid, byte[] key, CryptParameters keyParams) {
_uuid = uuid;
super(uuid);
_encryptedMasterKey = key;
_encryptedMasterKeyParams = keyParams;
}
// getKey decrypts the encrypted master key in this slot using the given cipher and returns it.
/**
* Decrypts the encrypted master key in this slot using the given cipher and returns it.
* @throws SlotException if a generic crypto operation error occurred.
* @throws SlotIntegrityException if an error occurred while verifying the integrity of the slot.
*/
public MasterKey getKey(Cipher cipher) throws SlotException, SlotIntegrityException {
try {
CryptResult res = CryptoUtils.decrypt(_encryptedMasterKey, cipher, _encryptedMasterKeyParams);
@@ -57,7 +60,10 @@ public abstract class Slot implements Serializable {
}
}
// setKey encrypts the given master key using the given cipher and stores the result in this slot.
/**
* Encrypts the given master key using the given cipher and stores the result in this slot.
* @throws SlotException if a generic crypto operation error occurred.
*/
public void setKey(MasterKey masterKey, Cipher cipher) throws SlotException {
try {
byte[] masterKeyBytes = masterKey.getBytes();
@@ -95,7 +101,7 @@ public abstract class Slot implements Serializable {
try {
JSONObject obj = new JSONObject();
obj.put("type", getType());
obj.put("uuid", _uuid.toString());
obj.put("uuid", getUUID().toString());
obj.put("key", Hex.encode(_encryptedMasterKey));
obj.put("key_params", _encryptedMasterKeyParams.toJson());
return obj;
@@ -132,8 +138,8 @@ public abstract class Slot implements Serializable {
boolean repaired = obj.optBoolean("repaired", false);
slot = new PasswordSlot(uuid, key, keyParams, scryptParams, repaired);
break;
case Slot.TYPE_FINGERPRINT:
slot = new FingerprintSlot(uuid, key, keyParams);
case Slot.TYPE_BIOMETRIC:
slot = new BiometricSlot(uuid, key, keyParams);
break;
default:
throw new SlotException("unrecognized slot type");
@@ -146,8 +152,4 @@ public abstract class Slot implements Serializable {
}
public abstract byte getType();
public UUID getUUID() {
return _uuid;
}
}
@@ -1,18 +1,15 @@
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
public class SlotList implements Iterable<Slot>, Serializable {
private List<Slot> _slots = new ArrayList<>();
public class SlotList extends UUIDMap<Slot> {
public JSONArray toJson() {
JSONArray array = new JSONArray();
for (Slot slot : this) {
@@ -38,23 +35,6 @@ public class SlotList implements Iterable<Slot>, Serializable {
return slots;
}
public void add(Slot slot) {
for (Slot s : this) {
if (s.getUUID().equals(slot.getUUID())) {
throw new AssertionError("slot found with the same uuid");
}
}
_slots.add(slot);
}
public void remove(Slot slot) {
_slots.remove(slot);
}
public int size() {
return _slots.size();
}
public <T extends Slot> T find(Class<T> type) {
for (Slot slot : this) {
if (slot.getClass() == type) {
@@ -77,31 +57,4 @@ public class SlotList implements Iterable<Slot>, Serializable {
public <T extends Slot> boolean has(Class<T> type) {
return find(type) != null;
}
public void replace(Slot newSlot) {
Slot oldSlot = mustGetByUUID(newSlot.getUUID());
_slots.set(_slots.indexOf(oldSlot), newSlot);
}
public Slot getByUUID(UUID uuid) {
for (Slot slot : _slots) {
if (slot.getUUID().equals(uuid)) {
return slot;
}
}
return null;
}
private Slot mustGetByUUID(UUID uuid) {
Slot slot = getByUUID(uuid);
if (slot == null) {
throw new AssertionError(String.format("no slot found with UUID: %s", uuid.toString()));
}
return slot;
}
@Override
public Iterator<Slot> iterator() {
return _slots.iterator();
}
}
@@ -0,0 +1,136 @@
package com.beemdevelopment.aegis.helpers;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.slots.BiometricSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
/**
* A class that can prepare initialization of a BiometricSlot by generating a new
* key in the Android KeyStore and authenticating a cipher for it through a
* BiometricPrompt.
*/
public class BiometricSlotInitializer extends BiometricPrompt.AuthenticationCallback {
private BiometricSlot _slot;
private Listener _listener;
private BiometricPrompt _prompt;
public BiometricSlotInitializer(Fragment fragment, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(fragment, new UiThreadExecutor(), this);
}
public BiometricSlotInitializer(FragmentActivity activity, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(activity, new UiThreadExecutor(), this);
}
/**
* Generates a new key in the Android KeyStore for the new BiometricSlot,
* initializes a cipher with it and shows a BiometricPrompt to the user for
* authentication. If authentication is successful, the new slot will be
* initialized and delivered back through the listener.
*/
public void authenticate(BiometricPrompt.PromptInfo info) {
if (_slot != null) {
throw new IllegalStateException("Biometric authentication already in progress");
}
KeyStoreHandle keyStore;
try {
keyStore = new KeyStoreHandle();
} catch (KeyStoreHandleException e) {
fail(e);
return;
}
// generate a new Android KeyStore key
// and assign it the UUID of the new slot as an alias
Cipher cipher;
BiometricSlot slot = new BiometricSlot();
try {
SecretKey key = keyStore.generateKey(slot.getUUID().toString());
cipher = Slot.createEncryptCipher(key);
} catch (KeyStoreHandleException | SlotException e) {
fail(e);
return;
}
_slot = slot;
_prompt.authenticate(info, new BiometricPrompt.CryptoObject(cipher));
}
/**
* Cancels the BiometricPrompt and resets the state of the initializer. It will
* also attempt to delete the previously generated Android KeyStore key.
*/
public void cancelAuthentication() {
if (_slot == null) {
throw new IllegalStateException("Biometric authentication not in progress");
}
reset();
_prompt.cancelAuthentication();
}
private void reset() {
if (_slot != null) {
try {
// clean up the unused KeyStore key
// this is non-critical, so just fail silently if an error occurs
String uuid = _slot.getUUID().toString();
KeyStoreHandle keyStore = new KeyStoreHandle();
if (keyStore.containsKey(uuid)) {
keyStore.deleteKey(uuid);
}
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
_slot = null;
}
}
private void fail(int errorCode, CharSequence errString) {
reset();
_listener.onSlotInitializationFailed(errorCode, errString);
}
private void fail(Exception e) {
e.printStackTrace();
fail(0, e.toString());
}
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
fail(errorCode, errString.toString());
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
_listener.onInitializeSlot(_slot, Objects.requireNonNull(result.getCryptoObject()).getCipher());
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
public interface Listener {
void onInitializeSlot(BiometricSlot slot, Cipher cipher);
void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString);
}
}
@@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.biometric.BiometricConstants;
import androidx.biometric.BiometricManager;
public class BiometricsHelper {
private BiometricsHelper() {
}
public static BiometricManager getManager(Context context) {
BiometricManager manager = BiometricManager.from(context);
if (manager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
return manager;
}
return null;
}
public static boolean isCanceled(int errorCode) {
return errorCode == BiometricConstants.ERROR_CANCELED
|| errorCode == BiometricConstants.ERROR_USER_CANCELED
|| errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON;
}
public static boolean isAvailable(Context context) {
return getManager(context) != null;
}
}
@@ -1,26 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.Manifest;
import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
public class FingerprintHelper {
private FingerprintHelper() {
}
public static FingerprintManager getManager(Context context) {
if (isSupported() && PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
return manager;
}
}
return null;
}
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}
@@ -1,156 +0,0 @@
// This file was originally taken from https://github.com/googlesamples/android-FingerprintDialog/blob/2feb02945ae220ebd1bc2c2b620a1d43e30daea8/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintUiHelper.java
// It has been modified to suit Aegis' needs
/*
* Copyright (C) 2015 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
*/
package com.beemdevelopment.aegis.helpers;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.mattprecious.swirl.SwirlView;
import androidx.annotation.RequiresApi;
/**
* Small helper class to manage text/icon around fingerprint authentication UI.
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback {
private static final long ERROR_TIMEOUT_MILLIS = 1600;
private static final long SUCCESS_DELAY_MILLIS = 100;
private final FingerprintManager mFingerprintManager;
private final SwirlView mIcon;
private final TextView mErrorTextView;
private final Callback mCallback;
private CancellationSignal mCancellationSignal;
private boolean mSelfCancelled;
/**
* Constructor for {@link FingerprintUiHelper}.
*/
public FingerprintUiHelper(FingerprintManager fingerprintManager,
SwirlView icon, TextView errorTextView, Callback callback) {
mFingerprintManager = fingerprintManager;
mIcon = icon;
mErrorTextView = errorTextView;
mCallback = callback;
}
public boolean isFingerprintAuthAvailable() {
// The line below prevents the false positive inspection from Android Studio
// noinspection ResourceType
return mFingerprintManager.isHardwareDetected()
&& mFingerprintManager.hasEnrolledFingerprints();
}
public void startListening(FingerprintManager.CryptoObject cryptoObject) {
if (!isFingerprintAuthAvailable()) {
return;
}
mCancellationSignal = new CancellationSignal();
mSelfCancelled = false;
// The line below prevents the false positive inspection from Android Studio
// noinspection ResourceType
mFingerprintManager
.authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null);
mIcon.setState(SwirlView.State.ON);
}
public void stopListening() {
if (mCancellationSignal != null) {
mSelfCancelled = true;
mCancellationSignal.cancel();
mCancellationSignal = null;
}
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
if (!mSelfCancelled) {
showError(errString);
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onError();
}
}, ERROR_TIMEOUT_MILLIS);
}
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
showError(helpString);
}
@Override
public void onAuthenticationFailed() {
showError(mIcon.getResources().getString(
R.string.fingerprint_not_recognized));
}
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mIcon.setState(SwirlView.State.OFF);
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_success));
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onAuthenticated();
}
}, SUCCESS_DELAY_MILLIS);
// ugly hack to keep the fingerprint icon visible while also giving visual feedback of success to the user
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mIcon.setState(SwirlView.State.ON);
}
}, 500);
}
private void showError(CharSequence error) {
mIcon.setState(SwirlView.State.ERROR);
mErrorTextView.setText(error);
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
}
private Runnable mResetErrorTextRunnable = new Runnable() {
@Override
public void run() {
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_hint));
mIcon.setState(SwirlView.State.ON);
}
};
public interface Callback {
void onAuthenticated();
void onError();
}
}
@@ -3,8 +3,15 @@ package com.beemdevelopment.aegis.helpers;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
import java.util.Map;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private DatabaseEntry _selectedEntry;
private final ItemTouchHelperAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
@@ -22,6 +29,10 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
_isLongPressDragEnabled = enabled;
}
public void setSelectedEntry(DatabaseEntry entry) {
_selectedEntry = entry;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
@@ -30,7 +41,15 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
int swipeFlags = 0;
int position = viewHolder.getAdapterPosition();
EntryAdapter adapter = (EntryAdapter)recyclerView.getAdapter();
if (adapter.getEntryAt(position) != _selectedEntry)
{
dragFlags = 0;
}
return makeMovementFlags(dragFlags, swipeFlags);
}
@@ -56,4 +75,6 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
_positionChanged = false;
}
}
}
@@ -0,0 +1,23 @@
package com.beemdevelopment.aegis.helpers;
import android.content.res.Resources;
import android.graphics.Color;
import android.util.TypedValue;
import androidx.annotation.ColorInt;
import com.beemdevelopment.aegis.R;
public class ThemeHelper {
private ThemeHelper() {
}
public static int getThemeColor(int attributeId, Resources.Theme currentTheme) {
TypedValue typedValue = new TypedValue();
currentTheme.resolveAttribute(attributeId, typedValue, true);
@ColorInt int color = typedValue.data;
return color;
}
}
@@ -12,6 +12,11 @@ public class UiRefresher {
_handler = new Handler();
}
public void destroy() {
stop();
_listener = null;
}
public void start() {
if (_running) {
return;
@@ -22,15 +27,14 @@ public class UiRefresher {
_handler.postDelayed(new Runnable() {
@Override
public void run() {
if (_running) {
_listener.onRefresh();
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
}
_listener.onRefresh();
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
}
}, _listener.getMillisTillNextRefresh());
}
public void stop() {
_handler.removeCallbacksAndMessages(null);
_running = false;
}
@@ -0,0 +1,17 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import java.util.concurrent.Executor;
public class UiThreadExecutor implements Executor {
private final Handler _handler = new Handler(Looper.getMainLooper());
@Override
public void execute(@NonNull Runnable command) {
_handler.post(command);
}
}
@@ -32,6 +32,7 @@ public class AegisImporter extends DatabaseImporter {
throw new UnsupportedOperationException();
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
try {
byte[] bytes = reader.readAll();
@@ -2,6 +2,9 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
@@ -20,11 +23,14 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
@@ -32,9 +38,18 @@ import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AndOtpImporter extends DatabaseImporter {
private static final int INT_SIZE = 4;
private static final int NONCE_SIZE = 12;
private static final int TAG_SIZE = 16;
private static final int SALT_SIZE = 12;
private static final int KEY_SIZE = 256; // bits
private static final int MAX_ITERATIONS = 10000;
public AndOtpImporter(Context context) {
super(context);
@@ -50,6 +65,7 @@ public class AndOtpImporter extends DatabaseImporter {
throw new UnsupportedOperationException();
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
byte[] bytes;
try {
@@ -80,45 +96,76 @@ public class AndOtpImporter extends DatabaseImporter {
_data = data;
}
public DecryptedState decrypt(char[] password) throws DatabaseImporterException {
private DecryptedState decrypt(char[] password, boolean oldFormat) throws DatabaseImporterException {
try {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support encrypted andOTP backups
// it is not a secure way to derive a key from a password
MessageDigest hash = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
SecretKey key;
int offset = 0;
if (oldFormat) {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support the old andOTP backup format
// it is not a secure way to derive a key from a password
MessageDigest hash = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
key = new SecretKeySpec(keyBytes, "AES");
} else {
offset = INT_SIZE + SALT_SIZE;
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1 || iterations > MAX_ITERATIONS) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, iterations, KEY_SIZE);
key = factory.generateSecret(spec);
}
// extract nonce and tag
byte[] nonce = Arrays.copyOfRange(_data, 0, CryptoUtils.CRYPTO_AEAD_NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - CryptoUtils.CRYPTO_AEAD_TAG_SIZE, _data.length);
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
CryptParameters params = new CryptParameters(nonce, tag);
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
int offset = CryptoUtils.CRYPTO_AEAD_NONCE_SIZE;
int len = _data.length - CryptoUtils.CRYPTO_AEAD_NONCE_SIZE - CryptoUtils.CRYPTO_AEAD_TAG_SIZE;
CryptResult result = CryptoUtils.decrypt(_data, offset, len, cipher, params);
int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
return read(result.getData());
} catch (IOException | BadPaddingException | JSONException e) {
throw new DatabaseImporterException(e);
} catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| InvalidKeySpecException
| NoSuchPaddingException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, password -> {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
String[] choices = new String[]{
context.getResources().getString(R.string.andotp_new_format),
context.getResources().getString(R.string.andotp_old_format)
};
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
.setTitle(R.string.choose_andotp_importer)
.setSingleChoiceItems(choices, 0, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Dialogs.showPasswordInputDialog(context, password -> {
try {
DecryptedState state = decrypt(password, i != 0);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
})
.create());
}
}
@@ -0,0 +1,133 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.util.Xml;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.PreferenceParser;
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.IOException;
public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml";
private static final String _pkgName = "com.authy.authy";
public AuthyImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(reader.getStream(), null);
parser.nextTag();
JSONArray array = new JSONArray();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals("com.authy.storage.tokens.authenticator.key")) {
array = new JSONArray(entry.Value);
}
}
return new State(array);
} catch (XmlPullParserException | JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private JSONArray _obj;
private State(JSONArray obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
for (int i = 0; i < _obj.length(); i++) {
JSONObject entryObj = _obj.getJSONObject(i);
try {
DatabaseEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static DatabaseEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
authyEntryInfo.OriginalName = entry.getString("originalName");
authyEntryInfo.AccountType = entry.getString("accountType");
authyEntryInfo.Name = entry.optString("name");
sanitizeEntryInfo(authyEntryInfo);
int digits = entry.getInt("digits");
byte[] secret = Base32.decode(entry.getString("decryptedSecret").toCharArray());
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
return new DatabaseEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
} catch (OtpInfoException | JSONException | Base32Exception e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
private static void sanitizeEntryInfo(AuthyEntryInfo info) {
String seperator = "";
if (info.OriginalName.contains(":")) {
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
seperator = ":";
} else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
seperator = " - ";
} else {
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
}
info.Name = info.Name.replace(info.Issuer + seperator, "");
}
}
private static class AuthyEntryInfo {
String OriginalName;
String AccountType;
String Issuer;
String Name;
}
}
@@ -6,6 +6,7 @@ import android.net.Uri;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
@@ -31,13 +32,18 @@ public abstract class DatabaseImporter {
// note: keep these lists sorted alphabetically
_importers = new LinkedHashMap<>();
_importers.put("Aegis", AegisImporter.class);
_importers.put("Authy", AuthyImporter.class);
_importers.put("andOTP", AndOtpImporter.class);
_importers.put("FreeOTP", FreeOtpImporter.class);
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
_importers.put("Google Authenticator", GoogleAuthImporter.class);
_importers.put("Steam", SteamImporter.class);
_importers.put("WinAuth", WinAuthImporter.class);
_appImporters = new LinkedHashMap<>();
_appImporters.put("Authy", AuthyImporter.class);
_appImporters.put("FreeOTP", FreeOtpImporter.class);
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
_appImporters.put("Steam", SteamImporter.class);
}
@@ -111,7 +117,7 @@ public abstract class DatabaseImporter {
}
public static class Result {
private List<DatabaseEntry> _entries = new ArrayList<>();
private UUIDMap<DatabaseEntry> _entries = new UUIDMap<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(DatabaseEntry entry) {
@@ -122,7 +128,7 @@ public abstract class DatabaseImporter {
_errors.add(error);
}
public List<DatabaseEntry> getEntries() {
public UUIDMap<DatabaseEntry> getEntries() {
return _entries;
}
@@ -131,43 +137,35 @@ public abstract class DatabaseImporter {
}
}
public static class FileReader implements Closeable {
public static class FileReader {
private InputStream _stream;
private boolean _internal;
private FileReader(InputStream stream) {
public FileReader(InputStream stream) {
this(stream, false);
}
public FileReader(InputStream stream, boolean internal) {
_stream = stream;
}
public static FileReader open(String filename)
throws FileNotFoundException {
FileInputStream stream = new FileInputStream(filename);
return new FileReader(stream);
}
public static FileReader open(SuFile file)
throws FileNotFoundException {
SuFileInputStream stream = new SuFileInputStream(file);
return new FileReader(stream);
}
public static FileReader open(Context context, Uri uri)
throws FileNotFoundException {
InputStream stream = context.getContentResolver().openInputStream(uri);
return new FileReader(stream);
_internal = internal;
}
public byte[] readAll() throws IOException {
ByteInputStream stream = ByteInputStream.create(_stream);
return stream.getBytes();
try (ByteInputStream stream = ByteInputStream.create(_stream)) {
return stream.getBytes();
}
}
public InputStream getStream() {
return _stream;
}
@Override
public void close() throws IOException {
_stream.close();
/**
* Reports whether this reader reads the internal state of an app.
* @return true if reading from internal file, false if reading from external file
*/
public boolean isInternal() {
return _internal;
}
}
@@ -7,7 +7,9 @@ import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.PreferenceParser;
import org.json.JSONArray;
import org.json.JSONException;
@@ -37,6 +39,7 @@ public class FreeOtpImporter extends DatabaseImporter {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
@@ -44,17 +47,22 @@ public class FreeOtpImporter extends DatabaseImporter {
parser.setInput(reader.getStream(), null);
parser.nextTag();
List<XmlEntry> entries = parse(parser);
List<JSONObject> entries = new ArrayList<>();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (!entry.Name.equals("tokenOrder")) {
entries.add(new JSONObject(entry.Value));
}
}
return new State(entries);
} catch (XmlPullParserException | IOException e) {
} catch (XmlPullParserException | IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private List<XmlEntry> _entries;
private List<JSONObject> _entries;
private State(List<XmlEntry> entries) {
public State(List<JSONObject> entries) {
super(false);
_entries = entries;
}
@@ -63,34 +71,37 @@ public class FreeOtpImporter extends DatabaseImporter {
public Result convert() {
Result result = new Result();
for (XmlEntry xmlEntry : _entries) {
// TODO: order
if (!xmlEntry.Name.equals("tokenOrder")) {
try {
DatabaseEntry entry = convertEntry(xmlEntry);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
for (JSONObject obj : _entries) {
try {
DatabaseEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static DatabaseEntry convertEntry(XmlEntry entry) throws DatabaseImporterEntryException {
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
JSONObject obj = new JSONObject(entry.Value);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
String issuer = obj.getString("issuerExt");
String name = obj.optString("label");
OtpInfo info;
switch (type) {
case "totp":
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
int period = obj.getInt("period");
if (issuer.equals("Steam")) {
info = new SteamInfo(secret, algo, digits, period);
} else {
info = new TotpInfo(secret, algo, digits, period);
}
break;
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
@@ -99,18 +110,16 @@ public class FreeOtpImporter extends DatabaseImporter {
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String issuer = obj.getString("issuerExt");
String name = obj.optString("label");
return new DatabaseEntry(info, name, issuer);
} catch (DatabaseImporterException | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, entry.Value);
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
private static List<XmlEntry> parse(XmlPullParser parser)
throws IOException, XmlPullParserException {
List<XmlEntry> entries = new ArrayList<>();
private static List<JSONObject> parseXml(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException {
List<JSONObject> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
@@ -123,7 +132,10 @@ public class FreeOtpImporter extends DatabaseImporter {
continue;
}
entries.add(parseEntry(parser));
JSONObject entry = parseXmlEntry(parser);
if (entry != null) {
entries.add(entry);
}
}
return entries;
@@ -137,19 +149,21 @@ public class FreeOtpImporter extends DatabaseImporter {
return bytes;
}
private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
private static JSONObject parseXmlEntry(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException {
parser.require(XmlPullParser.START_TAG, null, "string");
String name = parser.getAttributeValue(null, "name");
String value = parseText(parser);
String value = parseXmlText(parser);
parser.require(XmlPullParser.END_TAG, null, "string");
XmlEntry entry = new XmlEntry();
entry.Name = name;
entry.Value = value;
return entry;
if (name.equals("tokenOrder")) {
return null;
}
return new JSONObject(value);
}
private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {
private static String parseXmlText(XmlPullParser parser) throws IOException, XmlPullParserException {
String text = "";
if (parser.next() == XmlPullParser.TEXT) {
text = parser.getText();
@@ -176,9 +190,4 @@ public class FreeOtpImporter extends DatabaseImporter {
}
}
}
private static class XmlEntry {
String Name;
String Value;
}
}
@@ -0,0 +1,57 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class FreeOtpPlusImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
private static final String _pkgName = "org.liberty.android.freeotpplus";
public FreeOtpPlusImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
State state;
if (reader.isInternal()) {
state = new FreeOtpImporter(getContext()).read(reader);
} else {
try {
String json = new String(reader.readAll(), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
JSONArray array = obj.getJSONArray("tokens");
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
entries.add(array.getJSONObject(i));
}
state = new FreeOtpImporter.State(entries);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
return state;
}
}
@@ -43,6 +43,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
File file;
@@ -43,6 +43,7 @@ public class SteamImporter extends DatabaseImporter {
return new SuFile(_subDir, files[0].getName()).getPath();
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) {
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8));
@@ -0,0 +1,83 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
public class WinAuthImporter extends DatabaseImporter {
public WinAuthImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return null;
}
@Override
protected String getAppSubPath() {
return null;
}
@Override
public WinAuthImporter.State read(FileReader reader) throws DatabaseImporterException {
ArrayList<String> lines = new ArrayList<>();
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(reader.getStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
lines.add(line);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
return new State(lines);
}
public static class State extends DatabaseImporter.State {
private ArrayList<String> _lines;
private State(ArrayList<String> lines) {
super(false);
_lines = lines;
}
@Override
public Result convert() {
Result result = new Result();
for (String line : _lines) {
try {
DatabaseEntry entry = convertEntry(line);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static DatabaseEntry convertEntry(String line) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
DatabaseEntry databaseEntry = new DatabaseEntry(info);
databaseEntry.setIssuer(databaseEntry.getName());
databaseEntry.setName("WinAuth");
return databaseEntry;
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, line);
}
}
}
}
@@ -55,7 +55,12 @@ public class TotpInfo extends OtpInfo {
}
public static boolean isPeriodValid(int period) {
return period > 0;
if (period <= 0) {
return false;
}
// check for the possibility of an overflow when converting to milliseconds
return period <= Integer.MAX_VALUE / 1000;
}
public void setPeriod(int period) throws OtpInfoException {
@@ -0,0 +1,61 @@
package com.beemdevelopment.aegis.services;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.R;
public class NotificationService extends Service {
public static final int DATABASE_UNLOCKED_ID = 1;
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private static final String CODE_LOCK_DATABASE_ACTION = "lock_database";
public NotificationService() {
}
@Override
public int onStartCommand(Intent intent,int flags, int startId){
super.onStartCommand(intent, flags, startId);
serviceMethod();
return Service.START_STICKY;
}
public void serviceMethod() {
Intent intentAction = new Intent(CODE_LOCK_DATABASE_ACTION);
PendingIntent lockDatabaseIntent = PendingIntent.getBroadcast(this,1,intentAction, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CODE_LOCK_STATUS_ID)
.setSmallIcon(R.drawable.ic_fingerprint_black_24dp)
.setContentTitle(getString(R.string.app_name_full))
.setContentText(getString(R.string.vault_unlocked_state))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setOngoing(true)
.setContentIntent(lockDatabaseIntent);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(DATABASE_UNLOCKED_ID, builder.build());
}
@Override
public void onDestroy() {
super.onDestroy();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.cancel(DATABASE_UNLOCKED_ID);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
@@ -0,0 +1,128 @@
package com.beemdevelopment.aegis.ui;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.core.view.LayoutInflaterCompat;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.ui.glide.GlideLicense;
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
import de.psdev.licensesdialog.LicenseResolver;
import de.psdev.licensesdialog.LicensesDialog;
public class AboutActivity extends AegisActivity {
private static String GITHUB = "https://github.com/beemdevelopment/Aegis";
private static String WEBSITE_ALEXANDER = "https://alexbakker.me";
private static String GITHUB_MICHAEL = "https://github.com/michaelschattgen";
private static String MAIL_BEEMDEVELOPMENT = "beemdevelopment@gmail.com";
private static String WEBSITE_BEEMDEVELOPMENT = "https://beem.dev/";
private static String PLAYSTORE_BEEMDEVELOPMENT = "https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis";
@Override
protected void onCreate(Bundle savedInstanceState) {
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new IconicsLayoutInflater2(getDelegate()));
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
View btnLicenses = findViewById(R.id.btn_licenses);
btnLicenses.setOnClickListener(v -> showLicenseDialog());
TextView appVersion = findViewById(R.id.app_version);
appVersion.setText(getCurrentAppVersion());
View btnAppVersion = findViewById(R.id.btn_app_version);
btnAppVersion.setOnClickListener(v -> {
copyToClipboard(getCurrentAppVersion(), R.string.version_copied);
});
View btnGithub = findViewById(R.id.btn_github);
btnGithub.setOnClickListener(v -> openUrl(GITHUB));
View btnAlexander = findViewById(R.id.btn_alexander);
btnAlexander.setOnClickListener(v -> openUrl(WEBSITE_ALEXANDER));
View btnMichael = findViewById(R.id.btn_michael);
btnMichael.setOnClickListener(v -> openUrl(GITHUB_MICHAEL));
View btnMail = findViewById(R.id.btn_email);
btnMail.setOnClickListener(v -> openMail(MAIL_BEEMDEVELOPMENT));
View btnWebsite = findViewById(R.id.btn_website);
btnWebsite.setOnClickListener(v -> openUrl(WEBSITE_BEEMDEVELOPMENT));
View btnRate = findViewById(R.id.btn_rate);
btnRate.setOnClickListener(v -> openUrl(PLAYSTORE_BEEMDEVELOPMENT ));
View btnChangelog = findViewById(R.id.btn_changelog);
btnChangelog.setOnClickListener(v -> {
ChangelogDialog.create().setTheme(getCurrentTheme()).show(getSupportFragmentManager(), "CHANGELOG_DIALOG");
});
}
private static String getCurrentAppVersion() {
if (BuildConfig.DEBUG) {
return String.format("%s-%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.GIT_HASH, BuildConfig.GIT_BRANCH);
}
return BuildConfig.VERSION_NAME;
}
private void openUrl(String url) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW);
browserIntent.setData(Uri.parse(url));
browserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(browserIntent);
}
private void copyToClipboard(String text, @StringRes int messageId) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData data = ClipData.newPlainText("text/plain", text);
clipboard.setPrimaryClip(data);
Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show();
}
private void openMail(String mailaddress) {
Intent mailIntent = new Intent(Intent.ACTION_SENDTO);
mailIntent.setData(Uri.parse("mailto:" + mailaddress));
mailIntent.putExtra(Intent.EXTRA_EMAIL, mailaddress);
mailIntent.putExtra(Intent.EXTRA_SUBJECT, R.string.app_name_full);
startActivity(Intent.createChooser(mailIntent, this.getString(R.string.email)));
}
private void showLicenseDialog() {
String stylesheet = getString(R.string.custom_notices_format_style);
int backgroundColorResource = getCurrentTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
String backgroundColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(backgroundColorResource, getTheme())));
String textColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getTheme())));
String licenseColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, getTheme())));
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor);
LicenseResolver.registerLicense(new GlideLicense());
new LicensesDialog.Builder(this)
.setNotices(R.raw.notices)
.setTitle(R.string.licenses)
.setNoticesCssStyle(stylesheet)
.setIncludeOwnLicense(true)
.build()
.show();
}
}
@@ -2,6 +2,7 @@ package com.beemdevelopment.aegis.ui;
import android.animation.ValueAnimator;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
@@ -16,16 +17,21 @@ import com.beemdevelopment.aegis.Theme;
import androidx.annotation.CallSuper;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Locale;
public abstract class AegisActivity extends AppCompatActivity implements AegisApplication.LockListener {
private boolean _resumed;
private AegisApplication _app;
private Theme _currentTheme;
@Override
protected void onCreate(Bundle savedInstanceState) {
_app = (AegisApplication) getApplication();
// set the theme and create the activity
setPreferredTheme(getPreferences().getCurrentTheme());
// set the theme and locale before creating the activity
Preferences prefs = getPreferences();
setPreferredTheme(prefs.getCurrentTheme());
setLocale(prefs.getLocale());
super.onCreate(savedInstanceState);
// if the app was killed, relaunch MainActivity and close everything else
@@ -84,6 +90,8 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
protected void setPreferredTheme(Theme theme) {
_currentTheme = theme;
switch (theme) {
case LIGHT:
setTheme(R.style.AppTheme);
@@ -97,6 +105,15 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
}
protected void setLocale(Locale locale) {
Locale.setDefault(locale);
Configuration config = new Configuration();
config.locale = locale;
getBaseContext().getResources().updateConfiguration(config, getBaseContext().getResources().getDisplayMetrics());
}
/**
* Reports whether this Activity has been resumed. (i.e. onResume was called)
*/
@@ -122,4 +139,8 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
}
}
protected Theme getCurrentTheme() {
return _currentTheme;
}
}
@@ -2,56 +2,55 @@ package com.beemdevelopment.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.BiometricSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
import com.beemdevelopment.aegis.ui.tasks.SlotListTask;
import com.mattprecious.swirl.SwirlView;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.appcompat.app.AlertDialog;
public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotListTask.Callback {
public class AuthActivity extends AegisActivity implements SlotListTask.Callback {
private EditText _textPassword;
private CancelAction _cancelAction;
private SlotList _slots;
private FingerprintUiHelper _fingerHelper;
private FingerprintManager.CryptoObject _fingerCryptoObj;
private BiometricPrompt.CryptoObject _bioCryptoObj;
private BiometricPrompt _bioPrompt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_auth);
_textPassword = findViewById(R.id.text_password);
LinearLayout boxFingerprint = findViewById(R.id.box_fingerprint);
LinearLayout boxFingerprintInfo = findViewById(R.id.box_fingerprint_info);
TextView textFingerprint = findViewById(R.id.text_fingerprint);
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
Button decryptButton = findViewById(R.id.button_decrypt);
Button biometricsButton = findViewById(R.id.button_biometrics);
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
@@ -60,23 +59,17 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
return false;
});
SwirlView imgFingerprint = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewGroup insertPoint = findViewById(R.id.img_fingerprint_insert);
imgFingerprint = new SwirlView(this);
insertPoint.addView(imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
Intent intent = getIntent();
_slots = (SlotList) intent.getSerializableExtra("slots");
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
// only show the fingerprint controls if the api version is new enough, permission is granted, a scanner is found and a fingerprint slot is found
FingerprintManager manager = FingerprintHelper.getManager(this);
if (manager != null && _slots.has(FingerprintSlot.class)) {
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
boolean invalidated = false;
try {
// find a fingerprint slot with an id that matches an alias in the keystore
for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
// find a biometric slot with an id that matches an alias in the keystore
for (BiometricSlot slot : _slots.findAll(BiometricSlot.class)) {
String id = slot.getUUID().toString();
KeyStoreHandle handle = new KeyStoreHandle();
if (handle.containsKey(id)) {
@@ -86,10 +79,11 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
invalidated = true;
continue;
}
Cipher cipher = slot.createDecryptCipher(key);
_fingerCryptoObj = new FingerprintManager.CryptoObject(cipher);
_fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
boxFingerprint.setVisibility(View.VISIBLE);
_bioCryptoObj = new BiometricPrompt.CryptoObject(cipher);
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
biometricsButton.setVisibility(View.VISIBLE);
invalidated = false;
break;
}
@@ -100,7 +94,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
// display a help message if a matching invalidated keystore entry was found
if (invalidated) {
boxFingerprintInfo.setVisibility(View.VISIBLE);
boxBiometricInfo.setVisibility(View.VISIBLE);
}
}
@@ -115,7 +109,11 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
}
});
if (_fingerHelper == null) {
biometricsButton.setOnClickListener(v -> {
showBiometricPrompt();
});
if (_bioCryptoObj == null) {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
@@ -138,43 +136,49 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
private void selectPassword() {
_textPassword.selectAll();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
@Override
public void onBackPressed() {
// ignore back button presses
switch (_cancelAction) {
case KILL:
finishAffinity();
case CLOSE:
Intent intent = new Intent();
setResult(RESULT_CANCELED, intent);
finish();
}
}
@Override
public void onResume() {
super.onResume();
if (_fingerHelper != null) {
_fingerHelper.startListening(_fingerCryptoObj);
if (_bioPrompt != null) {
showBiometricPrompt();
}
}
public void showBiometricPrompt() {
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.authentication))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
_bioPrompt.authenticate(info, _bioCryptoObj);
}
@Override
public void onPause() {
super.onPause();
if (_fingerHelper != null) {
_fingerHelper.stopListening();
if (_bioPrompt != null) {
_bioPrompt.cancelAuthentication();
}
}
@Override
public void onAuthenticated() {
trySlots(FingerprintSlot.class, _fingerCryptoObj.getCipher());
}
@Override
public void onError() {
}
@Override
public void onTaskFinished(SlotListTask.Result result) {
if (result != null) {
@@ -193,4 +197,25 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
showError();
}
}
private class BiometricPromptListener extends BiometricPrompt.AuthenticationCallback {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show();
}
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
trySlots(BiometricSlot.class, _bioCryptoObj.getCipher());
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
}
}
@@ -0,0 +1,90 @@
package com.beemdevelopment.aegis.ui;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Bundle;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
public class ChangelogDialog extends DialogFragment {
private Theme _themeStyle;
public static ChangelogDialog create() {
return new ChangelogDialog();
}
@SuppressLint("InflateParams")
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View customView;
try {
customView = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_web_view, null);
} catch (InflateException e) {
e.printStackTrace();
return new AlertDialog.Builder(getActivity())
.setTitle(android.R.string.dialog_alert_title)
.setMessage(getString(R.string.webview_error))
.setPositiveButton(android.R.string.ok, null)
.show();
}
AlertDialog dialog = new AlertDialog.Builder(getActivity())
.setTitle("Changelog")
.setView(customView)
.setPositiveButton(android.R.string.ok, null)
.show();
final WebView webView = customView.findViewById(R.id.web_view);
StringBuilder buf = new StringBuilder();
try (InputStream html = getActivity().getAssets().open("changelog.html")) {
BufferedReader in = new BufferedReader(new InputStreamReader(html, "UTF-8"));
String str;
while ((str = in.readLine()) != null)
buf.append(str);
in.close();
String changelog = buf.toString();
changelog = replaceStylesheet(changelog);
webView.loadData(changelog, "text/html", "UTF-8");
} catch (IOException e) {
webView.loadData("<h1>Unable to load</h1><p>" + e.getLocalizedMessage() + "</p>", "text/html", "UTF-8");
}
return dialog;
}
private String replaceStylesheet(String changelog) {
int backgroundColorResource = _themeStyle == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
String backgroundColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(backgroundColorResource, getContext().getTheme())));
String textColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getContext().getTheme())));
return String.format(changelog, backgroundColor, textColor);
}
public ChangelogDialog setTheme(Theme theme) {
_themeStyle = theme;
return this;
}
}
@@ -4,39 +4,33 @@ import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.hardware.fingerprint.FingerprintManager;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.NumberPicker;
import android.widget.TextView;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
import com.mattprecious.swirl.SwirlView;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
public class Dialogs {
private Dialogs() {
@@ -76,6 +70,18 @@ public class Dialogs {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
EditText textPassword = view.findViewById(R.id.text_password);
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
CheckBox switchToggleVisibility = view.findViewById(R.id.check_toggle_visibility);
switchToggleVisibility.setOnCheckedChangeListener((CompoundButton.OnCheckedChangeListener) (buttonView, isChecked) -> {
if (isChecked) {
textPassword.setTransformationMethod(null);
textPassword.clearFocus();
textPasswordConfirm.setEnabled(false);
} else {
textPassword.setTransformationMethod(new PasswordTransformationMethod());
textPasswordConfirm.setEnabled(true);
}
});
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_password)
@@ -92,7 +98,7 @@ public class Dialogs {
// replace the default listener
button.setOnClickListener(v -> {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm)) {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm) && !switchToggleVisibility.isChecked()) {
return;
}
@@ -117,7 +123,7 @@ public class Dialogs {
TextWatcher watcher = new TextWatcher() {
public void onTextChanged(CharSequence c, int start, int before, int count) {
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
buttonOK.get().setEnabled(equal);
buttonOK.get().setEnabled(equal || switchToggleVisibility.isChecked());
}
public void beforeTextChanged(CharSequence c, int start, int count, int after) {
@@ -178,51 +184,6 @@ public class Dialogs {
showSecureDialog(dialog);
}
public static void showFingerprintDialog(Activity activity, Dialogs.SlotListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_fingerprint, null);
TextView textFingerprint = view.findViewById(R.id.text_fingerprint);
SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint);
FingerprintManager.CryptoObject obj;
FingerprintSlot slot;
final AtomicReference<FingerprintUiHelper> helper = new AtomicReference<>();
FingerprintManager manager = FingerprintHelper.getManager(activity);
try {
slot = new FingerprintSlot();
SecretKey key = new KeyStoreHandle().generateKey(slot.getUUID().toString());
Cipher cipher = Slot.createEncryptCipher(key);
obj = new FingerprintManager.CryptoObject(cipher);
} catch (KeyStoreHandleException | SlotException e) {
throw new RuntimeException(e);
}
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.register_fingerprint)
.setView(view)
.setNegativeButton(android.R.string.cancel, null)
.setOnDismissListener(d -> {
helper.get().stopListening();
})
.create();
helper.set(new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, new FingerprintUiHelper.Callback() {
@Override
public void onAuthenticated() {
listener.onSlotResult(slot, obj.getCipher());
dialog.dismiss();
}
@Override
public void onError() {
}
}));
helper.get().startListening(obj);
showSecureDialog(dialog);
}
public interface NumberInputListener {
void onNumberInputResult(int number);
}
@@ -4,11 +4,8 @@ import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
@@ -39,14 +36,13 @@ 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.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.beemdevelopment.aegis.util.Cloner;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
@@ -54,6 +50,8 @@ import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import de.hdodenhof.circleimageview.CircleImageView;
@@ -109,6 +107,8 @@ public class EditEntryActivity extends AegisActivity {
setTitle(R.string.add_new_profile);
}
String selectedGroup = intent.getStringExtra("selectedGroup");
// set up fields
_iconView = findViewById(R.id.profile_drawable);
_kropView = findViewById(R.id.krop_view);
@@ -133,51 +133,49 @@ public class EditEntryActivity extends AegisActivity {
_advancedSettings = findViewById(R.id.expandableLayout);
// fill the fields with values if possible
if (_origEntry != null) {
if (_origEntry.hasIcon()) {
byte[] imageBytes = _origEntry.getIcon();
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
_iconView.setImageBitmap(bitmap);
_hasCustomIcon = true;
} else {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
}
_textName.setText(_origEntry.getName());
_textIssuer.setText(_origEntry.getIssuer());
OtpInfo info = _origEntry.getInfo();
if (info instanceof TotpInfo) {
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
_rowPeriod.setVisibility(View.VISIBLE);
} else if (info instanceof HotpInfo) {
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
_rowCounter.setVisibility(View.VISIBLE);
} else {
throw new RuntimeException();
}
_textDigits.setText(Integer.toString(info.getDigits()));
byte[] secretBytes = _origEntry.getInfo().getSecret();
if (secretBytes != null) {
char[] secretChars = Base32.encode(secretBytes);
_textSecret.setText(secretChars, 0, secretChars.length);
}
String type = _origEntry.getInfo().getType();
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
String algo = _origEntry.getInfo().getAlgorithm(false);
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
String group = _origEntry.getGroup();
if (group != null) {
int pos = _groups.contains(group) ? _groups.headSet(group).size() : -1;
_spinnerGroup.setSelection(pos + 1, false);
}
if (_origEntry.hasIcon()) {
Glide.with(this)
.asDrawable()
.load(_origEntry)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
_hasCustomIcon = true;
} else {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
}
_textName.setText(_origEntry.getName());
_textIssuer.setText(_origEntry.getIssuer());
OtpInfo info = _origEntry.getInfo();
if (info instanceof TotpInfo) {
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
_rowPeriod.setVisibility(View.VISIBLE);
} else if (info instanceof HotpInfo) {
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
_rowCounter.setVisibility(View.VISIBLE);
} else {
throw new RuntimeException();
}
_textDigits.setText(Integer.toString(info.getDigits()));
byte[] secretBytes = _origEntry.getInfo().getSecret();
if (secretBytes != null) {
char[] secretChars = Base32.encode(secretBytes);
_textSecret.setText(secretChars, 0, secretChars.length);
}
String type = _origEntry.getInfo().getType();
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
String algo = _origEntry.getInfo().getAlgorithm(false);
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
String group = _origEntry.getGroup();
setGroup(group);
// update the icon if the text changed
_textIssuer.addTextChangedListener(_iconChangeListener);
_textName.addTextChangedListener(_iconChangeListener);
@@ -252,9 +250,19 @@ public class EditEntryActivity extends AegisActivity {
// automatically open advanced settings since 'Secret' is required.
if (_isNew) {
openAdvancedSettings();
setGroup(selectedGroup);
}
}
private void setGroup(String groupName) {
if (groupName == null) {
return;
}
int pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() : -1;
_spinnerGroup.setSelection(pos + 1, false);
}
private void openAdvancedSettings() {
Animation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new AccelerateInterpolator());
@@ -321,7 +329,7 @@ public class EditEntryActivity extends AegisActivity {
}
// close the activity if the entry has not been changed
if (_origEntry != null && !_hasChangedIcon && _origEntry.equals(entry.get())) {
if (!_hasChangedIcon && _origEntry.equals(entry.get())) {
super.onBackPressed();
return;
}
@@ -391,24 +399,30 @@ public class EditEntryActivity extends AegisActivity {
@Override
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
Uri inputFile = (data.getData());
InputStream inputStream;
Bitmap bitmap;
try {
inputStream = getContentResolver().openInputStream(inputFile);
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
_kropView.setBitmap(bitmap);
_kropView.setVisibility(View.VISIBLE);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Glide.with(this)
.asBitmap()
.load(data.getData())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
_kropView.setBitmap(resource);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
_iconView.setVisibility(View.GONE);
_kropView.setVisibility(View.VISIBLE);
_saveImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
_iconView.setVisibility(View.VISIBLE);
_kropView.setVisibility(View.GONE);
_hasCustomIcon = true;
_hasChangedIcon = true;
@@ -449,7 +463,6 @@ public class EditEntryActivity extends AegisActivity {
throw new ParseException("Secret is not valid base32.");
}
// set otp info
OtpInfo info;
try {
switch (type.toLowerCase()) {
@@ -478,14 +491,8 @@ public class EditEntryActivity extends AegisActivity {
throw new ParseException("The entered info is incorrect: " + e.getMessage());
}
// set database entry info
DatabaseEntry entry;
if (_origEntry == null) {
entry = new DatabaseEntry(info);
} else {
entry = cloneEntry(_origEntry);
entry.setInfo(info);
}
DatabaseEntry entry = Cloner.clone(_origEntry);
entry.setInfo(info);
entry.setIssuer(_textIssuer.getText().toString());
entry.setName(_textName.getText().toString());
@@ -499,8 +506,9 @@ public class EditEntryActivity extends AegisActivity {
if (_hasChangedIcon) {
if (_hasCustomIcon) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
drawableToBitmap(_iconView.getDrawable()).compress(Bitmap.CompressFormat.JPEG, 100, stream);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
byte[] data = stream.toByteArray();
entry.setIcon(data);
} else {
@@ -560,41 +568,6 @@ public class EditEntryActivity extends AegisActivity {
return -1;
}
private static Bitmap drawableToBitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
final int width = !drawable.getBounds().isEmpty() ? drawable
.getBounds().width() : drawable.getIntrinsicWidth();
final int height = !drawable.getBounds().isEmpty() ? drawable
.getBounds().height() : drawable.getIntrinsicHeight();
final Bitmap bitmap = Bitmap.createBitmap(width <= 0 ? 1 : width,
height <= 0 ? 1 : height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
private static DatabaseEntry cloneEntry(DatabaseEntry entry) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(entry);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (DatabaseEntry) ois.readObject();
} catch (ClassNotFoundException | IOException e) {
throw new RuntimeException(e);
}
}
private static class ParseException extends Exception {
public ParseException(String message) {
super(message);
@@ -4,6 +4,8 @@ import android.content.Intent;
import android.os.Bundle;
import android.view.WindowManager;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.Database;
@@ -12,11 +14,10 @@ import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.BiometricSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
@@ -29,8 +30,6 @@ import org.json.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.fragment.app.Fragment;
public class IntroActivity extends AppIntro2 implements DerivationTask.Callback {
public static final int RESULT_OK = 0;
public static final int RESULT_EXCEPTION = 1;
@@ -65,7 +64,7 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
SliderPage homeSliderPage = new SliderPage();
homeSliderPage.setTitle(getString(R.string.welcome));
homeSliderPage.setImageDrawable(R.drawable.icon_foreground_white);
homeSliderPage.setImageDrawable(R.drawable.app_icon);
homeSliderPage.setTitleColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setDescription(getString(R.string.app_description));
homeSliderPage.setDescColor(getResources().getColor(R.color.primary_text_dark));
@@ -83,7 +82,7 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
SliderPage endSliderPage = new SliderPage();
endSliderPage.setTitle(getString(R.string.setup_completed));
endSliderPage.setDescription(getString(R.string.setup_completed_description));
endSliderPage.setImageDrawable(R.drawable.icon_foreground_white);
endSliderPage.setImageDrawable(R.drawable.app_icon);
endSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
_endSlide = AppIntroFragment.newInstance(endSliderPage);
addSlide(_endSlide);
@@ -135,7 +134,6 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
creds = new DatabaseFileCredentials();
}
SlotList slots = null;
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
// encrypt the master key with a key derived from the user's password
// and add it to the list of slots
@@ -150,18 +148,14 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
}
}
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_FINGER) {
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
BiometricSlot slot = _authenticatedSlide.getBiometricSlot();
try {
// encrypt the master key with the fingerprint key
// and add it to the list of slots
FingerprintSlot slot = _authenticatedSlide.getFingerSlot();
Cipher cipher = _authenticatedSlide.getFingerCipher();
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
slot.setKey(creds.getKey(), _authenticatedSlide.getBiometriCipher());
} catch (SlotException e) {
setException(e);
return;
}
creds.getSlots().add(slot);
}
// finally, save the database
@@ -11,15 +11,19 @@ import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
@@ -31,7 +35,6 @@ import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.ChecksumException;
import com.google.zxing.FormatException;
@@ -42,6 +45,7 @@ import com.google.zxing.RGBLuminanceSource;
import com.google.zxing.Reader;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import com.mikepenz.iconics.context.IconicsContextWrapper;
import java.io.IOException;
import java.io.InputStream;
@@ -62,21 +66,30 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
// permission request codes
private static final int CODE_PERM_CAMERA = 0;
private static final int CODE_PERM_READ_STORAGE = 1;
private AegisApplication _app;
private DatabaseManager _db;
private boolean _loaded;
private String _checkedGroup;
private String _selectedGroup;
private boolean _searchSubmitted;
private DatabaseEntry _selectedEntry;
private ActionMode _actionMode;
private Menu _menu;
private SearchView _searchView;
private FloatingActionsMenu _fabMenu;
private EntryListView _entryListView;
private FabScrollHelper _fabScrollHelper;
private ActionMode.Callback _actionModeCallbacks = new ActionModeCallbacks();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_app = (AegisApplication) getApplication();
_db = _app.getDatabaseManager();
_loaded = false;
@@ -88,6 +101,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
_entryListView.setListener(this);
_entryListView.setShowAccountName(getPreferences().isAccountNameVisible());
_entryListView.setSearchAccountName(getPreferences().isSearchAccountNameEnabled());
_entryListView.setHighlightEntry(getPreferences().isEntryHighlightEnabled());
_entryListView.setTapToReveal(getPreferences().isTapToRevealEnabled());
_entryListView.setTapToRevealTime(getPreferences().getTapToRevealTime());
_entryListView.setSortCategory(getPreferences().getCurrentSortCategory(), false);
@@ -101,12 +116,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
});
findViewById(R.id.fab_scan_image).setOnClickListener(view -> {
_fabMenu.collapse();
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE);
startScanImageActivity();
});
findViewById(R.id.fab_scan).setOnClickListener(view -> {
_fabMenu.collapse();
@@ -116,6 +126,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_fabScrollHelper = new FabScrollHelper(_fabMenu);
}
@Override
protected void onDestroy() {
_entryListView.setListener(null);
super.onDestroy();
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// collapse the fab menu on touch
@@ -148,7 +164,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
// don't process any activity results if the vault is locked
if (requestCode != CODE_DECRYPT && _db.isLocked()) {
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _db.isLocked()) {
return;
}
@@ -177,6 +193,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
case CODE_SCAN_IMAGE:
onScanImageResult(resultCode, data);
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
@@ -190,6 +208,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
case CODE_PERM_CAMERA:
startScanActivity();
break;
case CODE_PERM_READ_STORAGE:
startScanImageActivity();
break;
}
}
@@ -199,10 +220,14 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = getPreferences().isAccountNameVisible();
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
ViewMode viewMode = getPreferences().getCurrentViewMode();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setSearchAccountName(searchAccountName);
_entryListView.setHighlightEntry(highlightEntry);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
@@ -212,10 +237,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void startEditProfileActivity(int requestCode, DatabaseEntry entry, boolean isNew) {
Intent intent = new Intent(this, EditEntryActivity.class);
if (entry != null) {
intent.putExtra("entry", entry);
}
intent.putExtra("entry", entry != null ? entry : DatabaseEntry.getDefault());
intent.putExtra("isNew", isNew);
intent.putExtra("selectedGroup", _selectedGroup);
intent.putExtra("groups", new ArrayList<>(_db.getGroups()));
startActivityForResult(intent, requestCode);
}
@@ -243,8 +267,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
} else {
// this profile has been serialized/deserialized and is no longer the same instance it once was
// to deal with this, the replaceEntry functions are used
_db.replaceEntry(entry);
_entryListView.replaceEntry(entry);
DatabaseEntry oldEntry = _db.replaceEntry(entry);
_entryListView.replaceEntry(oldEntry, entry);
saveDatabase();
}
}
@@ -302,18 +326,22 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
// if the group no longer exists, switch back to 'All'
TreeSet<String> groups = _db.getGroups();
if (_checkedGroup != null && !groups.contains(_checkedGroup)) {
if (_selectedGroup != null && !groups.contains(_selectedGroup)) {
menu.findItem(R.id.menu_filter_all).setChecked(true);
setGroupFilter(null);
}
for (String group : groups) {
MenuItem item = menu.add(R.id.action_filter_group, Menu.NONE, Menu.NONE, group);
if (group.equals(_checkedGroup)) {
if (group.equals(_selectedGroup)) {
item.setChecked(true);
}
}
if (groups.size() > 0) {
menu.add(R.id.action_filter_group, Menu.NONE, 10, R.string.filter_ungrouped);
}
menu.setGroupCheckable(R.id.action_filter_group, true, true);
}
@@ -324,7 +352,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void setGroupFilter(String group) {
getSupportActionBar().setSubtitle(group);
_checkedGroup = group;
_selectedGroup = group;
_entryListView.setGroupFilter(group, true);
}
@@ -366,6 +394,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startActivityForResult(scannerActivity, CODE_SCAN);
}
private void startScanImageActivity() {
if (!PermissionHelper.request(this, CODE_PERM_READ_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)) {
return;
}
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE);
}
private void doShortcutActions() {
Intent intent = getIntent();
String action = intent.getStringExtra("action");
@@ -382,6 +422,33 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
intent.removeExtra("action");
}
private void handleDeeplink() {
if (_db.isLocked()) {
return;
}
Intent intent = getIntent();
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
getIntent().setData(null);
getIntent().setAction(null);
GoogleAuthInfo info = null;
try {
info = GoogleAuthInfo.parseUri(uri);
} catch (GoogleAuthInfoException e) {
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
if (info != null) {
DatabaseEntry entry = new DatabaseEntry(info);
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
}
}
}
@Override
protected void onResume() {
super.onResume();
@@ -411,61 +478,36 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
loadEntries();
}
handleDeeplink();
updateLockIcon();
doShortcutActions();
}
@Override
public void onBackPressed() {
if (!_searchView.isIconified() || _searchSubmitted) {
_searchSubmitted = false;
_entryListView.setSearchFilter(null);
collapseSearchView();
setTitle("Aegis");
setGroupFilter(_selectedGroup);
return;
}
if (_app.isAutoLockEnabled()) {
_app.lock();
return;
}
super.onBackPressed();
}
private BottomSheetDialog createBottomSheet(final DatabaseEntry entry) {
BottomSheetDialog dialog = new BottomSheetDialog(this);
dialog.setContentView(R.layout.bottom_sheet_edit_entry);
dialog.setCancelable(true);
dialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
dialog.show();
dialog.findViewById(R.id.copy_button).setOnClickListener(view -> {
dialog.dismiss();
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
clipboard.setPrimaryClip(clip);
Toast.makeText(this, getString(R.string.code_copied), Toast.LENGTH_SHORT).show();
});
dialog.findViewById(R.id.delete_button).setOnClickListener(view -> {
dialog.dismiss();
Dialogs.showDeleteEntryDialog(this, (d, which) -> {
deleteEntry(entry);
// update the filter list if the group no longer exists
if (entry.getGroup() != null) {
if (!_db.getGroups().contains(entry.getGroup())) {
updateGroupFilterMenu();
}
}
});
});
dialog.findViewById(R.id.edit_button).setOnClickListener(view -> {
dialog.dismiss();
startEditProfileActivity(CODE_EDIT_ENTRY, entry, false);
});
return dialog;
}
private void deleteEntry(DatabaseEntry entry) {
_db.removeEntry(entry);
DatabaseEntry oldEntry = _db.removeEntry(entry);
saveDatabase();
_entryListView.removeEntry(entry);
_entryListView.removeEntry(oldEntry);
}
@Override
@@ -477,16 +519,53 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
updateGroupFilterMenu();
updateSortCategoryMenu();
}
MenuItem searchViewMenuItem = menu.findItem(R.id.mi_search);
_searchView = (SearchView) searchViewMenuItem.getActionView();
_searchView.setFocusable(false);
_searchView.setQueryHint(getString(R.string.search));
_searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
setTitle(getString(R.string.search));
getSupportActionBar().setSubtitle(s);
_searchSubmitted = true;
collapseSearchView();
return false;
}
@Override
public boolean onQueryTextChange(String s) {
if (!_searchSubmitted) {
_entryListView.setSearchFilter(s);
}
return false;
}
});
_searchView.setOnSearchClickListener(v -> {
if (_searchSubmitted) {
_searchSubmitted = false;
_entryListView.setSearchFilter(null);
}
});
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
case R.id.action_settings: {
Intent intent = new Intent(this, PreferencesActivity.class);
startActivityForResult(intent, CODE_PREFERENCES);
return true;
}
case R.id.action_about: {
Intent intent = new Intent(this, AboutActivity.class);
startActivity(intent);
return true;
}
case R.id.action_lock:
_app.lock();
return true;
@@ -531,6 +610,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void collapseSearchView() {
_searchView.setQuery(null, false);
_searchView.setIconified(true);
}
private boolean unlockDatabase(DatabaseFileCredentials creds) {
try {
if (!_db.isLoaded()) {
@@ -566,6 +650,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("slots", _db.getFileHeader().getSlots());
intent.putExtra("requiresUnlock", false);
intent.putExtra("cancelAction", CancelAction.KILL);
startActivityForResult(intent, CODE_DECRYPT);
}
@@ -587,7 +672,29 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onEntryClick(DatabaseEntry entry) {
createBottomSheet(entry).show();
if (_selectedEntry != null) {
if (_selectedEntry == entry) {
_actionMode.finish();
}
return;
}
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
clipboard.setPrimaryClip(clip);
Toast.makeText(this, getString(R.string.code_copied), Toast.LENGTH_SHORT).show();
}
@Override
public void onLongEntryClick(DatabaseEntry entry) {
if (_selectedEntry != null) {
return;
}
_selectedEntry = entry;
_entryListView.setActionModeState(true, entry);
_actionMode = this.startSupportActionMode(_actionModeCallbacks);
}
@Override
@@ -612,6 +719,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onLocked() {
if (_actionMode != null) {
_actionMode.finish();
}
_entryListView.clearEntries();
_loaded = false;
@@ -621,4 +732,50 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
super.onLocked();
}
private class ActionModeCallbacks implements ActionMode.Callback {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.menu_action_mode, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.action_edit:
startEditProfileActivity(CODE_EDIT_ENTRY, _selectedEntry, false);
mode.finish();
return true;
case R.id.action_delete:
Dialogs.showDeleteEntryDialog(MainActivity.this, (d, which) -> {
deleteEntry(_selectedEntry);
if (_selectedEntry.getGroup() != null) {
if (!_db.getGroups().contains(_selectedEntry.getGroup())) {
updateGroupFilterMenu();
}
}
mode.finish();
});
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
_entryListView.setActionModeState(false, null);
_selectedEntry = null;
_actionMode = null;
}
}
}
@@ -12,8 +12,15 @@ import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ViewMode;
@@ -24,24 +31,29 @@ import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.BiometricSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.importers.AegisImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.takisoft.preferencex.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -51,9 +63,6 @@ import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
public class PreferencesFragment extends PreferenceFragmentCompat {
// activity request codes
private static final int CODE_IMPORT = 0;
@@ -72,15 +81,17 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
// keep a reference to the type of database converter the user selected
private Class<? extends DatabaseImporter> _importerType;
private AegisImporter.State _importerState;
private UUIDMap<DatabaseEntry> _importerEntries;
private SwitchPreference _encryptionPreference;
private SwitchPreference _fingerprintPreference;
private SwitchPreference _biometricsPreference;
private Preference _autoLockPreference;
private Preference _setPasswordPreference;
private Preference _slotsPreference;
private Preference _groupsPreference;
@Override
public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
AegisApplication app = (AegisApplication) getActivity().getApplication();
@@ -108,13 +119,20 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_result.putExtra("needsRecreate", true);
getActivity().recreate();
})
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
}
});
Preference langPreference = findPreference("pref_lang");
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRecreate", true);
getActivity().recreate();
return true;
});
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
Preference viewModePreference = findPreference("pref_view_mode");
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode]));
@@ -132,7 +150,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_result.putExtra("needsRefresh", true);
dialog.dismiss();
})
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
@@ -186,6 +204,18 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
Preference searchAccountNamePreference = findPreference("pref_search_names");
searchAccountNamePreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference tapToRevealPreference = findPreference("pref_tap_to_reveal");
tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
@@ -251,6 +281,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
e.printStackTrace();
}
getActivity().stopService(new Intent(getActivity(), NotificationService.class));
updateEncryptionPreferences();
}
})
@@ -261,18 +292,25 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
_fingerprintPreference = (SwitchPreference) findPreference("pref_fingerprint");
_fingerprintPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
_biometricsPreference = (SwitchPreference) findPreference("pref_biometrics");
_biometricsPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
DatabaseFileCredentials creds = _db.getCredentials();
SlotList slots = creds.getSlots();
if (!slots.has(FingerprintSlot.class)) {
Dialogs.showFingerprintDialog(getActivity(), new RegisterFingerprintListener());
if (!slots.has(BiometricSlot.class)) {
if (BiometricsHelper.isAvailable(getContext())) {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(PreferencesFragment.this, new RegisterBiometricsListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.set_up_biometric))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
initializer.authenticate(info);
}
} else {
// remove the fingerprint slot
FingerprintSlot slot = slots.find(FingerprintSlot.class);
// remove the biometric slot
BiometricSlot slot = slots.find(BiometricSlot.class);
slots.remove(slot);
_db.setCredentials(creds);
@@ -319,10 +357,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return true;
}
});
_autoLockPreference = findPreference("pref_auto_lock");
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
updateEncryptionPreferences();
}
@@ -428,7 +468,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
SuFile file = importer.getAppPath();
try (DatabaseImporter.FileReader reader = DatabaseImporter.FileReader.open(file)) {
try (SuFileInputStream stream = new SuFileInputStream(file)) {
DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream, true);
importDatabase(importer, reader);
}
} catch (PackageManager.NameNotFoundException e) {
@@ -450,6 +491,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisImporter.EncryptedState) state).getSlots());
intent.putExtra("cancelAction", CancelAction.CLOSE);
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
} else {
state.decrypt(getActivity(), new DatabaseImporter.DecryptListener() {
@@ -500,8 +542,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
try (DatabaseImporter.FileReader reader = DatabaseImporter.FileReader.open(getContext(), uri)) {
try (InputStream stream = getContext().getContentResolver().openInputStream(uri)) {
DatabaseImporter importer = DatabaseImporter.create(getContext(), _importerType);
DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream);
importDatabase(importer, reader);
} catch (FileNotFoundException e) {
Toast.makeText(getActivity(), R.string.file_not_found, Toast.LENGTH_SHORT).show();
@@ -522,12 +565,15 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
List<DatabaseEntry> entries = result.getEntries();
List<DatabaseImporterEntryException> errors = result.getErrors();
_importerEntries = result.getEntries();
List<ImportEntry> entries = new ArrayList<>();
for (DatabaseEntry entry : _importerEntries) {
entries.add(new ImportEntry(entry));
}
Intent intent = new Intent(getActivity(), SelectEntriesActivity.class);
intent.putExtra("entries", (ArrayList<DatabaseEntry>) entries);
intent.putExtra("errors", (ArrayList<DatabaseImporterEntryException>) errors);
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
intent.putExtra("errors", (ArrayList<DatabaseImporterEntryException>) result.getErrors());
startActivityForResult(intent, CODE_SELECT_ENTRIES);
}
@@ -600,21 +646,24 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
List<DatabaseEntry> entries = (ArrayList<DatabaseEntry>) data.getSerializableExtra("entries");
for (DatabaseEntry entry : entries) {
List<ImportEntry> selectedEntries = (ArrayList<ImportEntry>) data.getSerializableExtra("entries");
for (ImportEntry selectedEntry : selectedEntries) {
DatabaseEntry savedEntry = _importerEntries.getByUUID(selectedEntry.getUUID());
// temporary: randomize the UUID of duplicate entries and add them anyway
if (_db.getEntryByUUID(entry.getUUID()) != null) {
entry.resetUUID();
if (_db.isEntryDuplicate(savedEntry)) {
savedEntry.resetUUID();
}
_db.addEntry(entry);
_db.addEntry(savedEntry);
}
_importerEntries = null;
if (!saveDatabase()) {
return;
}
String toastMessage = getResources().getString(R.string.imported_entries_count, entries.size());
String toastMessage = getResources().getString(R.string.imported_entries_count, selectedEntries.size());
Toast.makeText(getContext(), toastMessage, Toast.LENGTH_SHORT).show();
_result.putExtra("needsRecreate", true);
@@ -635,22 +684,24 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
boolean encrypted = _db.isEncryptionEnabled();
_encryptionPreference.setChecked(encrypted, true);
_setPasswordPreference.setVisible(encrypted);
_fingerprintPreference.setVisible(encrypted);
_biometricsPreference.setVisible(encrypted);
_slotsPreference.setEnabled(encrypted);
_autoLockPreference.setVisible(encrypted);
if (encrypted) {
SlotList slots = _db.getCredentials().getSlots();
boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1;
boolean multiFinger = slots.findAll(FingerprintSlot.class).size() > 1;
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiFinger;
boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1;
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiBio;
boolean canUseBio = BiometricsHelper.isAvailable(getContext());
_setPasswordPreference.setEnabled(!multiPassword);
_fingerprintPreference.setEnabled(FingerprintHelper.getManager(getContext()) != null && !multiFinger);
_fingerprintPreference.setChecked(slots.has(FingerprintSlot.class), true);
_biometricsPreference.setEnabled(canUseBio && !multiBio);
_biometricsPreference.setChecked(slots.has(BiometricSlot.class), true);
_slotsPreference.setVisible(showSlots);
} else {
_setPasswordPreference.setEnabled(false);
_fingerprintPreference.setEnabled(false);
_fingerprintPreference.setChecked(false, true);
_biometricsPreference.setEnabled(false);
_biometricsPreference.setChecked(false, true);
_slotsPreference.setVisible(false);
}
}
@@ -687,20 +738,17 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
}
private class RegisterFingerprintListener implements Dialogs.SlotListener {
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
DatabaseFileCredentials creds = _db.getCredentials();
SlotList slots = creds.getSlots();
try {
slot.setKey(creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
onSlotInitializationFailed(0, e.toString());
return;
}
slots.add(slot);
creds.getSlots().add(slot);
_db.setCredentials(creds);
saveDatabase();
@@ -708,8 +756,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
@Override
public void onException(Exception e) {
Toast.makeText(getActivity(), getString(R.string.encryption_enable_fingerprint_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(getActivity(), String.format("%s: %s", getString(R.string.encryption_enable_biometrics_error), errString), Toast.LENGTH_LONG).show();
}
}
}
@@ -727,6 +777,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
getActivity().startService(new Intent(getActivity(), NotificationService.class));
updateEncryptionPreferences();
}
@@ -17,7 +17,6 @@ import android.view.MenuItem;
import android.widget.Toast;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
@@ -56,12 +55,11 @@ public class SelectEntriesActivity extends AegisActivity {
entriesView.setNestedScrollingEnabled(false);
Intent intent = getIntent();
List<DatabaseEntry> entries = (ArrayList<DatabaseEntry>) intent.getSerializableExtra("entries");
List<ImportEntry> entries = (ArrayList<ImportEntry>) intent.getSerializableExtra("entries");
List<DatabaseImporterEntryException> errors = (ArrayList<DatabaseImporterEntryException>) intent.getSerializableExtra("errors");
for (DatabaseEntry entry : entries) {
ImportEntry importEntry = new ImportEntry(entry);
_adapter.addEntry(importEntry);
for (ImportEntry entry : entries) {
_adapter.addEntry(entry);
}
if (errors.size() > 0) {
@@ -103,9 +101,9 @@ public class SelectEntriesActivity extends AegisActivity {
}
private void returnSelectedEntries() {
List<DatabaseEntry> entries = _adapter.getSelectedEntries();
List<ImportEntry> entries = _adapter.getCheckedEntries();
Intent intent = new Intent();
intent.putExtra("entries", (ArrayList<DatabaseEntry>) entries);
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
setResult(RESULT_OK, intent);
finish();
}
@@ -7,26 +7,29 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.BiometricSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.ui.views.SlotAdapter;
import javax.crypto.Cipher;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, Dialogs.SlotListener {
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener {
private DatabaseFileCredentials _creds;
private SlotAdapter _adapter;
@@ -42,12 +45,19 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
findViewById(R.id.button_add_fingerprint).setOnClickListener(view -> {
Dialogs.showFingerprintDialog(this ,this);
findViewById(R.id.button_add_biometric).setOnClickListener(view -> {
if (BiometricsHelper.isAvailable(this)) {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(SlotManagerActivity.this, new RegisterBiometricsListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.add_biometric_slot))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
initializer.authenticate(info);
}
});
findViewById(R.id.button_add_password).setOnClickListener(view -> {
Dialogs.showSetPasswordDialog(this, this);
Dialogs.showSetPasswordDialog(this, new PasswordListener());
});
// set up the recycler view
@@ -64,17 +74,17 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
_adapter.addSlot(slot);
}
updateFingerprintButton();
updateBiometricsButton();
}
private void updateFingerprintButton() {
// only show the fingerprint option if we can get an instance of the fingerprint manager
private void updateBiometricsButton() {
// only show the biometrics option if we can get an instance of the biometrics manager
// and if none of the slots in the collection has a matching alias in the keystore
int visibility = View.VISIBLE;
if (FingerprintHelper.getManager(this) != null) {
if (BiometricsHelper.isAvailable(this)) {
try {
KeyStoreHandle keyStore = new KeyStoreHandle();
for (FingerprintSlot slot : _creds.getSlots().findAll(FingerprintSlot.class)) {
for (BiometricSlot slot : _creds.getSlots().findAll(BiometricSlot.class)) {
if (keyStore.containsKey(slot.getUUID().toString())) {
visibility = View.GONE;
break;
@@ -86,7 +96,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
} else {
visibility = View.GONE;
}
findViewById(R.id.button_add_fingerprint).setVisibility(visibility);
findViewById(R.id.button_add_biometric).setVisibility(visibility);
}
private void onSave() {
@@ -162,29 +172,60 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
slots.remove(slot);
_adapter.removeSlot(slot);
_edited = true;
updateFingerprintButton();
updateBiometricsButton();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
try {
slot.setKey(_creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
return;
}
private void addSlot(Slot slot) {
_creds.getSlots().add(slot);
_adapter.addSlot(slot);
_edited = true;
updateFingerprintButton();
updateBiometricsButton();
}
@Override
public void onException(Exception e) {
Toast.makeText(this, getString(R.string.adding_new_slot_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
private void showSlotError(String error) {
Toast.makeText(SlotManagerActivity.this, getString(R.string.adding_new_slot_error) + error, Toast.LENGTH_SHORT).show();
}
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
try {
slot.setKey(_creds.getKey(), cipher);
addSlot(slot);
} catch (SlotException e) {
onSlotInitializationFailed(0, e.toString());
}
}
@Override
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
showSlotError(errString.toString());
}
}
}
private class PasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
try {
slot.setKey(_creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
return;
}
addSlot(slot);
}
@Override
public void onException(Exception e) {
showSlotError(e.toString());
}
}
}
@@ -0,0 +1,21 @@
package com.beemdevelopment.aegis.ui.glide;
import android.content.Context;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import java.nio.ByteBuffer;
@GlideModule
public class AegisGlideModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.prepend(DatabaseEntry.class, ByteBuffer.class, new IconLoader.Factory());
}
}
@@ -0,0 +1,34 @@
package com.beemdevelopment.aegis.ui.glide;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import de.psdev.licensesdialog.licenses.License;
public class GlideLicense extends License {
@Override
public String getName() {
return "Glide License";
}
@Override
public String readSummaryTextFromResources(Context context) {
return getContent(context, R.raw.glide_license);
}
@Override
public String readFullTextFromResources(Context context) {
return getContent(context, R.raw.glide_license);
}
@Override
public String getVersion() {
return null;
}
@Override
public String getUrl() {
return "https://github.com/bumptech/glide/blob/master/LICENSE";
}
}
@@ -0,0 +1,76 @@
package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import java.nio.ByteBuffer;
public class IconLoader implements ModelLoader<DatabaseEntry, ByteBuffer> {
@Override
public LoadData<ByteBuffer> buildLoadData(@NonNull DatabaseEntry model, int width, int height, @NonNull Options options) {
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model));
}
@Override
public boolean handles(@NonNull DatabaseEntry model) {
return true;
}
public static class Fetcher implements DataFetcher<ByteBuffer> {
private DatabaseEntry _model;
private Fetcher(DatabaseEntry model) {
_model = model;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
byte[] bytes = _model.getIcon();
ByteBuffer buf = ByteBuffer.wrap(bytes);
callback.onDataReady(buf);
}
@Override
public void cleanup() {
}
@Override
public void cancel() {
}
@NonNull
@Override
public Class<ByteBuffer> getDataClass() {
return ByteBuffer.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.MEMORY_CACHE;
}
}
public static class Factory implements ModelLoaderFactory<DatabaseEntry, ByteBuffer> {
@NonNull
@Override
public ModelLoader<DatabaseEntry, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
return new IconLoader();
}
@Override
public void teardown() {
}
}
}
@@ -0,0 +1,31 @@
package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.Key;
import java.security.MessageDigest;
import java.util.UUID;
public class UUIDKey implements Key {
private UUID _uuid;
public UUIDKey(UUID uuid) {
_uuid = uuid;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(_uuid.toString().getBytes(CHARSET));
}
@Override
public boolean equals(Object o) {
return _uuid.equals(o);
}
@Override
public int hashCode() {
return _uuid.hashCode();
}
}
@@ -2,23 +2,39 @@ package com.beemdevelopment.aegis.ui.models;
import com.beemdevelopment.aegis.db.DatabaseEntry;
public class ImportEntry {
private DatabaseEntry _entry;
import java.io.Serializable;
import java.util.UUID;
public class ImportEntry implements Serializable {
private UUID _uuid;
private String _name;
private String _issuer;
private transient Listener _listener;
private boolean _isChecked = true;
private Listener _listener;
public ImportEntry(DatabaseEntry entry) {
_entry = entry;
_uuid = entry.getUUID();
_name = entry.getName();
_issuer = entry.getIssuer();
}
public UUID getUUID() {
return _uuid;
}
public String getName() {
return _name;
}
public String getIssuer() {
return _issuer;
}
public void setOnCheckedChangedListener(Listener listener) {
_listener = listener;
}
public DatabaseEntry getDatabaseEntry() {
return _entry;
}
public boolean isChecked() {
return _isChecked;
}
@@ -1,63 +1,58 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Context;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.Bundle;
import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.BiometricSlot;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.github.paolorotolo.appintro.ISlideSelectionListener;
import com.google.android.material.snackbar.Snackbar;
import com.mattprecious.swirl.SwirlView;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.fragment.app.Fragment;
public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener {
public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, ISlideSelectionListener {
private int _cryptType;
private EditText _textPassword;
private EditText _textPasswordConfirm;
private CheckBox _checkPasswordVisibility;
private int _bgColor;
private LinearLayout _boxFingerprint;
private SwirlView _imgFingerprint;
private TextView _textFingerprint;
private FingerprintUiHelper _fingerHelper;
private KeyStoreHandle _storeHandle;
private FingerprintSlot _fingerSlot;
private FingerprintManager.CryptoObject _fingerCryptoObj;
private boolean _fingerAuthenticated;
private BiometricSlot _bioSlot;
private Cipher _bioCipher;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_authenticated_slide, container, false);
_textPassword = view.findViewById(R.id.text_password);
_textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
_boxFingerprint = view.findViewById(R.id.box_fingerprint);
_checkPasswordVisibility = view.findViewById(R.id.check_toggle_visibility);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewGroup insertPoint = view.findViewById(R.id.img_fingerprint_insert);
_imgFingerprint = new SwirlView(getContext());
insertPoint.addView(_imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
_checkPasswordVisibility.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
_textPassword.setTransformationMethod(null);
_textPassword.clearFocus();
_textPasswordConfirm.setEnabled(false);
} else {
_textPassword.setTransformationMethod(new PasswordTransformationMethod());
_textPasswordConfirm.setEnabled(true);
}
});
_textFingerprint = view.findViewById(R.id.text_fingerprint);
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
return view;
}
@@ -66,114 +61,96 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
return _cryptType;
}
public BiometricSlot getBiometricSlot() {
return _bioSlot;
}
public Cipher getBiometriCipher() {
return _bioCipher;
}
public char[] getPassword() {
return EditTextHelper.getEditTextChars(_textPassword);
}
public Cipher getFingerCipher() {
return _fingerCryptoObj.getCipher();
}
public FingerprintSlot getFingerSlot() {
return _fingerSlot;
}
public void setBgColor(int color) {
_bgColor = color;
}
public void showBiometricPrompt() {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(this, new BiometricsListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.set_up_biometric))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
initializer.authenticate(info);
}
@Override
public void onSlideSelected() {
Intent intent = getActivity().getIntent();
_cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
switch(_cryptType) {
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
break;
case CustomAuthenticationSlide.CRYPT_TYPE_FINGER:
_boxFingerprint.setVisibility(View.VISIBLE);
SecretKey key;
try {
if (_storeHandle == null) {
_storeHandle = new KeyStoreHandle();
_fingerSlot = new FingerprintSlot();
}
key = _storeHandle.generateKey(_fingerSlot.getUUID().toString());
} catch (KeyStoreHandleException e) {
throw new RuntimeException(e);
}
if (_fingerHelper == null) {
FingerprintManager fingerManager = (FingerprintManager) getContext().getSystemService(Context.FINGERPRINT_SERVICE);
_fingerHelper = new FingerprintUiHelper(fingerManager, _imgFingerprint, _textFingerprint, this);
}
try {
Cipher cipher = Slot.createEncryptCipher(key);
_fingerCryptoObj = new FingerprintManager.CryptoObject(cipher);
} catch (Exception e) {
throw new RuntimeException(e);
}
_fingerHelper.startListening(_fingerCryptoObj);
break;
default:
throw new RuntimeException();
if (_cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
showBiometricPrompt();
}
}
@Override
public void onSlideDeselected() {
if (_fingerHelper != null) {
_fingerAuthenticated = false;
_boxFingerprint.setVisibility(View.INVISIBLE);
_fingerHelper.stopListening();
}
}
@Override
public boolean isPolicyRespected() {
switch(_cryptType) {
switch (_cryptType) {
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
return true;
case CustomAuthenticationSlide.CRYPT_TYPE_FINGER:
if (!_fingerAuthenticated) {
case CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC:
if (_bioSlot == null) {
return false;
}
// intentional fallthrough
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm);
if (EditTextHelper.getEditTextChars(_textPassword).length > 0) {
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) || _checkPasswordVisibility.isChecked();
}
return false;
default:
throw new RuntimeException();
return false;
}
}
@Override
public void onUserIllegallyRequestedNextPage() {
String message;
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) && !_checkPasswordVisibility.isChecked()) {
message = getString(R.string.password_equality_error);
} else if (!_fingerAuthenticated) {
message = getString(R.string.register_fingerprint);
} else {
return;
}
View view = getView();
if (view != null) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show();
View view = getView();
if (view != null) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show();
}
} else if (_bioSlot == null) {
showBiometricPrompt();
}
}
@Override
public void onAuthenticated() {
_fingerAuthenticated = true;
}
private class BiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onError() {
@Override
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
_bioSlot = slot;
_bioCipher = cipher;
}
@Override
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(CustomAuthenticatedSlide.this.getContext(), errString, Toast.LENGTH_LONG).show();
}
}
}
}
@@ -1,7 +1,6 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -10,18 +9,18 @@ import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener {
public static final int CRYPT_TYPE_INVALID = 0;
public static final int CRYPT_TYPE_NONE = 1;
public static final int CRYPT_TYPE_PASS = 2;
public static final int CRYPT_TYPE_FINGER = 3;
public static final int CRYPT_TYPE_BIOMETRIC = 3;
private RadioGroup _buttonGroup;
private int _bgColor;
@@ -34,10 +33,9 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
onCheckedChanged(_buttonGroup, _buttonGroup.getCheckedRadioButtonId());
// only enable the fingerprint option if the api version is new enough, permission is granted and a scanner is found
FingerprintManager manager = FingerprintHelper.getManager(getContext());
if (manager != null) {
RadioButton button = view.findViewById(R.id.rb_fingerprint);
TextView text = view.findViewById(R.id.text_rb_fingerprint);
if (BiometricsHelper.isAvailable(getContext())) {
RadioButton button = view.findViewById(R.id.rb_biometrics);
TextView text = view.findViewById(R.id.text_rb_biometrics);
button.setEnabled(true);
text.setEnabled(true);
}
@@ -75,8 +73,8 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
case R.id.rb_password:
id = CRYPT_TYPE_PASS;
break;
case R.id.rb_fingerprint:
id = CRYPT_TYPE_FINGER;
case R.id.rb_biometrics:
id = CRYPT_TYPE_BIOMETRIC;
break;
default:
throw new RuntimeException();
@@ -9,7 +9,7 @@ import com.beemdevelopment.aegis.ui.Dialogs;
import androidx.annotation.CallSuper;
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, Void, Result> {
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, String, Result> {
private ProgressDialog _dialog;
public ProgressDialogTask(Context context, String message) {
@@ -33,6 +33,13 @@ public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Param
}
}
@Override
protected void onProgressUpdate(String... values) {
if (values.length == 1) {
_dialog.setMessage(values[0]);
}
}
protected void setPriority() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
}
@@ -1,12 +1,11 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.app.ProgressDialog;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.BiometricSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
@@ -38,8 +37,8 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
if (slot instanceof PasswordSlot) {
char[] password = (char[]) params.getObj();
return decryptPasswordSlot((PasswordSlot) slot, password);
} else if (slot instanceof FingerprintSlot) {
return decryptFingerprintSlot((FingerprintSlot) slot, (Cipher) params.getObj());
} else if (slot instanceof BiometricSlot) {
return decryptBiometricSlot((BiometricSlot) slot, (Cipher) params.getObj());
}
} catch (SlotException e) {
throw new RuntimeException(e);
@@ -51,7 +50,7 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
return null;
}
private Result decryptFingerprintSlot(FingerprintSlot slot, Cipher cipher)
private Result decryptBiometricSlot(BiometricSlot slot, Cipher cipher)
throws SlotException, SlotIntegrityException {
MasterKey key = slot.getKey(cipher);
return new Result(key, slot);
@@ -72,8 +71,7 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
throw e;
}
ProgressDialog dialog = getDialog();
dialog.setMessage(dialog.getContext().getString(R.string.unlocking_vault_repair));
publishProgress(getDialog().getContext().getString(R.string.unlocking_vault_repair));
// try to decrypt the password slot with the old key
SecretKey oldKey = slot.deriveKey(oldPasswordBytes);
@@ -1,9 +1,13 @@
package com.beemdevelopment.aegis.ui.views;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.db.DatabaseEntry;
@@ -17,30 +21,41 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import androidx.recyclerview.widget.RecyclerView;
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
private EntryListView _view;
private List<DatabaseEntry> _entries;
private List<DatabaseEntry> _shownEntries;
private static Listener _listener;
private DatabaseEntry _selectedEntry;
private DatabaseEntry _focusedEntry;
private boolean _showAccountName;
private boolean _searchAccountName;
private boolean _highlightEntry;
private boolean _tapToReveal;
private int _tapToRevealTime;
private String _groupFilter;
private SortCategory _sortCategory;
private ViewMode _viewMode;
private String _searchFilter;
private boolean _isPeriodUniform = true;
private Handler _dimHandler;
// keeps track of the viewholders that are currently bound
private List<EntryHolder> _holders;
public EntryAdapter(Listener listener) {
public EntryAdapter(EntryListView view) {
_entries = new ArrayList<>();
_shownEntries = new ArrayList<>();
_holders = new ArrayList<>();
_listener = listener;
_dimHandler = new Handler();
_view = view;
}
public void destroy() {
for (EntryHolder holder : _holders) {
holder.destroy();
}
_view = null;
}
public void setShowAccountName(boolean showAccountName) {
@@ -55,6 +70,18 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_tapToRevealTime = number;
}
public void setSearchAccountName(boolean searchAccountName) {
_searchAccountName = searchAccountName;
}
public void setHighlightEntry(boolean highlightEntry) {
_highlightEntry = highlightEntry;
}
public DatabaseEntry getEntryAt(int position) {
return _shownEntries.get(position);
}
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
if (isEntryFiltered(entry)) {
@@ -97,7 +124,6 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
public void removeEntry(DatabaseEntry entry) {
entry = getEntryByUUID(entry.getUUID());
_entries.remove(entry);
if (_shownEntries.contains(entry)) {
@@ -113,10 +139,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_entries.clear();
_shownEntries.clear();
notifyDataSetChanged();
checkPeriodUniformity();
}
public void replaceEntry(DatabaseEntry newEntry) {
DatabaseEntry oldEntry = getEntryByUUID(newEntry.getUUID());
public void replaceEntry(DatabaseEntry oldEntry, DatabaseEntry newEntry) {
_entries.set(_entries.indexOf(oldEntry), newEntry);
if (_shownEntries.contains(oldEntry)) {
@@ -141,19 +167,24 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
private boolean isEntryFiltered(DatabaseEntry entry) {
String group = entry.getGroup();
if (_groupFilter == null) {
return false;
}
return group == null || !group.equals(_groupFilter);
}
String issuer = entry.getIssuer().toLowerCase();
String name = entry.getName().toLowerCase();
private DatabaseEntry getEntryByUUID(UUID uuid) {
for (DatabaseEntry entry : _entries) {
if (entry.getUUID().equals(uuid)) {
return entry;
if (_groupFilter != null) {
if (group == null && _groupFilter.equals(_view.getContext().getString(R.string.filter_ungrouped))) {
return false;
}
if (group == null || !group.equals(_groupFilter)) {
return true;
}
}
throw new AssertionError("no entry found with the same id");
if (_searchFilter == null) {
return false;
}
return !issuer.contains(_searchFilter) && !(_searchAccountName && name.contains(_searchFilter));
}
public void refresh(boolean hard) {
@@ -181,12 +212,18 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
if (_sortCategory == category) {
return;
}
_sortCategory = category;
if (apply) {
updateShownEntries();
}
}
public void setSearchFilter(String search) {
_searchFilter = search != null ? search.toLowerCase() : null;
updateShownEntries();
}
private void updateShownEntries() {
// clear the list of shown entries first
_shownEntries.clear();
@@ -223,7 +260,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
return;
}
_listener.onEntryDrop(_shownEntries.get(position));
_view.onEntryDrop(_shownEntries.get(position));
}
@Override
@@ -234,7 +271,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
// notify the database first
_listener.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
_view.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
// update our side of things
Collections.swap(_entries, firstPosition, secondPosition);
@@ -251,7 +288,9 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
public EntryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(_viewMode.getLayoutId(), parent, false);
return new EntryHolder(view);
EntryHolder holder = new EntryHolder(view);
_view.setPreloadView(holder.getIconView());
return holder;
}
@Override
@@ -263,18 +302,32 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public void onBindViewHolder(final EntryHolder holder, int position) {
DatabaseEntry entry = _shownEntries.get(position);
holder.setFocused(entry == _selectedEntry);
boolean hidden = _tapToReveal && entry != _focusedEntry;
boolean dimmed = _highlightEntry && _focusedEntry != null && _focusedEntry != entry;
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
holder.setData(entry, _showAccountName, showProgress, _tapToReveal);
holder.setTapToRevealTime(_tapToRevealTime);
holder.setData(entry, _showAccountName, showProgress, hidden, dimmed);
holder.loadIcon(_view);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
if (_tapToReveal && holder.isCodeHidden()) {
holder.revealCode();
} else {
_listener.onEntryClick(_shownEntries.get(position));
boolean handled = false;
if (_selectedEntry == null) {
if (_highlightEntry || _tapToReveal) {
if (_focusedEntry == entry) {
resetFocus();
handled = true;
} else {
focusEntry(entry);
}
}
}
if (!handled) {
_view.onEntryClick(entry);
}
}
});
@@ -282,7 +335,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public boolean onLongClick(View v) {
int position = holder.getAdapterPosition();
return _listener.onLongEntryClick(_shownEntries.get(position));
if (_selectedEntry == null) {
setSelectedEntry(_shownEntries.get(position));
holder.setFocused(true);
}
return _view.onLongEntryClick(_shownEntries.get(position));
}
});
holder.setOnRefreshClickListener(new View.OnClickListener() {
@@ -297,7 +355,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
// notify the listener that the counter has been incremented
// this gives it a chance to save the database
_listener.onEntryChange(entry);
_view.onEntryChange(entry);
// finally, refresh the code in the UI
holder.refreshCode();
@@ -322,7 +380,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
holder.setShowProgress(!_isPeriodUniform);
}
_listener.onPeriodUniformityChanged(_isPeriodUniform);
_view.onPeriodUniformityChanged(_isPeriodUniform);
}
public int getUniformPeriod() {
@@ -348,6 +406,58 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
return period;
}
private void focusEntry(DatabaseEntry entry) {
_focusedEntry = entry;
_dimHandler.removeCallbacksAndMessages(null);
for (EntryHolder holder : _holders) {
if (holder.getEntry() != _focusedEntry) {
if (_highlightEntry) {
holder.dim();
}
if (_tapToReveal) {
holder.hideCode();
}
} else {
if (_highlightEntry) {
holder.highlight();
}
if (_tapToReveal) {
holder.revealCode();
}
}
}
_dimHandler.postDelayed(this::resetFocus, _tapToRevealTime * 1000);
}
private void resetFocus() {
for (EntryHolder holder : _holders) {
if (_highlightEntry) {
holder.highlight();
}
if (_tapToReveal) {
holder.hideCode();
}
}
_focusedEntry = null;
}
public void setSelectedEntry(DatabaseEntry entry) {
if (entry == null) {
notifyItemChanged(_shownEntries.indexOf(_selectedEntry));
} else if (_highlightEntry) {
resetFocus();
}
_selectedEntry = entry;
}
public boolean isDragAndDropAllowed() {
return _sortCategory == SortCategory.CUSTOM && _groupFilter == null && _searchFilter == null;
}
public boolean isPeriodUniform() {
return getUniformPeriod() != -1;
}
@@ -1,61 +1,64 @@
package com.beemdevelopment.aegis.ui.views;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PorterDuff;
import android.os.Handler;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
public class EntryHolder extends RecyclerView.ViewHolder {
private static final float DEFAULT_ALPHA = 1.0f;
private static final float DIMMED_ALPHA = 0.2f;
private TextView _profileName;
private TextView _profileCode;
private TextView _profileIssuer;
private ImageView _profileDrawable;
private DatabaseEntry _entry;
private ImageView _buttonRefresh;
private View _entryDivider;
private boolean _hidden;
private int _tapToRevealTime;
private PeriodProgressBar _progressBar;
private View _view;
private UiRefresher _refresher;
private Handler _hiddenHandler;
public EntryHolder(final View view) {
super(view);
_view = view.findViewById(R.id.rlCardEntry);
_profileName = view.findViewById(R.id.profile_account_name);
_profileCode = view.findViewById(R.id.profile_code);
_profileIssuer = view.findViewById(R.id.profile_issuer);
_profileDrawable = view.findViewById(R.id.ivTextDrawable);
_buttonRefresh = view.findViewById(R.id.buttonRefresh);
_entryDivider = view.findViewById(R.id.entryDivider);
_progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
_view.setBackground(_view.getContext().getResources().getDrawable(R.color.card_background));
_refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
public void onRefresh() {
if (!isCodeHidden()) {
if (_hidden) {
refreshCode();
}
@@ -67,11 +70,9 @@ public class EntryHolder extends RecyclerView.ViewHolder {
return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation();
}
});
_hiddenHandler = new Handler();
}
public void setData(DatabaseEntry entry, boolean showAccountName, boolean showProgress, boolean hidden) {
public void setData(DatabaseEntry entry, boolean showAccountName, boolean showProgress, boolean hidden, boolean dimmed) {
_entry = entry;
_hidden = hidden;
@@ -87,27 +88,35 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_profileName.setText(" - " + entry.getName());
}
if (_entry.hasIcon()) {
byte[] imageBytes = entry.getIcon();
Bitmap image = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
_profileDrawable.setImageBitmap(image);
} else {
TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName(), _profileDrawable);
_profileDrawable.setImageDrawable(drawable);
}
// cancel any scheduled hideCode calls
_hiddenHandler.removeCallbacksAndMessages(null);
if (_hidden) {
hideCode();
} else {
refreshCode();
}
itemView.setAlpha(dimmed ? DIMMED_ALPHA : DEFAULT_ALPHA);
}
public void setTapToRevealTime(int number) {
_tapToRevealTime = number;
public DatabaseEntry getEntry() {
return _entry;
}
public void loadIcon(Fragment fragment) {
if (_entry.hasIcon()) {
Glide.with(fragment)
.asDrawable()
.load(_entry)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_profileDrawable);
} else {
TextDrawable drawable = TextDrawableHelper.generate(_entry.getIssuer(), _entry.getName(), _profileDrawable);
_profileDrawable.setImageDrawable(drawable);
}
}
public ImageView getIconView() {
return _profileDrawable;
}
public void setOnRefreshClickListener(View.OnClickListener listener) {
@@ -122,17 +131,25 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_progressBar.setVisibility(showProgress ? View.VISIBLE : View.GONE);
if (showProgress) {
_progressBar.setPeriod(((TotpInfo) _entry.getInfo()).getPeriod());
if (_entryDivider != null) {
_entryDivider.setVisibility(View.GONE);
}
startRefreshLoop();
} else {
stopRefreshLoop();
}
}
public void setFocused(boolean focused) {
if (focused) {
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, _view.getContext().getTheme()));
} else {
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackground, _view.getContext().getTheme()));
}
_view.setSelected(focused);
}
public void destroy() {
_refresher.destroy();
}
public void startRefreshLoop() {
_refresher.start();
}
@@ -142,7 +159,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
public void refreshCode() {
if (!isCodeHidden()) {
if (!_hidden) {
updateCode();
}
}
@@ -150,31 +167,40 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private void updateCode() {
OtpInfo info = _entry.getInfo();
String text;
if (info instanceof SteamInfo) {
text = info.getOtp();
} else {
String otp = info.getOtp();
text = otp.substring(0, (otp.length() / 2)
+ (otp.length() % 2)) + " "
+ otp.substring(otp.length() / 2);
String otp = info.getOtp();
if (!(info instanceof SteamInfo)) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < otp.length(); i++) {
if (i != 0 && i % 3 == 0) {
sb.append(" ");
}
sb.append(otp.charAt(i));
}
otp = sb.toString();
}
_profileCode.setText(text);
_profileCode.setText(otp);
}
public void revealCode() {
updateCode();
_hiddenHandler.postDelayed(this::hideCode, _tapToRevealTime * 1000);
_hidden = false;
}
private void hideCode() {
public void hideCode() {
_profileCode.setText(R.string.tap_to_reveal);
_hidden = true;
}
public boolean isCodeHidden() {
return _hidden;
public void dim() {
animateAlphaTo(DIMMED_ALPHA);
}
public void highlight() {
animateAlphaTo(DEFAULT_ALPHA);
}
private void animateAlphaTo(float alpha) {
itemView.animate().alpha(alpha).setDuration(200).start();
}
}
@@ -1,12 +1,23 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
@@ -15,22 +26,28 @@ import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import java.util.Collections;
import java.util.List;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class EntryListView extends Fragment implements EntryAdapter.Listener {
private EntryAdapter _adapter;
private Listener _listener;
private SimpleItemTouchHelperCallback _touchCallback;
private RecyclerView _recyclerView;
private RecyclerView.ItemDecoration _dividerDecoration;
private ViewPreloadSizeProvider<DatabaseEntry> _preloadSizeProvider;
private PeriodProgressBar _progressBar;
private boolean _showProgress;
private ViewMode _viewMode;
private LinearLayout _emptyStateView;
private UiRefresher _refresher;
@@ -41,6 +58,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_showProgress = false;
}
@Override
public void onDestroy() {
_adapter.destroy();
super.onDestroy();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
@@ -56,8 +79,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
});
LinearLayoutManager mLayoutManager = new LinearLayoutManager(view.getContext());
_recyclerView.setLayoutManager(mLayoutManager);
// set up icon preloading
_preloadSizeProvider = new ViewPreloadSizeProvider<>();
IconPreloadProvider modelProvider = new IconPreloadProvider();
RecyclerViewPreloader<DatabaseEntry> preloader = new RecyclerViewPreloader<>(Glide.with(this), modelProvider, _preloadSizeProvider, 10);
_recyclerView.addOnScrollListener(preloader);
LinearLayoutManager layoutManager = new LinearLayoutManager(view.getContext());
_recyclerView.setLayoutManager(layoutManager);
_touchCallback = new SimpleItemTouchHelperCallback(_adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(_touchCallback);
touchHelper.attachToRecyclerView(_recyclerView);
@@ -79,9 +108,21 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
});
_emptyStateView = view.findViewById(R.id.vEmptyList);
return view;
}
public void setPreloadView(View view) {
_preloadSizeProvider.setView(view);
}
@Override
public void onDestroyView() {
_refresher.destroy();
super.onDestroyView();
}
public void setGroupFilter(String group, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(group == null);
_adapter.setGroupFilter(group, apply);
@@ -91,6 +132,12 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
}
public void setActionModeState(boolean enabled, DatabaseEntry entry) {
_touchCallback.setSelectedEntry(entry);
_touchCallback.setIsLongPressDragEnabled(enabled && _adapter.isDragAndDropAllowed());
_adapter.setSelectedEntry(entry);
}
public void setSortCategory(SortCategory sortCategory, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(sortCategory == SortCategory.CUSTOM);
_adapter.setSortCategory(sortCategory, apply);
@@ -100,8 +147,15 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
}
public void setSearchFilter(String search) {
_touchCallback.setIsLongPressDragEnabled(search == null);
_adapter.setSearchFilter(search);
}
public void setViewMode(ViewMode mode) {
_adapter.setViewMode(mode);
_viewMode = mode;
updateDividerDecoration();
_adapter.setViewMode(_viewMode);
}
public void refresh(boolean hard) {
@@ -120,9 +174,9 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_listener.onEntryClick(entry);
}
@Override
public boolean onLongEntryClick(DatabaseEntry entry) {
return false;
_listener.onLongEntryClick(entry);
return true;
}
@Override
@@ -142,7 +196,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
@Override
public void onPeriodUniformityChanged(boolean isUniform) {
_showProgress = isUniform;
setShowProgress(isUniform);
if (_showProgress) {
_progressBar.setVisibility(View.VISIBLE);
_progressBar.setPeriod(_adapter.getUniformPeriod());
@@ -157,6 +211,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_adapter.setShowAccountName(showAccountName);
}
public void setSearchAccountName(boolean searchAccountName) {
_adapter.setSearchAccountName(searchAccountName);
}
public void setHighlightEntry(boolean highlightEntry) {
_adapter.setHighlightEntry(highlightEntry);
}
public void setTapToReveal(boolean tapToReveal) {
_adapter.setTapToReveal(tapToReveal);
}
@@ -167,22 +229,25 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
public void addEntry(DatabaseEntry entry) {
_adapter.addEntry(entry);
updateEmptyState();
}
public void addEntries(List<DatabaseEntry> entries) {
_adapter.addEntries(entries);
updateEmptyState();
}
public void removeEntry(DatabaseEntry entry) {
_adapter.removeEntry(entry);
updateEmptyState();
}
public void clearEntries() {
_adapter.clearEntries();
}
public void replaceEntry(DatabaseEntry entry) {
_adapter.replaceEntry(entry);
public void replaceEntry(DatabaseEntry oldEntry, DatabaseEntry newEntry) {
_adapter.replaceEntry(oldEntry, newEntry);
}
public void runEntriesAnimation() {
@@ -194,11 +259,84 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_recyclerView.scheduleLayoutAnimation();
}
private void setShowProgress(boolean showProgress) {
_showProgress = showProgress;
updateDividerDecoration();
}
private void updateDividerDecoration() {
if (_dividerDecoration != null) {
_recyclerView.removeItemDecoration(_dividerDecoration);
}
float height = _viewMode.getDividerHeight();
if (_showProgress && height == 0) {
DividerItemDecoration divider = new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL);
divider.setDrawable(ContextCompat.getDrawable(getContext(), R.drawable.entry_divider));
_dividerDecoration = divider;
} else {
_dividerDecoration = new VerticalSpaceItemDecoration(height);
}
_recyclerView.addItemDecoration(_dividerDecoration);
}
private void updateEmptyState() {
if (_adapter.getItemCount() > 0) {
_recyclerView.setVisibility(View.VISIBLE);
_emptyStateView.setVisibility(View.GONE);
} else {
_recyclerView.setVisibility(View.GONE);
_emptyStateView.setVisibility(View.VISIBLE);
}
}
public interface Listener {
void onEntryClick(DatabaseEntry entry);
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
void onEntryDrop(DatabaseEntry entry);
void onEntryChange(DatabaseEntry entry);
void onLongEntryClick(DatabaseEntry entry);
void onScroll(int dx, int dy);
}
private class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {
private int _height;
private VerticalSpaceItemDecoration(float dp) {
// convert dp to pixels
_height = (int) (dp * (getContext().getResources().getDisplayMetrics().densityDpi / 160f));
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
if (parent.getChildAdapterPosition(view) == 0) {
// the first item should also have a top margin
outRect.top = _height;
}
outRect.bottom = _height;
}
}
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<DatabaseEntry> {
@NonNull
@Override
public List<DatabaseEntry> getPreloadItems(int position) {
DatabaseEntry entry = _adapter.getEntryAt(position);
if (!entry.hasIcon()) {
return Collections.emptyList();
}
return Collections.singletonList(entry);
}
@Nullable
@Override
public RequestBuilder getPreloadRequestBuilder(@NonNull DatabaseEntry entry) {
return Glide.with(EntryListView.this)
.asDrawable()
.load(entry)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
}
}
}
@@ -56,17 +56,7 @@ public class ImportEntriesAdapter extends RecyclerView.Adapter<ImportEntryHolder
return _entries.size();
}
public List<DatabaseEntry> getSelectedEntries() {
List<DatabaseEntry> entries = new ArrayList<>();
for (ImportEntry entry : getCheckedEntries()) {
entries.add(entry.getDatabaseEntry());
}
return entries;
}
private List<ImportEntry> getCheckedEntries() {
public List<ImportEntry> getCheckedEntries() {
List<ImportEntry> entries = new ArrayList<>();
for (ImportEntry entry : _entries) {
@@ -31,9 +31,8 @@ public class ImportEntryHolder extends RecyclerView.ViewHolder implements Import
_entry = entry;
Context context = itemView.getContext();
DatabaseEntry dbEntry = entry.getDatabaseEntry();
_issuer.setText(!dbEntry.getIssuer().isEmpty() ? dbEntry.getIssuer() : context.getString(R.string.unknown_issuer));
_accountName.setText(!dbEntry.getName().isEmpty() ? dbEntry.getName() : context.getString(R.string.unknown_account_name));
_issuer.setText(!entry.getIssuer().isEmpty() ? entry.getIssuer() : context.getString(R.string.unknown_issuer));
_accountName.setText(!entry.getName().isEmpty() ? entry.getName() : context.getString(R.string.unknown_account_name));
_checkbox.setChecked(entry.isChecked());
}
@@ -5,16 +5,16 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.BiometricSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.RawSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
public class SlotHolder extends RecyclerView.ViewHolder {
private TextView _slotUsed;
@@ -36,10 +36,10 @@ public class SlotHolder extends RecyclerView.ViewHolder {
if (slot instanceof PasswordSlot) {
_slotName.setText(R.string.password);
_slotImg.setImageResource(R.drawable.ic_create_black_24dp);
} else if (slot instanceof FingerprintSlot) {
_slotName.setText(R.string.authentication_method_fingerprint);
} else if (slot instanceof BiometricSlot) {
_slotName.setText(R.string.authentication_method_biometrics);
_slotImg.setImageResource(R.drawable.ic_fingerprint_black_24dp);
if (FingerprintHelper.isSupported()) {
if (BiometricsHelper.isAvailable(itemView.getContext())) {
try {
KeyStoreHandle keyStore = new KeyStoreHandle();
if (keyStore.containsKey(slot.getUUID().toString())) {
@@ -0,0 +1,32 @@
package com.beemdevelopment.aegis.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Cloner {
private Cloner() {
}
/**
* Returns an exact clone of the given Serializable object.
*/
@SuppressWarnings("unchecked cast")
public static <T extends Serializable> T clone(T obj) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (T) ois.readObject();
} catch (ClassNotFoundException | IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,79 @@
package com.beemdevelopment.aegis.util;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class PreferenceParser {
private PreferenceParser() {
}
public static List<XmlEntry> parse(XmlPullParser parser) throws IOException, XmlPullParserException {
List<XmlEntry> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (!parser.getName().equals("string")) {
skip(parser);
continue;
}
entries.add(parseEntry(parser));
}
return entries;
}
private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, "string");
String name = parser.getAttributeValue(null, "name");
String value = parseText(parser);
parser.require(XmlPullParser.END_TAG, null, "string");
XmlEntry entry = new XmlEntry();
entry.Name = name;
entry.Value = value;
return entry;
}
private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {
String text = "";
if (parser.next() == XmlPullParser.TEXT) {
text = parser.getText();
parser.nextTag();
}
return text;
}
private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException {
// source: https://developer.android.com/training/basics/network-ops/xml.html
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
public static class XmlEntry {
public String Name;
public String Value;
}
}
@@ -0,0 +1,162 @@
package com.beemdevelopment.aegis.util;
import androidx.annotation.NonNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.UUID;
/**
* A map data structure abstraction for storing values with a UUID as the key. Keys
* must be specified by the value itself, instead of separately. It uses a
* LinkedHashMap internally (a hash map with a separate linked list that maintains
* the order).
* @param <T> The type of values in this map
*/
public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializable {
private LinkedHashMap<UUID, T> _map = new LinkedHashMap<>();
/**
* Adds a value to the internal map.
* @throws AssertionError if a map value with the UUID of the given value already exists.
*/
public void add(T value) {
UUID uuid = value.getUUID();
if (_map.containsKey(uuid)) {
throw new AssertionError(String.format("Existing value found with UUID: %s", uuid));
}
_map.put(uuid, value);
}
/**
* Removes a value from the internal map.
* @throws AssertionError if no map value exists with the UUID of the given value.
* @return The old value that is now no longer present in the internal map.
*/
public T remove(T value) {
T oldValue = getByUUID(value.getUUID());
_map.remove(oldValue.getUUID());
return oldValue;
}
/**
* Replaces an old value (with the same UUID as the new given value) in the
* internal map with the new given value.
* @throws AssertionError if no map value exists with the UUID of the given value.
* @return The old value that is now no longer present in the internal map.
*/
public T replace(T newValue) {
T oldValue = getByUUID(newValue.getUUID());
_map.put(oldValue.getUUID(), newValue);
return oldValue;
}
/**
* Swaps the position of value1 and value2 in the internal map. This operation is
* quite expensive because it has to reallocate the entire underlying LinkedHashMap.
* @throws AssertionError if no map value exists with the UUID of the given entries.
*/
public void swap(T value1, T value2) {
boolean found1 = false;
boolean found2 = false;
List<T> values = new ArrayList<>();
for (T value : _map.values()) {
if (value.getUUID().equals(value1.getUUID())) {
values.add(value2);
found1 = true;
} else if (value.getUUID().equals(value2.getUUID())) {
values.add(value1);
found2 = true;
} else {
values.add(value);
}
}
if (!found1) {
throw new AssertionError(String.format("No value found for value1 with UUID: %s", value1.getUUID()));
}
if (!found2) {
throw new AssertionError(String.format("No value found for value2 with UUID: %s", value2.getUUID()));
}
_map.clear();
for (T value : values) {
_map.put(value.getUUID(), value);
}
}
/**
* Reports whether the internal map contains a value with the UUID of the given value.
*/
public boolean has(T value) {
return _map.containsKey(value.getUUID());
}
/**
* Returns a read-only view of the values in the internal map.
*/
public Collection<T> getValues() {
return Collections.unmodifiableCollection(_map.values());
}
/**
* Retrieves an entry from the internal map that has the given UUID.
* @throws AssertionError if no map value exists with the given UUID.
*/
public T getByUUID(UUID uuid) {
T value = _map.get(uuid);
if (value == null) {
throw new AssertionError(String.format("No value found with UUID: %s", uuid));
}
return value;
}
@NonNull
@Override
public Iterator<T> iterator() {
return _map.values().iterator();
}
public static abstract class Value implements Serializable {
private UUID _uuid;
protected Value(UUID uuid) {
_uuid = uuid;
}
protected Value() {
this(UUID.randomUUID());
}
public final UUID getUUID() {
return _uuid;
}
/**
* Resets the UUID of this value by generating a new random one.
* The caller must ensure that this Value is not in a UUIDMap yet. Otherwise, bad things will happen.
*/
public final void resetUUID() {
_uuid = UUID.randomUUID();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Value)) {
return false;
}
return getUUID().equals(((Value) o).getUUID());
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

@@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="137.93103"
android:viewportHeight="137.93103">
<group android:translateX="-11.034483"
android:translateY="-11.034483">
<path
android:pathData="M55.78,0h48.44C120.28,0 128.31,0 137,2.73A34,34 0,0 1,157.27 23C160,31.69 160,39.72 160,55.78v48.44c0,16.06 0,24.09 -2.73,32.74A34,34 0,0 1,137 157.27C128.31,160 120.28,160 104.22,160H55.78c-16.06,0 -24.09,0 -32.74,-2.73A34,34 0,0 1,2.73 137C0,128.31 0,120.28 0,104.22V55.78C0,39.72 0,31.69 2.73,23A34,34 0,0 1,23 2.73C31.69,0 39.72,0 55.78,0h0">
<aapt:attr name="android:fillColor">
<gradient
android:startY="28.48"
android:startX="51.92"
android:endY="189.51"
android:endX="139.67"
android:type="linear">
<item android:offset="0" android:color="#FFFFFFFF"/>
<item android:offset="0.37" android:color="#FFF5FAFC"/>
<item android:offset="1" android:color="#FFDBEBF5"/>
<item android:offset="1" android:color="#FFDBEBF5"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/divider"/>
<size android:height="0.1dp"/>
</shape>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>
@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<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"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.ui.AboutActivity">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:isScrollContainer="true">
<include layout="@layout/content_about" />
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+40 -36
View File
@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.AuthActivity">
tools:context="com.beemdevelopment.aegis.ui.AuthActivity">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
@@ -25,49 +26,52 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="12dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/textView4"
android:text="@string/authentication_enter_password"
android:textColor="?attr/secondaryText"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:id="@+id/text_password"/>
<Button
android:id="@+id/button_decrypt"
android:minWidth="125dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/unlock" />
</LinearLayout>
<LinearLayout
android:id="@+id/box_fingerprint"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="12dp"
android:visibility="gone">
android:text="@string/authentication_enter_password"
android:textColor="?attr/secondaryText" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:hintEnabled="false"
app:passwordToggleEnabled="true"
app:passwordToggleTint="#949494">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/img_fingerprint_insert"
android:layout_width="60dp"
android:layout_height="60dp"
android:orientation="horizontal"/>
<TextView
android:id="@+id/text_fingerprint"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="15dp"
android:text="@string/fingerprint_hint"
android:textColor="?attr/secondaryText"/>
android:layout_height="wrap_content">
<Button
android:id="@+id/button_decrypt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="125dp"
android:text="@string/unlock" />
<Button
android:id="@+id/button_biometrics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="125dp"
android:text="@string/biometrics"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/box_fingerprint_info"
android:id="@+id/box_biometric_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
@@ -83,7 +87,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/invalidated_fingerprint"/>
android:text="@string/invalidated_biometrics"/>
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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"
android:background="?attr/background"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:context="com.beemdevelopment.aegis.ui.EditEntryActivity">
<LinearLayout
android:layout_width="match_parent"
+2 -2
View File
@@ -6,12 +6,12 @@
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.GroupManagerActivity">
tools:context="com.beemdevelopment.aegis.ui.GroupManagerActivity">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<LinearLayout
android:layout_width="match_parent"
+1 -1
View File
@@ -5,6 +5,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.IntroActivity">
tools:context="com.beemdevelopment.aegis.ui.IntroActivity">
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+2 -2
View File
@@ -6,14 +6,14 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.MainActivity">
tools:context="com.beemdevelopment.aegis.ui.MainActivity">
<fragment
android:name="com.beemdevelopment.aegis.ui.views.EntryListView"
android:id="@+id/key_profiles"
android:layout_height="match_parent"
android:layout_width="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>
<!-- note: the fab should always be the last element to be sure it's displayed on top -->
<com.getbase.floatingactionbutton.FloatingActionsMenu
+1 -1
View File
@@ -9,6 +9,6 @@
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.ScannerActivity">
tools:context="com.beemdevelopment.aegis.ui.ScannerActivity">
</RelativeLayout>
+4 -7
View File
@@ -6,15 +6,12 @@
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.SlotManagerActivity">
tools:context="com.beemdevelopment.aegis.ui.SlotManagerActivity">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<LinearLayout
android:layout_width="match_parent"
@@ -56,7 +53,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/button_add_fingerprint"
android:id="@+id/button_add_biometric"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
@@ -78,7 +75,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_fingerprint"
android:text="@string/add_biometric"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
@@ -1,72 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="?attr/background"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:id="@+id/copy_button"
android:clickable="true"
android:focusable="true"
android:background="?attr/background"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_content_copy_black_24dp"
android:tint="?attr/iconColorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/copy"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/background"
android:id="@+id/edit_button"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_create_black_24dp"
android:tint="?attr/iconColorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:id="@+id/delete_button"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_delete_black_24dp"
android:tint="?attr/iconColorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/delete"/>
</LinearLayout>
</LinearLayout>
+1 -1
View File
@@ -6,12 +6,12 @@
android:clickable="true"
android:focusable="true"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="vertical">
<LinearLayout
android:orientation="horizontal"
android:background="?attr/cardBackground"
android:id="@+id/rlCardEntry"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:layout_width="match_parent"
@@ -11,6 +11,7 @@
<LinearLayout
android:orientation="horizontal"
android:background="?attr/cardBackground"
android:id="@+id/rlCardEntry"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:layout_width="match_parent"
@@ -128,10 +129,4 @@
android:layout_weight="1"/>
</LinearLayout>
<View
android:id="@+id/entryDivider"
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/divider" />
</LinearLayout>
+1 -1
View File
@@ -6,12 +6,12 @@
android:clickable="true"
android:focusable="true"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="vertical">
<LinearLayout
android:orientation="horizontal"
android:background="?attr/cardBackground"
android:id="@+id/rlCardEntry"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:layout_width="match_parent"
+485
View File
@@ -0,0 +1,485 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
app:cardUseCompatPadding="true">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true"
android:background="?attr/cardBackground"
android:layout_marginBottom="8dp"
tools:ignore="MissingPrefix">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:background="?attr/cardBackground"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="72dp"
android:orientation="horizontal"
android:background="?attr/selectableItemBackground"
android:padding="16dp">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center_vertical"
android:src="@mipmap/ic_launcher"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:text="@string/app_name_full"
android:textAppearance="@style/TextAppearance.AppCompat.Headline" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-info-outline"
app:ico_size="16dp" />
<LinearLayout
android:id="@+id/btn_app_version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/version"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:id="@+id/app_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0.0.0 (0)"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/btn_changelog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-time-restore"
app:ico_size="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/changelog"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/whats_new"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/btn_github"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-github"
app:ico_size="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/github"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/github_description"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/btn_licenses"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-file-text"
app:ico_size="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/licenses"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/licenses_description"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true"
android:background="?attr/cardBackground"
android:layout_marginBottom="8dp"
tools:ignore="MissingPrefix">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:background="?attr/cardBackground"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingLeft="16dp"
android:paddingTop="24dp"
android:paddingRight="16dp"
android:paddingBottom="16dp"
android:text="@string/org_name_full"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="?android:textColorSecondary" />
<LinearLayout
android:id="@+id/btn_alexander"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/list_item_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-account"
app:ico_size="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/author_alex"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/country_netherlands"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/btn_michael"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/list_item_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-account"
app:ico_size="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/author_michael"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/country_netherlands"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/btn_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/list_item_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-email"
app:ico_size="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/email_us"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
<LinearLayout
android:id="@+id/btn_website"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/list_item_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-globe"
app:ico_size="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/visit_website"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true"
android:layout_marginBottom="8dp"
android:background="?attr/cardBackground"
tools:ignore="MissingPrefix">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:background="?attr/cardBackground"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingLeft="16dp"
android:paddingTop="24dp"
android:paddingRight="16dp"
android:paddingBottom="16dp"
android:text="@string/about_support"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="?android:textColorSecondary" />
<LinearLayout
android:id="@+id/btn_rate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:clickable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/list_item_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
app:ico_color="@color/icon_about"
app:ico_icon="gmi-star"
app:ico_size="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_rate"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support_rate_description"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingTop="20dp">
<com.mattprecious.swirl.SwirlView
android:id="@+id/img_fingerprint"
android:layout_width="60dp"
android:layout_height="60dp"/>
<TextView
android:id="@+id/text_fingerprint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:text="@string/fingerprint_hint"
android:textColor="?attr/secondaryText"
android:layout_gravity="center_vertical"/>
</LinearLayout>

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