Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68bb2b7e17 | ||
|
|
b1d9be10f2 | ||
|
|
1d513441c6 | ||
|
|
0a31103f91 | ||
|
|
85bf4adc72 | ||
|
|
cc55a6dacb | ||
|
|
e099e2cacd | ||
|
|
658946ff70 | ||
|
|
b6a31ed789 | ||
|
|
9c272a0d4d | ||
|
|
3e626a37db | ||
|
|
e2150e3823 | ||
|
|
6480fef315 | ||
|
|
95df2ceb42 | ||
|
|
189698dddb | ||
|
|
438d5581aa | ||
|
|
7af78b11f5 | ||
|
|
5d68211ce9 | ||
|
|
a7e1a88dc8 | ||
|
|
3054032a37 | ||
|
|
79279630ba | ||
|
|
e019fb6db2 | ||
|
|
85bdecc573 | ||
|
|
21f3d93295 | ||
|
|
888e9d0e4a | ||
|
|
e6d84e6859 | ||
|
|
65371f18d4 | ||
|
|
1b5ab33c25 | ||
|
|
396dd0b169 | ||
|
|
eb40369634 | ||
|
|
0c87f06972 | ||
|
|
c774796422 | ||
|
|
c12c6ab107 | ||
|
|
e3b9b67fee | ||
|
|
2407b4b780 | ||
|
|
e962d91c77 | ||
|
|
d0afa2f61f | ||
|
|
58d13ba9e3 | ||
|
|
631ca76136 | ||
|
|
9cb9d47857 | ||
|
|
2323d89938 | ||
|
|
6769fefd00 | ||
|
|
5dbf1c7ce2 | ||
|
|
8d3c113475 | ||
|
|
087bf08d44 | ||
|
|
32b2ce280c | ||
|
|
37c9b033c5 | ||
|
|
f8d806999d | ||
|
|
86c4011797 | ||
|
|
bbc4c1d61c | ||
|
|
951e908d4b | ||
|
|
536929ff0e | ||
|
|
1f5a46b883 | ||
|
|
4cab47235b | ||
|
|
5524a77736 | ||
|
|
a3cdfe693d | ||
|
|
9e4ad1495d | ||
|
|
914da94c01 | ||
|
|
950dcce803 | ||
|
|
f82e480c32 | ||
|
|
9c5726cbca | ||
|
|
550e53f225 | ||
|
|
6e04ca054d | ||
|
|
b014d95005 | ||
|
|
0123b98e47 | ||
|
|
d7093487dd | ||
|
|
02c9a0cb1c | ||
|
|
eb29be587f | ||
|
|
f64a23d98f | ||
|
|
8bbadb9e57 | ||
|
|
4066cd83cc | ||
|
|
cca35bd5e5 | ||
|
|
187adfa738 | ||
|
|
246c03dea1 | ||
|
|
e68571a7a6 | ||
|
|
128c85fc0c | ||
|
|
c75b25c4b6 | ||
|
|
6abcfcea75 | ||
|
|
b300b72626 | ||
|
|
b2995955b6 | ||
|
|
1c9d07a47d | ||
|
|
66f99eb4d4 | ||
|
|
ff584a323d | ||
|
|
8c658ac930 | ||
|
|
588c1c07df | ||
|
|
f54639c791 | ||
|
|
7c78ddc929 | ||
|
|
9f3ccdc508 | ||
|
|
3556cd0e66 |
@@ -0,0 +1,33 @@
|
||||
---
|
||||
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 fingerprint unlock)/Yes/No
|
||||
* __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.
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: "Feature request"
|
||||
about: "Suggest a new feature for this project"
|
||||
labels: proposal
|
||||
---
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
language: android
|
||||
android:
|
||||
components:
|
||||
- android-28
|
||||
- build-tools-28.0.3
|
||||
script:
|
||||
- ./gradlew build test
|
||||
@@ -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.
|
||||
@@ -7,21 +7,41 @@ android {
|
||||
applicationId "com.beemdevelopment.aegis"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 10
|
||||
versionName "0.4"
|
||||
versionCode 22
|
||||
versionName "1.0.3"
|
||||
}
|
||||
|
||||
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
|
||||
@@ -48,5 +68,17 @@ dependencies {
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9'
|
||||
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.4.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.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);
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,16 @@
|
||||
|
||||
<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 +29,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 +60,8 @@
|
||||
<activity
|
||||
android:name=".ui.GroupManagerActivity"
|
||||
android:label="Manage groups" />
|
||||
|
||||
<service android:name=".services.NotificationService" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,70 @@
|
||||
<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.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>
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 28 KiB |
|
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,6 +13,7 @@ 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 java.util.ArrayList;
|
||||
@@ -24,6 +27,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();
|
||||
@@ -33,11 +39,18 @@ public class AegisApplication extends Application {
|
||||
|
||||
// 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 +78,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 +96,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;
|
||||
|
||||
@@ -16,7 +20,8 @@ public class Preferences {
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -70,4 +75,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,4 +137,11 @@ public class CryptoUtils {
|
||||
byteBuf.get(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static byte[] toBytesOld(char[] chars) {
|
||||
CharBuffer charBuf = CharBuffer.wrap(chars);
|
||||
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
|
||||
return byteBuf.array();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.beemdevelopment.aegis.db.slots;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.CryptParameters;
|
||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.crypto.SCryptParameters;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
|
||||
@@ -10,18 +11,21 @@ import org.json.JSONObject;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class PasswordSlot extends RawSlot {
|
||||
private boolean _repaired;
|
||||
private SCryptParameters _params;
|
||||
|
||||
public PasswordSlot() {
|
||||
super();
|
||||
}
|
||||
|
||||
protected PasswordSlot(UUID uuid, byte[] key, CryptParameters keyParams, SCryptParameters scryptParams) {
|
||||
protected PasswordSlot(UUID uuid, byte[] key, CryptParameters keyParams, SCryptParameters scryptParams, boolean repaired) {
|
||||
super(uuid, key, keyParams);
|
||||
_params = scryptParams;
|
||||
_repaired = repaired;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -32,6 +36,7 @@ public class PasswordSlot extends RawSlot {
|
||||
obj.put("r", _params.getR());
|
||||
obj.put("p", _params.getP());
|
||||
obj.put("salt", Hex.encode(_params.getSalt()));
|
||||
obj.put("repaired", _repaired);
|
||||
return obj;
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -48,6 +53,23 @@ public class PasswordSlot extends RawSlot {
|
||||
return CryptoUtils.deriveKey(password, _params);
|
||||
}
|
||||
|
||||
public SecretKey deriveKey(byte[] data) {
|
||||
return CryptoUtils.deriveKey(data, _params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setKey(MasterKey masterKey, Cipher cipher) throws SlotException {
|
||||
super.setKey(masterKey, cipher);
|
||||
_repaired = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this slot was repaired and is no longer affected by issue #95.
|
||||
*/
|
||||
public boolean isRepaired() {
|
||||
return _repaired;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getType() {
|
||||
return TYPE_DERIVED;
|
||||
|
||||
@@ -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;
|
||||
|
||||
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;
|
||||
@@ -129,7 +135,8 @@ public abstract class Slot implements Serializable {
|
||||
obj.getInt("p"),
|
||||
Hex.decode(obj.getString("salt"))
|
||||
);
|
||||
slot = new PasswordSlot(uuid, key, keyParams, scryptParams);
|
||||
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);
|
||||
@@ -145,8 +152,4 @@ public abstract class Slot implements Serializable {
|
||||
}
|
||||
|
||||
public abstract byte getType();
|
||||
|
||||
public UUID getUUID() {
|
||||
return _uuid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +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;
|
||||
|
||||
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) {
|
||||
@@ -37,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) {
|
||||
@@ -76,9 +57,4 @@ public class SlotList implements Iterable<Slot>, Serializable {
|
||||
public <T extends Slot> boolean has(Class<T> type) {
|
||||
return find(type) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Slot> iterator() {
|
||||
return _slots.iterator();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,17 @@ public class EditTextHelper {
|
||||
}
|
||||
|
||||
public static char[] getEditTextChars(EditText text) {
|
||||
Editable editable = text.getText();
|
||||
char[] chars = new char[editable.length()];
|
||||
editable.getChars(0, editable.length(), chars, 0);
|
||||
return getEditTextChars(text, false);
|
||||
}
|
||||
|
||||
public static char[] getEditTextChars(EditText text, boolean removeSpaces) {
|
||||
String editTextString = text.getText().toString();
|
||||
if (removeSpaces) {
|
||||
editTextString = editTextString.replaceAll("\\s","");
|
||||
}
|
||||
|
||||
char[] chars = new char[editTextString.length()];
|
||||
editTextString.getChars(0, editTextString.length(), chars, 0);
|
||||
return chars;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,16 @@ import android.content.Context;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class FingerprintHelper {
|
||||
private FingerprintHelper() {
|
||||
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public static FingerprintManager getManager(Context context) {
|
||||
if (isSupported() && PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
|
||||
if (PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
|
||||
FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
|
||||
if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
|
||||
return manager;
|
||||
@@ -20,6 +23,11 @@ public class FingerprintHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public static boolean isAvailable(Context context) {
|
||||
return getManager(context) != null;
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,17 @@ 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);
|
||||
|
||||
_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 +116,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 +127,7 @@ public abstract class DatabaseImporter {
|
||||
_errors.add(error);
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getEntries() {
|
||||
public UUIDMap<DatabaseEntry> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
@@ -131,43 +136,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));
|
||||
|
||||
@@ -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,121 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
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.Iconics;
|
||||
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
|
||||
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
|
||||
|
||||
import androidx.core.view.LayoutInflaterCompat;
|
||||
|
||||
import de.psdev.licensesdialog.LicenseResolver;
|
||||
import de.psdev.licensesdialog.LicensesDialog;
|
||||
import de.psdev.licensesdialog.licenses.License;
|
||||
|
||||
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);
|
||||
|
||||
Iconics.init(getApplicationContext());
|
||||
Iconics.registerFont(new MaterialDesignIconic());
|
||||
|
||||
View btnLicenses = findViewById(R.id.btn_licenses);
|
||||
btnLicenses.setOnClickListener(v -> showLicenseDialog());
|
||||
|
||||
TextView appVersion = findViewById(R.id.app_version);
|
||||
appVersion.setText(getCurrentVersion());
|
||||
|
||||
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 String getCurrentVersion() {
|
||||
try {
|
||||
return getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return "Unknown version";
|
||||
}
|
||||
|
||||
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 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,15 +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
|
||||
@@ -53,6 +60,18 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
_resumed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
_resumed = false;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onLocked() {
|
||||
@@ -71,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);
|
||||
@@ -84,6 +105,22 @@ 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)
|
||||
*/
|
||||
protected boolean isOpen() {
|
||||
return _resumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this Activity instance has become an orphan. This can happen if
|
||||
* the vault was locked by an external trigger while the Activity was still open.
|
||||
@@ -102,4 +139,8 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Theme getCurrentTheme() {
|
||||
return _currentTheme;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.beemdevelopment.aegis.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
@@ -17,9 +18,9 @@ import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.CancelAction;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
|
||||
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
|
||||
import com.beemdevelopment.aegis.crypto.MasterKey;
|
||||
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
|
||||
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
|
||||
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
|
||||
@@ -40,11 +41,13 @@ import androidx.appcompat.app.AlertDialog;
|
||||
public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotListTask.Callback {
|
||||
private EditText _textPassword;
|
||||
|
||||
private CancelAction _cancelAction;
|
||||
private SlotList _slots;
|
||||
private FingerprintUiHelper _fingerHelper;
|
||||
private FingerprintManager.CryptoObject _fingerCryptoObj;
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_auth);
|
||||
@@ -70,11 +73,13 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
|
||||
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)) {
|
||||
if (_slots.has(FingerprintSlot.class) && FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(this)) {
|
||||
boolean invalidated = false;
|
||||
FingerprintManager manager = FingerprintHelper.getManager(this);
|
||||
|
||||
try {
|
||||
// find a fingerprint slot with an id that matches an alias in the keystore
|
||||
for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
|
||||
@@ -136,14 +141,6 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
new SlotListTask<>(type, this, this).execute(params);
|
||||
}
|
||||
|
||||
private void setKey(MasterKey key) {
|
||||
// send the master key back to the main activity
|
||||
Intent result = new Intent();
|
||||
result.putExtra("creds", new DatabaseFileCredentials(key, _slots));
|
||||
setResult(RESULT_OK, result);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void selectPassword() {
|
||||
_textPassword.selectAll();
|
||||
|
||||
@@ -153,10 +150,19 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
|
||||
@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
|
||||
@SuppressLint("NewApi")
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
@@ -166,6 +172,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
@@ -175,6 +182,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
public void onAuthenticated() {
|
||||
trySlots(FingerprintSlot.class, _fingerCryptoObj.getCipher());
|
||||
}
|
||||
@@ -185,9 +193,19 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskFinished(MasterKey key) {
|
||||
if (key != null) {
|
||||
setKey(key);
|
||||
public void onTaskFinished(SlotListTask.Result result) {
|
||||
if (result != null) {
|
||||
// replace the old slot with the repaired one
|
||||
if (result.isSlotRepaired()) {
|
||||
_slots.replace(result.getSlot());
|
||||
}
|
||||
|
||||
// send the master key back to the main activity
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("creds", new DatabaseFileCredentials(result.getKey(), _slots));
|
||||
intent.putExtra("repairedSlot", result.isSlotRepaired());
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
} else {
|
||||
showError();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
@@ -35,6 +36,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
@@ -178,6 +180,7 @@ public class Dialogs {
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
@@ -444,12 +458,11 @@ public class EditEntryActivity extends AegisActivity {
|
||||
|
||||
byte[] secret;
|
||||
try {
|
||||
secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret));
|
||||
secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret, true));
|
||||
} catch (Base32Exception e) {
|
||||
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);
|
||||
|
||||
@@ -65,7 +65,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 +83,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);
|
||||
|
||||
@@ -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;
|
||||
@@ -101,12 +114,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 +124,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 +162,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;
|
||||
}
|
||||
|
||||
@@ -190,6 +204,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
case CODE_PERM_CAMERA:
|
||||
startScanActivity();
|
||||
break;
|
||||
case CODE_PERM_READ_STORAGE:
|
||||
startScanImageActivity();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,10 +229,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 +259,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,14 +318,14 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -324,7 +340,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);
|
||||
}
|
||||
|
||||
@@ -346,7 +362,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
private void onDecryptResult(int resultCode, Intent intent) {
|
||||
DatabaseFileCredentials creds = (DatabaseFileCredentials) intent.getSerializableExtra("creds");
|
||||
unlockDatabase(creds);
|
||||
boolean unlocked = unlockDatabase(creds);
|
||||
|
||||
// save the database in case a slot was repaired
|
||||
if (unlocked && intent.getBooleanExtra("repairedSlot", false)) {
|
||||
_db.setCredentials(creds);
|
||||
saveDatabase();
|
||||
}
|
||||
|
||||
doShortcutActions();
|
||||
}
|
||||
@@ -360,6 +382,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");
|
||||
@@ -376,6 +410,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();
|
||||
@@ -405,61 +466,41 @@ 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
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(IconicsContextWrapper.wrap(newBase));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -471,19 +512,55 @@ 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();
|
||||
startAuthActivity();
|
||||
return true;
|
||||
default:
|
||||
if (item.getGroupId() == R.id.action_filter_group) {
|
||||
@@ -526,11 +603,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private void unlockDatabase(DatabaseFileCredentials creds) {
|
||||
if (_loaded) {
|
||||
return;
|
||||
}
|
||||
private void collapseSearchView() {
|
||||
_searchView.setQuery(null, false);
|
||||
_searchView.setIconified(true);
|
||||
}
|
||||
|
||||
private boolean unlockDatabase(DatabaseFileCredentials creds) {
|
||||
try {
|
||||
if (!_db.isLoaded()) {
|
||||
_db.load();
|
||||
@@ -538,7 +616,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
if (_db.isLocked()) {
|
||||
if (creds == null) {
|
||||
startAuthActivity();
|
||||
return;
|
||||
return false;
|
||||
} else {
|
||||
_db.unlock(creds);
|
||||
}
|
||||
@@ -546,10 +624,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
} catch (DatabaseManagerException e) {
|
||||
Toast.makeText(this, getString(R.string.decryption_error), Toast.LENGTH_LONG).show();
|
||||
startAuthActivity();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
loadEntries();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void loadEntries() {
|
||||
@@ -564,6 +643,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);
|
||||
}
|
||||
|
||||
@@ -585,7 +665,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
|
||||
@@ -610,8 +712,63 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
|
||||
@Override
|
||||
public void onLocked() {
|
||||
if (_actionMode != null) {
|
||||
_actionMode.finish();
|
||||
}
|
||||
|
||||
_entryListView.clearEntries();
|
||||
_loaded = false;
|
||||
|
||||
if (isOpen()) {
|
||||
startAuthActivity();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.widget.Toast;
|
||||
|
||||
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;
|
||||
@@ -35,13 +36,18 @@ 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.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.takisoft.preferencex.PreferenceFragmentCompat;
|
||||
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;
|
||||
@@ -72,9 +78,11 @@ 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 Preference _autoLockPreference;
|
||||
private Preference _setPasswordPreference;
|
||||
private Preference _slotsPreference;
|
||||
private Preference _groupsPreference;
|
||||
@@ -115,6 +123,13 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
});
|
||||
|
||||
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]));
|
||||
@@ -251,6 +266,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
getActivity().stopService(new Intent(getActivity(), NotificationService.class));
|
||||
updateEncryptionPreferences();
|
||||
}
|
||||
})
|
||||
@@ -269,7 +285,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
SlotList slots = creds.getSlots();
|
||||
|
||||
if (!slots.has(FingerprintSlot.class)) {
|
||||
Dialogs.showFingerprintDialog(getActivity(), new RegisterFingerprintListener());
|
||||
if (FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(getContext())) {
|
||||
Dialogs.showFingerprintDialog(getActivity(), new RegisterFingerprintListener());
|
||||
}
|
||||
} else {
|
||||
// remove the fingerprint slot
|
||||
FingerprintSlot slot = slots.find(FingerprintSlot.class);
|
||||
@@ -319,6 +337,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
_autoLockPreference = findPreference("pref_auto_lock");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -428,7 +448,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 +471,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 +522,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 +545,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 +626,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);
|
||||
@@ -637,14 +666,16 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
_setPasswordPreference.setVisible(encrypted);
|
||||
_fingerprintPreference.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 canUseFinger = FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(getContext());
|
||||
_setPasswordPreference.setEnabled(!multiPassword);
|
||||
_fingerprintPreference.setEnabled(FingerprintHelper.getManager(getContext()) != null && !multiFinger);
|
||||
_fingerprintPreference.setEnabled(canUseFinger && !multiFinger);
|
||||
_fingerprintPreference.setChecked(slots.has(FingerprintSlot.class), true);
|
||||
_slotsPreference.setVisible(showSlots);
|
||||
} else {
|
||||
@@ -727,6 +758,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();
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
findViewById(R.id.button_add_fingerprint).setOnClickListener(view -> {
|
||||
Dialogs.showFingerprintDialog(this ,this);
|
||||
if (FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(this)) {
|
||||
Dialogs.showFingerprintDialog(this ,this);
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.button_add_password).setOnClickListener(view -> {
|
||||
@@ -71,7 +73,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
|
||||
// only show the fingerprint option if we can get an instance of the fingerprint 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 (FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(this)) {
|
||||
try {
|
||||
KeyStoreHandle keyStore = new KeyStoreHandle();
|
||||
for (FingerprintSlot slot : _creds.getSlots().findAll(FingerprintSlot.class)) {
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
package com.beemdevelopment.aegis.ui.slides;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
@@ -70,6 +71,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
|
||||
return EditTextHelper.getEditTextChars(_textPassword);
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public Cipher getFingerCipher() {
|
||||
return _fingerCryptoObj.getCipher();
|
||||
}
|
||||
@@ -83,6 +85,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
public void onSlideSelected() {
|
||||
Intent intent = getActivity().getIntent();
|
||||
_cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
|
||||
@@ -124,6 +127,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
public void onSlideDeselected() {
|
||||
if (_fingerHelper != null) {
|
||||
_fingerAuthenticated = false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.beemdevelopment.aegis.ui.slides;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Bundle;
|
||||
@@ -34,8 +35,7 @@ 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) {
|
||||
if (FingerprintHelper.isSupported() && FingerprintHelper.isAvailable(getContext())) {
|
||||
RadioButton button = view.findViewById(R.id.rb_fingerprint);
|
||||
TextView text = view.findViewById(R.id.text_rb_fingerprint);
|
||||
button.setEnabled(true);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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.PasswordSlot;
|
||||
@@ -14,7 +16,7 @@ import com.beemdevelopment.aegis.db.slots.SlotList;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTask.Params, MasterKey> {
|
||||
public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTask.Params, SlotListTask.Result> {
|
||||
private Callback _cb;
|
||||
private Class<T> _type;
|
||||
|
||||
@@ -25,51 +27,81 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MasterKey doInBackground(SlotListTask.Params... args) {
|
||||
protected Result doInBackground(SlotListTask.Params... args) {
|
||||
setPriority();
|
||||
|
||||
Params params = args[0];
|
||||
SlotList slots = params.getSlots();
|
||||
try {
|
||||
if (!slots.has(_type)) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
MasterKey masterKey = null;
|
||||
for (Slot slot : slots.findAll(_type)) {
|
||||
try {
|
||||
if (slot instanceof PasswordSlot) {
|
||||
char[] password = (char[])params.getObj();
|
||||
SecretKey key = ((PasswordSlot)slot).deriveKey(password);
|
||||
Cipher cipher = slot.createDecryptCipher(key);
|
||||
masterKey = slot.getKey(cipher);
|
||||
} else if (slot instanceof FingerprintSlot) {
|
||||
masterKey = slot.getKey((Cipher)params.getObj());
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
break;
|
||||
} catch (SlotIntegrityException e) {
|
||||
|
||||
for (Slot slot : slots.findAll(_type)) {
|
||||
try {
|
||||
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());
|
||||
}
|
||||
}
|
||||
} catch (SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (SlotIntegrityException ignored) {
|
||||
|
||||
if (masterKey == null) {
|
||||
throw new SlotIntegrityException();
|
||||
}
|
||||
|
||||
return masterKey;
|
||||
} catch (SlotIntegrityException e) {
|
||||
return null;
|
||||
} catch (SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Result decryptFingerprintSlot(FingerprintSlot slot, Cipher cipher)
|
||||
throws SlotException, SlotIntegrityException {
|
||||
MasterKey key = slot.getKey(cipher);
|
||||
return new Result(key, slot);
|
||||
}
|
||||
|
||||
private Result decryptPasswordSlot(PasswordSlot slot, char[] password)
|
||||
throws SlotIntegrityException, SlotException {
|
||||
MasterKey masterKey;
|
||||
SecretKey key = slot.deriveKey(password);
|
||||
byte[] oldPasswordBytes = CryptoUtils.toBytesOld(password);
|
||||
|
||||
try {
|
||||
masterKey = decryptPasswordSlot(slot, key);
|
||||
} catch (SlotIntegrityException e) {
|
||||
// a bug introduced in afb9e59 caused passwords longer than 64 bytes to produce a different key than before
|
||||
// so, try again with the old password encode function if the password is longer than 64 bytes
|
||||
if (slot.isRepaired() || oldPasswordBytes.length <= 64) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
ProgressDialog dialog = getDialog();
|
||||
dialog.setMessage(dialog.getContext().getString(R.string.unlocking_vault_repair));
|
||||
|
||||
// try to decrypt the password slot with the old key
|
||||
SecretKey oldKey = slot.deriveKey(oldPasswordBytes);
|
||||
masterKey = decryptPasswordSlot(slot, oldKey);
|
||||
}
|
||||
|
||||
// if necessary, repair the slot by re-encrypting the master key with the correct key
|
||||
// slots with passwords smaller than 64 bytes also get this treatment to make sure those also have 'repaired' set to true
|
||||
boolean repaired = false;
|
||||
if (!slot.isRepaired()) {
|
||||
Cipher cipher = Slot.createEncryptCipher(key);
|
||||
slot.setKey(masterKey, cipher);
|
||||
repaired = true;
|
||||
}
|
||||
|
||||
return new Result(masterKey, slot, repaired);
|
||||
}
|
||||
|
||||
private MasterKey decryptPasswordSlot(PasswordSlot slot, SecretKey key)
|
||||
throws SlotException, SlotIntegrityException {
|
||||
Cipher cipher = slot.createDecryptCipher(key);
|
||||
return slot.getKey(cipher);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(MasterKey masterKey) {
|
||||
super.onPostExecute(masterKey);
|
||||
_cb.onTaskFinished(masterKey);
|
||||
protected void onPostExecute(Result result) {
|
||||
super.onPostExecute(result);
|
||||
_cb.onTaskFinished(result);
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
@@ -90,7 +122,35 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
|
||||
}
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private MasterKey _key;
|
||||
private Slot _slot;
|
||||
private boolean _repaired;
|
||||
|
||||
public Result(MasterKey key, Slot slot, boolean repaired) {
|
||||
_key = key;
|
||||
_slot = slot;
|
||||
_repaired = repaired;
|
||||
}
|
||||
|
||||
public Result(MasterKey key, Slot slot) {
|
||||
this(key, slot, false);
|
||||
}
|
||||
|
||||
public MasterKey getKey() {
|
||||
return _key;
|
||||
}
|
||||
|
||||
public Slot getSlot() {
|
||||
return _slot;
|
||||
}
|
||||
|
||||
public boolean isSlotRepaired() {
|
||||
return _repaired;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(MasterKey key);
|
||||
void onTaskFinished(Result result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,30 +17,38 @@ 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 boolean _showAccountName;
|
||||
private boolean _tapToReveal;
|
||||
private int _tapToRevealTime;
|
||||
private String _groupFilter;
|
||||
private SortCategory _sortCategory;
|
||||
private ViewMode _viewMode;
|
||||
private String _searchFilter;
|
||||
private boolean _isPeriodUniform = true;
|
||||
|
||||
// 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;
|
||||
_view = view;
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
for (EntryHolder holder : _holders) {
|
||||
holder.destroy();
|
||||
}
|
||||
_view = null;
|
||||
}
|
||||
|
||||
public void setShowAccountName(boolean showAccountName) {
|
||||
@@ -55,6 +63,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
_tapToRevealTime = number;
|
||||
}
|
||||
|
||||
public DatabaseEntry getEntryAt(int position) {
|
||||
return _shownEntries.get(position);
|
||||
}
|
||||
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
_entries.add(entry);
|
||||
if (isEntryFiltered(entry)) {
|
||||
@@ -93,11 +105,10 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
public void addEntries(List<DatabaseEntry> entries) {
|
||||
_entries.addAll(entries);
|
||||
updateShownEntries();
|
||||
checkPeriodUniformity();
|
||||
checkPeriodUniformity(true);
|
||||
}
|
||||
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
entry = getEntryByUUID(entry.getUUID());
|
||||
_entries.remove(entry);
|
||||
|
||||
if (_shownEntries.contains(entry)) {
|
||||
@@ -113,10 +124,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 +152,13 @@ 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();
|
||||
|
||||
private DatabaseEntry getEntryByUUID(UUID uuid) {
|
||||
for (DatabaseEntry entry : _entries) {
|
||||
if (entry.getUUID().equals(uuid)) {
|
||||
return entry;
|
||||
}
|
||||
if (_groupFilter != null && (group == null || !group.equals(_groupFilter))) {
|
||||
return true;
|
||||
}
|
||||
throw new AssertionError("no entry found with the same id");
|
||||
|
||||
return _searchFilter != null && !issuer.contains(_searchFilter);
|
||||
}
|
||||
|
||||
public void refresh(boolean hard) {
|
||||
@@ -181,12 +186,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 +234,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
return;
|
||||
}
|
||||
|
||||
_listener.onEntryDrop(_shownEntries.get(position));
|
||||
_view.onEntryDrop(_shownEntries.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -234,7 +245,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 +262,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 +276,21 @@ 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 showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
|
||||
holder.setData(entry, _showAccountName, showProgress, _tapToReveal);
|
||||
holder.setTapToRevealTime(_tapToRevealTime);
|
||||
holder.loadIcon(_view);
|
||||
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
if (_tapToReveal && holder.isCodeHidden()) {
|
||||
if (_tapToReveal && holder.isCodeHidden() && _selectedEntry == null) {
|
||||
holder.revealCode();
|
||||
} else {
|
||||
_listener.onEntryClick(_shownEntries.get(position));
|
||||
_view.onEntryClick(_shownEntries.get(position));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -282,7 +298,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 +318,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();
|
||||
@@ -308,8 +329,12 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
}
|
||||
|
||||
private void checkPeriodUniformity() {
|
||||
checkPeriodUniformity(false);
|
||||
}
|
||||
|
||||
private void checkPeriodUniformity(boolean force) {
|
||||
boolean uniform = isPeriodUniform();
|
||||
if (uniform == _isPeriodUniform) {
|
||||
if (!force && uniform == _isPeriodUniform) {
|
||||
return;
|
||||
}
|
||||
_isPeriodUniform = uniform;
|
||||
@@ -318,7 +343,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
holder.setShowProgress(!_isPeriodUniform);
|
||||
}
|
||||
|
||||
_listener.onPeriodUniformityChanged(_isPeriodUniform);
|
||||
_view.onPeriodUniformityChanged(_isPeriodUniform);
|
||||
}
|
||||
|
||||
public int getUniformPeriod() {
|
||||
@@ -344,6 +369,18 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
return period;
|
||||
}
|
||||
|
||||
public void setSelectedEntry(DatabaseEntry entry) {
|
||||
if (entry == null) {
|
||||
notifyItemChanged(_shownEntries.indexOf(_selectedEntry));
|
||||
}
|
||||
|
||||
_selectedEntry = entry;
|
||||
}
|
||||
|
||||
public boolean isDragAndDropAllowed() {
|
||||
return _sortCategory == SortCategory.CUSTOM && _groupFilter == null && _searchFilter == null;
|
||||
}
|
||||
|
||||
public boolean isPeriodUniform() {
|
||||
return getUniformPeriod() != -1;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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;
|
||||
@@ -12,12 +10,16 @@ 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 com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
@@ -27,12 +29,12 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
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;
|
||||
@@ -40,17 +42,18 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
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
|
||||
@@ -87,15 +90,6 @@ 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);
|
||||
|
||||
@@ -106,6 +100,24 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
}
|
||||
}
|
||||
|
||||
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 setTapToRevealTime(int number) {
|
||||
_tapToRevealTime = number;
|
||||
}
|
||||
@@ -122,17 +134,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();
|
||||
}
|
||||
@@ -150,17 +170,19 @@ 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() {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
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 com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.SortCategory;
|
||||
@@ -15,10 +17,21 @@ 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.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;
|
||||
@@ -29,8 +42,12 @@ public class EntryListView extends Fragment implements EntryAdapter.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());
|
||||
@@ -167,22 +221,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 +251,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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
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>
|
||||
|
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>
|
||||
@@ -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,23 +26,38 @@
|
||||
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"/>
|
||||
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>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_decrypt"
|
||||
android:layout_width="125dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="125dp"
|
||||
android:text="@string/unlock" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/box_fingerprint"
|
||||
android:orientation="horizontal"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
<?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: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>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/web_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="none" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -22,4 +22,47 @@
|
||||
android:id="@+id/rvKeyProfiles"
|
||||
android:layoutAnimation="@anim/layout_animation_fall_down"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center|fill_vertical"
|
||||
android:id="@+id/vEmptyList"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="150dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:src="@drawable/ic_qrcode_scan"
|
||||
android:tint="?attr/primaryText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/empty_list_title"
|
||||
android:paddingTop="17dp"
|
||||
android:textColor="?attr/primaryText"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView5"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineSpacingExtra="5dp"
|
||||
android:paddingTop="7dp"
|
||||
android:text="@string/empty_list"
|
||||
android:textAlignment="center" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/action_edit"
|
||||
android:orderInCategory="90"
|
||||
android:icon="@drawable/ic_mode_edit"
|
||||
android:title="Edit"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:title="@string/action_delete"
|
||||
android:orderInCategory="100"
|
||||
android:icon="@drawable/ic_delete_white"
|
||||
android:tint="?attr/iconColorPrimary"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
@@ -2,7 +2,7 @@
|
||||
<menu 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"
|
||||
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.EditEntryActivity">
|
||||
tools:context="com.beemdevelopment.aegis.ui.EditEntryActivity">
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
app:showAsAction="ifRoom"
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<menu 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"
|
||||
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.MainActivity">
|
||||
tools:context="com.beemdevelopment.aegis.ui.MainActivity">
|
||||
<item
|
||||
android:id="@+id/mi_search"
|
||||
android:title="@string/search"
|
||||
app:showAsAction="always"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
/>
|
||||
<item
|
||||
android:id="@+id/action_lock"
|
||||
android:icon="@drawable/ic_lock"
|
||||
@@ -57,4 +63,9 @@
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/action_settings"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_about"
|
||||
android:orderInCategory="110"
|
||||
android:title="@string/action_about"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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"
|
||||
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.ScannerActivity">
|
||||
tools:context="com.beemdevelopment.aegis.ui.ScannerActivity">
|
||||
<item
|
||||
android:id="@+id/action_camera"
|
||||
android:icon="@drawable/ic_camera_front_24dp"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<menu 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"
|
||||
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.SlotManagerActivity">
|
||||
tools:context="com.beemdevelopment.aegis.ui.SlotManagerActivity">
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
app:showAsAction="ifRoom"
|
||||
|
||||