mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge remote-tracking branch 'hanscappelle/feature/fingerprint-password-handling' into feature/fingerprint-password-handling
This commit is contained in:
51
.gitignore
vendored
51
.gitignore
vendored
@@ -1,8 +1,51 @@
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/app/build
|
||||
/projectFilesBackup
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea/*
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion = 22
|
||||
compileSdkVersion = 23
|
||||
buildToolsVersion = '26.0.2'
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.android.keepass"
|
||||
minSdkVersion 3
|
||||
minSdkVersion 4
|
||||
targetSdkVersion 12
|
||||
|
||||
versionCode = 155
|
||||
@@ -48,4 +48,6 @@ dependencies {
|
||||
compile 'com.madgag.spongycastle:core:1.58.0.0'
|
||||
compile 'com.madgag.spongycastle:prov:1.58.0.0'
|
||||
compile 'joda-time:joda-time:2.9.4'
|
||||
compile 'com.android.support:support-v4:23.0.0'
|
||||
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
android:largeScreens="true"
|
||||
android:anyDensity="true"
|
||||
/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
|
||||
<uses-permission android:name="android.permission.VIBRATE"></uses-permission>
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/launcher"
|
||||
|
||||
@@ -41,6 +41,7 @@ import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.text.SpannableString;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.method.PasswordTransformationMethod;
|
||||
@@ -228,12 +229,16 @@ public class EntryActivity extends LockCloseHideActivity {
|
||||
|
||||
private Notification getNotification(String intentText, int descResId) {
|
||||
String desc = getString(descResId);
|
||||
Notification notify = new Notification(R.drawable.notify, desc, System.currentTimeMillis());
|
||||
|
||||
Intent intent = new Intent(intentText);
|
||||
PendingIntent pending = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
notify.setLatestEventInfo(this, getString(R.string.app_name), desc, pending);
|
||||
// no longer supported for api level >22
|
||||
// notify.setLatestEventInfo(this, getString(R.string.app_name), desc, pending);
|
||||
// so instead using compat builder and create new notification
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
|
||||
Notification notify = builder.setContentIntent(pending).setContentText(desc).setContentTitle(getString(R.string.app_name))
|
||||
.setSmallIcon(R.drawable.notify).setTicker(desc).setWhen(System.currentTimeMillis()).build();
|
||||
|
||||
return notify;
|
||||
}
|
||||
|
||||
@@ -19,21 +19,22 @@
|
||||
*/
|
||||
package com.keepassdroid;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -58,6 +59,7 @@ import com.keepassdroid.database.edit.LoadDB;
|
||||
import com.keepassdroid.database.edit.OnFinish;
|
||||
import com.keepassdroid.dialog.PasswordEncodingDialogHelper;
|
||||
import com.keepassdroid.fileselect.BrowserDialog;
|
||||
import com.keepassdroid.fingerprint.FingerPrintHelper;
|
||||
import com.keepassdroid.intents.Intents;
|
||||
import com.keepassdroid.settings.AppSettingsActivity;
|
||||
import com.keepassdroid.utils.EmptyUtils;
|
||||
@@ -68,7 +70,9 @@ import com.keepassdroid.utils.Util;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
public class PasswordActivity extends LockingActivity {
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
public class PasswordActivity extends LockingActivity implements FingerPrintHelper.FingerPrintCallback {
|
||||
|
||||
public static final String KEY_DEFAULT_FILENAME = "defaultFileName";
|
||||
private static final String KEY_FILENAME = "fileName";
|
||||
@@ -86,11 +90,25 @@ public class PasswordActivity extends LockingActivity {
|
||||
private boolean mRememberKeyfile;
|
||||
SharedPreferences prefs;
|
||||
|
||||
public static void Launch(Activity act, String fileName) throws FileNotFoundException {
|
||||
Launch(act,fileName,"");
|
||||
private FingerPrintHelper fingerPrintHelper;
|
||||
private int mode;
|
||||
private static final String PREF_KEY_VALUE_PREFIX = "valueFor_"; // key is a combination of db file name and this prefix
|
||||
private static final String PREF_KEY_IV_PREFIX = "ivFor_"; // key is a combination of db file name and this prefix
|
||||
private View fingerprintView;
|
||||
private TextView confirmationView;
|
||||
private EditText passwordView;
|
||||
private Button confirmButton;
|
||||
|
||||
public static void Launch(
|
||||
Activity act,
|
||||
String fileName) throws FileNotFoundException {
|
||||
Launch(act, fileName, "");
|
||||
}
|
||||
|
||||
public static void Launch(Activity act, String fileName, String keyFile) throws FileNotFoundException {
|
||||
public static void Launch(
|
||||
Activity act,
|
||||
String fileName,
|
||||
String keyFile) throws FileNotFoundException {
|
||||
if (EmptyUtils.isNullOrEmpty(fileName)) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
@@ -114,7 +132,10 @@ public class PasswordActivity extends LockingActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
protected void onActivityResult(
|
||||
int requestCode,
|
||||
int resultCode,
|
||||
Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
switch (requestCode) {
|
||||
@@ -146,7 +167,7 @@ public class PasswordActivity extends LockingActivity {
|
||||
if (data != null) {
|
||||
Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
if (requestCode==GET_CONTENT) {
|
||||
if (requestCode == GET_CONTENT) {
|
||||
uri = UriUtil.translate(this, uri);
|
||||
}
|
||||
String path = uri.toString();
|
||||
@@ -173,7 +194,14 @@ public class PasswordActivity extends LockingActivity {
|
||||
mRememberKeyfile = prefs.getBoolean(getString(R.string.keyfile_key), getResources().getBoolean(R.bool.keyfile_default));
|
||||
setContentView(R.layout.password);
|
||||
|
||||
confirmButton = (Button) findViewById(R.id.pass_ok);
|
||||
fingerprintView = findViewById(R.id.fingerprint);
|
||||
confirmationView = (TextView) findViewById(R.id.fingerprint_label);
|
||||
passwordView = (EditText) findViewById(R.id.password);
|
||||
|
||||
new InitTask().execute(i);
|
||||
|
||||
initForFingerprint();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -189,6 +217,9 @@ public class PasswordActivity extends LockingActivity {
|
||||
|
||||
// Clear the shutdown flag
|
||||
App.clearShutdown();
|
||||
|
||||
// checks if fingerprint is available, will also start listening for fingerprints when available
|
||||
checkAvailability();
|
||||
}
|
||||
|
||||
private void retrieveSettings() {
|
||||
@@ -200,7 +231,7 @@ public class PasswordActivity extends LockingActivity {
|
||||
}
|
||||
|
||||
private Uri getKeyFile(Uri dbUri) {
|
||||
if ( mRememberKeyfile ) {
|
||||
if (mRememberKeyfile) {
|
||||
|
||||
return App.getFileHistory().getFileByName(dbUri);
|
||||
} else {
|
||||
@@ -223,15 +254,215 @@ public class PasswordActivity extends LockingActivity {
|
||||
}
|
||||
*/
|
||||
|
||||
private void errorMessage(int resId)
|
||||
{
|
||||
private void errorMessage(int resId) {
|
||||
Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
// fingerprint related code here
|
||||
|
||||
private void initForFingerprint() {
|
||||
fingerPrintHelper = new FingerPrintHelper(this, this);
|
||||
if (fingerPrintHelper.isFingerprintSupported()) {
|
||||
|
||||
// when text entered we can enable the logon/purchase button and if required update encryption/decryption mode
|
||||
passwordView.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(
|
||||
final CharSequence s,
|
||||
final int start,
|
||||
final int count,
|
||||
final int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(
|
||||
final CharSequence s,
|
||||
final int start,
|
||||
final int before,
|
||||
final int count) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(final Editable s) {
|
||||
final boolean validInput = s.length() > 0;
|
||||
// encrypt or decrypt mode based on how much input or not
|
||||
confirmationView.setText(validInput ? R.string.store_with_fingerprint : R.string.scanning_fingerprint);
|
||||
mode = validInput ? toggleMode(Cipher.ENCRYPT_MODE) : toggleMode(Cipher.DECRYPT_MODE);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// callback for fingerprint findings
|
||||
fingerPrintHelper.setAuthenticationCallback(new FingerprintManager.AuthenticationCallback() {
|
||||
@Override
|
||||
public void onAuthenticationError(
|
||||
final int errorCode,
|
||||
final CharSequence errString) {
|
||||
|
||||
// this is triggered on stop/start listening done by helper to switch between modes so don't restart here
|
||||
// errorCode = 5
|
||||
// errString = "Fingerprint operation canceled."
|
||||
//onException();
|
||||
//confirmationView.setText(errString);
|
||||
// true false fingerprint readings are handled otherwise with the toast messages, see below in code
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationHelp(
|
||||
final int helpCode,
|
||||
final CharSequence helpString) {
|
||||
|
||||
onException();
|
||||
confirmationView.setText(helpString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(final FingerprintManager.AuthenticationResult result) {
|
||||
|
||||
if (mode == Cipher.ENCRYPT_MODE) {
|
||||
|
||||
// newly store the entered password in encrypted way
|
||||
final String password = passwordView.getText().toString();
|
||||
fingerPrintHelper.encryptData(password);
|
||||
|
||||
} else if (mode == Cipher.DECRYPT_MODE) {
|
||||
|
||||
// retrieve the encrypted value from preferences
|
||||
final String encryptedValue = prefs.getString(getPreferenceKeyValue(), null);
|
||||
if (encryptedValue != null) {
|
||||
fingerPrintHelper.decryptData(encryptedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
onException();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private String getPreferenceKeyValue() {
|
||||
// makes it possible to store passwords uniqly per database
|
||||
return PREF_KEY_VALUE_PREFIX + (mDbUri != null ? mDbUri.getPath() : "");
|
||||
}
|
||||
|
||||
private String getPreferenceKeyIvSpec() {
|
||||
return PREF_KEY_IV_PREFIX + (mDbUri != null ? mDbUri.getPath() : "");
|
||||
}
|
||||
|
||||
private int toggleMode(final int newMode) {
|
||||
// check if mode is different so we can update fingerprint helper
|
||||
if (mode != newMode) {
|
||||
mode = newMode;
|
||||
switch (mode) {
|
||||
case Cipher.ENCRYPT_MODE:
|
||||
fingerPrintHelper.initEncryptData();
|
||||
break;
|
||||
case Cipher.DECRYPT_MODE:
|
||||
final String ivSpecValue = prefs.getString(getPreferenceKeyIvSpec(), null);
|
||||
fingerPrintHelper.initDecryptData(ivSpecValue);
|
||||
break;
|
||||
}
|
||||
return newMode;
|
||||
}
|
||||
// remains in current mode
|
||||
return mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// stop listening when we go in background
|
||||
if (fingerPrintHelper != null) {
|
||||
fingerPrintHelper.stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAvailability() {
|
||||
// fingerprint not supported (by API level or hardware) so keep option hidden
|
||||
if (!fingerPrintHelper.isFingerprintSupported()) {
|
||||
|
||||
fingerprintView.setVisibility(View.GONE);
|
||||
// since this is a fingerprint example inform user, don't do this in your app
|
||||
//confirmationView.setText(R.string.fingerprint_not_supported);
|
||||
confirmationView.setVisibility(View.GONE);
|
||||
}
|
||||
// fingerprint is available but not configured show icon but in disabled state with some information
|
||||
else if (!fingerPrintHelper.hasEnrolledFingerprints()) {
|
||||
|
||||
fingerprintView.setVisibility(View.VISIBLE);
|
||||
confirmationView.setVisibility(View.VISIBLE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
fingerprintView.setAlpha(0.3f);
|
||||
}
|
||||
// This happens when no fingerprints are registered. Listening won't start
|
||||
confirmationView.setText(R.string.configure_fingerprint);
|
||||
}
|
||||
// finally fingerprint available and configured so we can use it
|
||||
else {
|
||||
|
||||
fingerprintView.setVisibility(View.VISIBLE);
|
||||
confirmationView.setVisibility(View.VISIBLE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
fingerprintView.setAlpha(1f);
|
||||
}
|
||||
// fingerprint available but no stored password found yet for this DB so show info don't listen
|
||||
if (prefs.getString(getPreferenceKeyValue(), null) == null) {
|
||||
|
||||
confirmationView.setText(R.string.no_password_stored);
|
||||
}
|
||||
// all is set here so we can confirm to user and start listening for fingerprints
|
||||
else {
|
||||
|
||||
confirmationView.setText(R.string.scanning_fingerprint);
|
||||
// listen for decryption by default
|
||||
toggleMode(Cipher.DECRYPT_MODE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleEncryptedResult(
|
||||
final String value,
|
||||
final String ivSpec) {
|
||||
|
||||
prefs.edit()
|
||||
.putString(getPreferenceKeyValue(), value)
|
||||
.putString(getPreferenceKeyIvSpec(), ivSpec)
|
||||
.commit();
|
||||
// and remove visual input to reset UI
|
||||
passwordView.setText("");
|
||||
confirmationView.setText(R.string.encrypted_value_stored);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDecryptedResult(final String value) {
|
||||
// on decrypt enter it for the purchase/login action
|
||||
passwordView.setText(value);
|
||||
confirmButton.performClick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidKeyException() {
|
||||
Toast.makeText(this, R.string.fingerprint_invalid_key, Toast.LENGTH_SHORT).show();
|
||||
checkAvailability(); // restarts listening
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException() {
|
||||
Toast.makeText(this, R.string.fingerprint_error, Toast.LENGTH_SHORT).show();
|
||||
checkAvailability(); // restarts listening
|
||||
}
|
||||
|
||||
private class DefaultCheckChange implements CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView,
|
||||
public void onCheckedChanged(
|
||||
CompoundButton buttonView,
|
||||
boolean isChecked) {
|
||||
|
||||
String newDefaultFileName;
|
||||
@@ -262,13 +493,16 @@ public class PasswordActivity extends LockingActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void loadDatabase(String pass, String keyfile) {
|
||||
private void loadDatabase(
|
||||
String pass,
|
||||
String keyfile) {
|
||||
loadDatabase(pass, UriUtil.parseDefaultFile(keyfile));
|
||||
}
|
||||
|
||||
private void loadDatabase(String pass, Uri keyfile)
|
||||
{
|
||||
if ( pass.length() == 0 && (keyfile == null || keyfile.toString().length() == 0)) {
|
||||
private void loadDatabase(
|
||||
String pass,
|
||||
Uri keyfile) {
|
||||
if (pass.length() == 0 && (keyfile == null || keyfile.toString().length() == 0)) {
|
||||
errorMessage(R.string.error_nopass);
|
||||
return;
|
||||
}
|
||||
@@ -290,9 +524,11 @@ public class PasswordActivity extends LockingActivity {
|
||||
return Util.getEditText(this, resId);
|
||||
}
|
||||
|
||||
private void setEditText(int resId, String str) {
|
||||
private void setEditText(
|
||||
int resId,
|
||||
String str) {
|
||||
TextView te = (TextView) findViewById(resId);
|
||||
assert(te == null);
|
||||
assert (te == null);
|
||||
|
||||
if (te != null) {
|
||||
te.setText(str);
|
||||
@@ -311,7 +547,7 @@ public class PasswordActivity extends LockingActivity {
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch ( item.getItemId() ) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_about:
|
||||
AboutDialog dialog = new AboutDialog(this);
|
||||
dialog.show();
|
||||
@@ -326,9 +562,12 @@ public class PasswordActivity extends LockingActivity {
|
||||
}
|
||||
|
||||
private final class AfterLoad extends OnFinish {
|
||||
|
||||
private Database db;
|
||||
|
||||
public AfterLoad(Handler handler, Database db) {
|
||||
public AfterLoad(
|
||||
Handler handler,
|
||||
Database db) {
|
||||
super(handler);
|
||||
|
||||
this.db = db;
|
||||
@@ -336,17 +575,19 @@ public class PasswordActivity extends LockingActivity {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if ( db.passwordEncodingError) {
|
||||
if (db.passwordEncodingError) {
|
||||
PasswordEncodingDialogHelper dialog = new PasswordEncodingDialogHelper();
|
||||
dialog.show(PasswordActivity.this, new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
public void onClick(
|
||||
DialogInterface dialog,
|
||||
int which) {
|
||||
GroupActivity.Launch(PasswordActivity.this);
|
||||
}
|
||||
|
||||
});
|
||||
} else if ( mSuccess ) {
|
||||
} else if (mSuccess) {
|
||||
GroupActivity.Launch(PasswordActivity.this);
|
||||
} else {
|
||||
displayMessage(PasswordActivity.this);
|
||||
@@ -355,14 +596,16 @@ public class PasswordActivity extends LockingActivity {
|
||||
}
|
||||
|
||||
private class InitTask extends AsyncTask<Intent, Void, Integer> {
|
||||
|
||||
String password = "";
|
||||
boolean launch_immediately = false;
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground(Intent... args) {
|
||||
Intent i = args[0];
|
||||
String action = i.getAction();;
|
||||
if ( action != null && action.equals(VIEW_INTENT) ) {
|
||||
String action = i.getAction();
|
||||
;
|
||||
if (action != null && action.equals(VIEW_INTENT)) {
|
||||
Uri incoming = i.getData();
|
||||
mDbUri = incoming;
|
||||
|
||||
@@ -370,8 +613,7 @@ public class PasswordActivity extends LockingActivity {
|
||||
|
||||
if (incoming == null) {
|
||||
return R.string.error_can_not_handle_uri;
|
||||
}
|
||||
else if (incoming.getScheme().equals("file")) {
|
||||
} else if (incoming.getScheme().equals("file")) {
|
||||
String fileName = incoming.getPath();
|
||||
|
||||
if (fileName.length() == 0) {
|
||||
@@ -385,14 +627,14 @@ public class PasswordActivity extends LockingActivity {
|
||||
return R.string.FileNotFound;
|
||||
}
|
||||
|
||||
if(mKeyUri == null)
|
||||
if (mKeyUri == null) {
|
||||
mKeyUri = getKeyFile(mDbUri);
|
||||
}
|
||||
else if (incoming.getScheme().equals("content")) {
|
||||
if(mKeyUri == null)
|
||||
} else if (incoming.getScheme().equals("content")) {
|
||||
if (mKeyUri == null) {
|
||||
mKeyUri = getKeyFile(mDbUri);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return R.string.error_can_not_handle_uri;
|
||||
}
|
||||
password = i.getStringExtra(KEY_PASSWORD);
|
||||
@@ -404,7 +646,7 @@ public class PasswordActivity extends LockingActivity {
|
||||
password = i.getStringExtra(KEY_PASSWORD);
|
||||
launch_immediately = i.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false);
|
||||
|
||||
if ( mKeyUri == null || mKeyUri.toString().length() == 0) {
|
||||
if (mKeyUri == null || mKeyUri.toString().length() == 0) {
|
||||
mKeyUri = getKeyFile(mDbUri);
|
||||
}
|
||||
}
|
||||
@@ -412,7 +654,7 @@ public class PasswordActivity extends LockingActivity {
|
||||
}
|
||||
|
||||
public void onPostExecute(Integer result) {
|
||||
if(result != null) {
|
||||
if (result != null) {
|
||||
Toast.makeText(PasswordActivity.this, result, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
@@ -420,18 +662,18 @@ public class PasswordActivity extends LockingActivity {
|
||||
|
||||
populateView();
|
||||
|
||||
Button confirmButton = (Button) findViewById(R.id.pass_ok);
|
||||
confirmButton.setOnClickListener(new OkClickHandler());
|
||||
|
||||
CheckBox checkBox = (CheckBox) findViewById(R.id.show_password);
|
||||
// Show or hide password
|
||||
checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
|
||||
|
||||
public void onCheckedChanged(CompoundButton buttonView,
|
||||
public void onCheckedChanged(
|
||||
CompoundButton buttonView,
|
||||
boolean isChecked) {
|
||||
TextView password = (TextView) findViewById(R.id.password);
|
||||
|
||||
if ( isChecked ) {
|
||||
if (isChecked) {
|
||||
password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
|
||||
} else {
|
||||
password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
@@ -457,8 +699,7 @@ public class PasswordActivity extends LockingActivity {
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
i.setType("*/*");
|
||||
startActivityForResult(i, OPEN_DOC);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
i.setType("*/*");
|
||||
@@ -508,8 +749,9 @@ public class PasswordActivity extends LockingActivity {
|
||||
|
||||
retrieveSettings();
|
||||
|
||||
if (launch_immediately)
|
||||
if (launch_immediately) {
|
||||
loadDatabase(password, mKeyUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,16 +19,21 @@
|
||||
*/
|
||||
package com.keepassdroid.fileselect;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.Menu;
|
||||
@@ -74,6 +79,7 @@ import java.net.URLDecoder;
|
||||
|
||||
public class FileSelectActivity extends Activity {
|
||||
|
||||
private static final int MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE = 111;
|
||||
private ListView mList;
|
||||
private ListAdapter mAdapter;
|
||||
|
||||
@@ -218,7 +224,12 @@ public class FileSelectActivity extends Activity {
|
||||
startActivityForResult(i, OPEN_DOC);
|
||||
}
|
||||
else {
|
||||
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
Intent i;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
i = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
} else {
|
||||
i = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
}
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
i.setType("*/*");
|
||||
|
||||
@@ -409,6 +420,9 @@ public class FileSelectActivity extends Activity {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// check for storage permission
|
||||
checkStoragePermission();
|
||||
|
||||
// Check to see if we need to change modes
|
||||
if ( fileHistory.hasRecentFiles() != recentMode ) {
|
||||
// Restart the activity
|
||||
@@ -421,6 +435,60 @@ public class FileSelectActivity extends Activity {
|
||||
fnv.updateExternalStorageWarning();
|
||||
}
|
||||
|
||||
private void checkStoragePermission() {
|
||||
// Here, thisActivity is the current activity
|
||||
if (ContextCompat.checkSelfPermission(FileSelectActivity.this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
// Should we show an explanation?
|
||||
//if (ActivityCompat.shouldShowRequestPermissionRationale(FileSelectActivity.this,
|
||||
// Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
|
||||
// Show an explanation to the user *asynchronously* -- don't block
|
||||
// this thread waiting for the user's response! After the user
|
||||
// sees the explanation, try again to request the permission.
|
||||
|
||||
//} else {
|
||||
|
||||
// No explanation needed, we can request the permission.
|
||||
|
||||
ActivityCompat.requestPermissions(FileSelectActivity.this,
|
||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE);
|
||||
|
||||
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
|
||||
// app-defined int constant. The callback method gets the
|
||||
// result of the request.
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode,
|
||||
String permissions[], int[] grantResults) {
|
||||
switch (requestCode) {
|
||||
case MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE: {
|
||||
// If request is cancelled, the result arrays are empty.
|
||||
if (grantResults.length > 0
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
// permission was granted, yay! Do the
|
||||
// contacts-related task you need to do.
|
||||
|
||||
} else {
|
||||
|
||||
// permission denied, boo! Disable the
|
||||
// functionality that depends on this permission.
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// other 'case' lines to check for other
|
||||
// permissions this app might request
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.keepassdroid.fingerprint;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.os.CancellationSignal;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import android.util.Base64;
|
||||
|
||||
import java.security.KeyStore;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
public class FingerPrintHelper {
|
||||
|
||||
private static final String ALIAS_KEY = "example-key";
|
||||
|
||||
private FingerprintManager fingerprintManager;
|
||||
private KeyStore keyStore = null;
|
||||
private KeyGenerator keyGenerator = null;
|
||||
private Cipher cipher = null;
|
||||
private KeyguardManager keyguardManager = null;
|
||||
private FingerprintManager.CryptoObject cryptoObject = null;
|
||||
|
||||
private boolean initOk = false;
|
||||
private FingerPrintCallback fingerPrintCallback;
|
||||
private CancellationSignal cancellationSignal;
|
||||
private FingerprintManager.AuthenticationCallback authenticationCallback;
|
||||
|
||||
public void setAuthenticationCallback(final FingerprintManager.AuthenticationCallback authenticationCallback) {
|
||||
this.authenticationCallback = authenticationCallback;
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public void startListening() {
|
||||
// no need to start listening when not initialised
|
||||
if (!isFingerprintInitialized()) {
|
||||
if (fingerPrintCallback != null) {
|
||||
fingerPrintCallback.onException();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// starts listening for fingerprints with the initialised crypto object
|
||||
cancellationSignal = new CancellationSignal();
|
||||
fingerprintManager.authenticate(
|
||||
cryptoObject,
|
||||
cancellationSignal,
|
||||
0 /* flags */,
|
||||
authenticationCallback,
|
||||
null);
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public void stopListening() {
|
||||
if (!isFingerprintInitialized()) {
|
||||
return;
|
||||
}
|
||||
if (cancellationSignal != null) {
|
||||
cancellationSignal.cancel();
|
||||
cancellationSignal = null;
|
||||
}
|
||||
}
|
||||
|
||||
public interface FingerPrintCallback {
|
||||
|
||||
void handleEncryptedResult(String value, String ivSpec);
|
||||
|
||||
void handleDecryptedResult(String value);
|
||||
|
||||
void onInvalidKeyException();
|
||||
|
||||
void onException();
|
||||
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public FingerPrintHelper(
|
||||
final Context context,
|
||||
final FingerPrintCallback fingerPrintCallback) {
|
||||
|
||||
if (!isFingerprintSupported()) {
|
||||
// really not much to do when no fingerprint support found
|
||||
setInitOk(false);
|
||||
return;
|
||||
}
|
||||
this.fingerprintManager = context.getSystemService(FingerprintManager.class);
|
||||
this.keyguardManager = context.getSystemService(KeyguardManager.class);
|
||||
this.fingerPrintCallback = fingerPrintCallback;
|
||||
|
||||
if (hasEnrolledFingerprints()) {
|
||||
try {
|
||||
this.keyStore = KeyStore.getInstance("AndroidKeyStore");
|
||||
this.keyGenerator = KeyGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_AES,
|
||||
"AndroidKeyStore");
|
||||
this.cipher = Cipher.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_AES + "/"
|
||||
+ KeyProperties.BLOCK_MODE_CBC + "/"
|
||||
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
|
||||
this.cryptoObject = new FingerprintManager.CryptoObject(cipher);
|
||||
setInitOk(true);
|
||||
} catch (final Exception e) {
|
||||
setInitOk(false);
|
||||
fingerPrintCallback.onException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isFingerprintInitialized() {
|
||||
return hasEnrolledFingerprints() && initOk;
|
||||
}
|
||||
|
||||
@SuppressWarnings("NewApi")
|
||||
public void initEncryptData() {
|
||||
|
||||
if (!isFingerprintInitialized()) {
|
||||
if (fingerPrintCallback != null) {
|
||||
fingerPrintCallback.onException();
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
createNewKeyIfNeeded(false); // no need to keep deleting existing keys
|
||||
keyStore.load(null);
|
||||
final SecretKey key = (SecretKey) keyStore.getKey(ALIAS_KEY, null);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
|
||||
stopListening();
|
||||
startListening();
|
||||
|
||||
} catch (final KeyPermanentlyInvalidatedException invalidKeyException) {
|
||||
fingerPrintCallback.onInvalidKeyException();
|
||||
} catch (final Exception e) {
|
||||
fingerPrintCallback.onException();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("NewApi")
|
||||
public void encryptData(final String value) {
|
||||
|
||||
if (!isFingerprintInitialized()) {
|
||||
if (fingerPrintCallback != null) {
|
||||
fingerPrintCallback.onException();
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// actual do encryption here
|
||||
byte[] encrypted = cipher.doFinal(value.getBytes());
|
||||
final String encryptedValue = Base64.encodeToString(encrypted, 0 /* flags */);
|
||||
|
||||
// passes updated iv spec on to callback so this can be stored for decryption
|
||||
final IvParameterSpec spec = cipher.getParameters().getParameterSpec(IvParameterSpec.class);
|
||||
final String ivSpecValue = Base64.encodeToString(spec.getIV(), Base64.DEFAULT);
|
||||
fingerPrintCallback.handleEncryptedResult(encryptedValue, ivSpecValue);
|
||||
|
||||
} catch (final Exception e) {
|
||||
fingerPrintCallback.onException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("NewApi")
|
||||
public void initDecryptData(final String ivSpecValue) {
|
||||
|
||||
if (!isFingerprintInitialized()) {
|
||||
if (fingerPrintCallback != null) {
|
||||
fingerPrintCallback.onException();
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
createNewKeyIfNeeded(false);
|
||||
keyStore.load(null);
|
||||
final SecretKey key = (SecretKey) keyStore.getKey(ALIAS_KEY, null);
|
||||
|
||||
// important to restore spec here that was used for decryption
|
||||
final byte[] iv = Base64.decode(ivSpecValue, Base64.DEFAULT);
|
||||
final IvParameterSpec spec = new IvParameterSpec(iv);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||
|
||||
stopListening();
|
||||
startListening();
|
||||
|
||||
} catch (final KeyPermanentlyInvalidatedException invalidKeyException) {
|
||||
fingerPrintCallback.onInvalidKeyException();
|
||||
} catch (final Exception e) {
|
||||
fingerPrintCallback.onException();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("NewApi")
|
||||
public void decryptData(final String encryptedValue) {
|
||||
|
||||
if (!isFingerprintInitialized()) {
|
||||
if (fingerPrintCallback != null) {
|
||||
fingerPrintCallback.onException();
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// actual decryption here
|
||||
final byte[] encrypted = Base64.decode(encryptedValue, 0);
|
||||
byte[] decrypted = cipher.doFinal(encrypted);
|
||||
final String decryptedString = new String(decrypted);
|
||||
|
||||
//final String encryptedString = Base64.encodeToString(encrypted, 0 /* flags */);
|
||||
fingerPrintCallback.handleDecryptedResult(decryptedString);
|
||||
|
||||
} catch (final Exception e) {
|
||||
fingerPrintCallback.onException();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private void createNewKeyIfNeeded(final boolean allowDeleteExisting) {
|
||||
if (!isFingerprintInitialized()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
keyStore.load(null);
|
||||
if (allowDeleteExisting
|
||||
&& keyStore.containsAlias(ALIAS_KEY)) {
|
||||
|
||||
keyStore.deleteEntry(ALIAS_KEY);
|
||||
}
|
||||
|
||||
// Create new key if needed
|
||||
if (!keyStore.containsAlias(ALIAS_KEY)) {
|
||||
// Set the alias of the entry in Android KeyStore where the key will appear
|
||||
// and the constrains (purposes) in the constructor of the Builder
|
||||
keyGenerator.init(
|
||||
new KeyGenParameterSpec.Builder(
|
||||
ALIAS_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT |
|
||||
KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
// Require the user to authenticate with a fingerprint to authorize every use
|
||||
// of the key
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
.build());
|
||||
keyGenerator.generateKey();
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
fingerPrintCallback.onException();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public boolean isHardwareDetected() {
|
||||
return isFingerprintSupported()
|
||||
&& fingerprintManager != null
|
||||
&& fingerprintManager.isHardwareDetected();
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public boolean hasEnrolledFingerprints() {
|
||||
// fingerprint hardware supported and api level OK
|
||||
return isHardwareDetected()
|
||||
// fingerprints enrolled
|
||||
&& fingerprintManager != null
|
||||
&& fingerprintManager.hasEnrolledFingerprints()
|
||||
// and lockscreen configured
|
||||
&& keyguardManager.isKeyguardSecure();
|
||||
}
|
||||
|
||||
void setInitOk(final boolean initOk) {
|
||||
this.initOk = initOk;
|
||||
}
|
||||
|
||||
public boolean isFingerprintSupported() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
|
||||
}
|
||||
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_fp_40px.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_fp_40px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_fp_40px.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_fp_40px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_fp_40px.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_fp_40px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -19,7 +19,8 @@
|
||||
-->
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
android:layout_height="fill_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<TextView android:id="@+id/filename_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -57,20 +58,45 @@
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/password_label"
|
||||
android:layout_toLeftOf="@+id/fingerprint"
|
||||
android:singleLine="true"
|
||||
android:inputType="textPassword"
|
||||
android:hint="@string/hint_login_pass"/>
|
||||
|
||||
<!-- added these 2 fingerprint related views -->
|
||||
<ImageView
|
||||
android:id="@+id/fingerprint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/margin_small"
|
||||
android:layout_marginRight="@dimen/margin_small"
|
||||
android:layout_below="@id/password_label"
|
||||
android:layout_alignParentRight="true"
|
||||
android:src="@drawable/ic_fp_40px"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
/>
|
||||
<TextView
|
||||
android:id="@+id/fingerprint_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/fingerprint"
|
||||
android:text="@string/entry_and_or"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
/>
|
||||
|
||||
<ImageButton android:id="@+id/browse_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_launcher_folder_small"
|
||||
android:layout_below="@id/password"
|
||||
android:layout_below="@id/fingerprint_label"
|
||||
android:layout_alignParentRight="true"
|
||||
/>
|
||||
<EditText android:id="@+id/pass_keyfile"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/password"
|
||||
android:layout_below="@id/fingerprint_label"
|
||||
android:layout_toLeftOf="@id/browse_button"
|
||||
android:singleLine="true"
|
||||
android:hint="@string/entry_keyfile"/>
|
||||
|
||||
8
app/src/main/res/values/dimens.xml
Normal file
8
app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="margin_tiny">4dp</dimen>
|
||||
<dimen name="margin_small">8dp</dimen>
|
||||
<dimen name="margin_medium">16dp</dimen>
|
||||
<dimen name="margin_large">32dp</dimen>
|
||||
<dimen name="margin_huge">64dp</dimen>
|
||||
</resources>
|
||||
@@ -194,6 +194,13 @@
|
||||
<string name="warning_read_only">Your sd card is currently read-only. You may not be able to save changes to your database.</string>
|
||||
<string name="warning_unmounted">Your sd card is not currently mounted on your device. You will not be able to load or create your database.</string>
|
||||
<string name="version_label">Version:</string>
|
||||
<string name="configure_fingerprint">Fingerprint supported but not configured for device</string>
|
||||
<string name="scanning_fingerprint">Listening for fingerprints</string>
|
||||
<string name="encrypted_value_stored">Encrypted password stored</string>
|
||||
<string name="fingerprint_invalid_key">Invalid Key problem</string>
|
||||
<string name="fingerprint_error">Fingerprint problem</string>
|
||||
<string name="store_with_fingerprint">Use fingerprint to store this password</string>
|
||||
<string name="no_password_stored">No password stored yet for this database</string>
|
||||
|
||||
<string-array name="clipboard_timeout_options">
|
||||
<item>30 seconds</item>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.google.com/'
|
||||
name 'Google'
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.0'
|
||||
@@ -11,5 +15,9 @@ buildscript {
|
||||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://maven.google.com/'
|
||||
name 'Google'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user