mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
184 Commits
3.0.0_beta
...
3.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
045049243c | ||
|
|
b9813a3494 | ||
|
|
9b42a93ce1 | ||
|
|
8502bceef1 | ||
|
|
663387476f | ||
|
|
daafd83df9 | ||
|
|
f780f2725b | ||
|
|
483aca871a | ||
|
|
352e709c3b | ||
|
|
629057b2c1 | ||
|
|
0e5f53596d | ||
|
|
0d91f07646 | ||
|
|
db882a26ab | ||
|
|
7f01619358 | ||
|
|
ee109b4ceb | ||
|
|
7a398e5453 | ||
|
|
d4655d7034 | ||
|
|
9feb96b541 | ||
|
|
e939278193 | ||
|
|
d4ef1a2617 | ||
|
|
5f8746ced3 | ||
|
|
40a063e94f | ||
|
|
8f5439b958 | ||
|
|
e347f57d8b | ||
|
|
2efb8e8b8c | ||
|
|
e5bb69ea5f | ||
|
|
6ae186b2af | ||
|
|
71fdd2d92d | ||
|
|
3656689ff3 | ||
|
|
7d78406db6 | ||
|
|
ac47748e41 | ||
|
|
80f9b46479 | ||
|
|
999f1bf47a | ||
|
|
9e114eb2b8 | ||
|
|
d89b6529ef | ||
|
|
5caf11556a | ||
|
|
78cc6f0f40 | ||
|
|
0007cd4668 | ||
|
|
05195e41de | ||
|
|
66f44ef87d | ||
|
|
a0585d9b11 | ||
|
|
5067946b13 | ||
|
|
f52241d5a8 | ||
|
|
04ccb25fa3 | ||
|
|
5a3f4b60b8 | ||
|
|
4408b2e488 | ||
|
|
9a26acee35 | ||
|
|
6ac377348b | ||
|
|
daeb88d4f4 | ||
|
|
47bf199f52 | ||
|
|
91540b022d | ||
|
|
505a51b6b5 | ||
|
|
28400488aa | ||
|
|
45ae600289 | ||
|
|
8be6874651 | ||
|
|
f694a500e0 | ||
|
|
c415fa01fc | ||
|
|
5225a9459c | ||
|
|
2974b150af | ||
|
|
cf353c8067 | ||
|
|
3aacb5d8b3 | ||
|
|
114fbdbe01 | ||
|
|
f1f83cbec4 | ||
|
|
335c28c2c9 | ||
|
|
daf12cbcce | ||
|
|
bf56eca003 | ||
|
|
a12b7fd58a | ||
|
|
98d004edbf | ||
|
|
67586a98b3 | ||
|
|
c6d8911883 | ||
|
|
12e398ce9b | ||
|
|
5c4a202616 | ||
|
|
adc1ec8444 | ||
|
|
0227d2fcb4 | ||
|
|
95abe3b5ac | ||
|
|
133a902c54 | ||
|
|
aacb03d9ef | ||
|
|
fbeaa1781f | ||
|
|
ca73aad538 | ||
|
|
4a4bfefd17 | ||
|
|
6796b0cd2a | ||
|
|
29e1f824b0 | ||
|
|
51263a2911 | ||
|
|
dd7f857475 | ||
|
|
f0fdd4a537 | ||
|
|
9b847a0561 | ||
|
|
469e76b80a | ||
|
|
9c6994b476 | ||
|
|
52ba487617 | ||
|
|
21c57c9484 | ||
|
|
9b1ea6a07a | ||
|
|
f7d7bb0ea3 | ||
|
|
dd96b9ef53 | ||
|
|
b6b01893ba | ||
|
|
ad531d793d | ||
|
|
a9dd11e24a | ||
|
|
115983830b | ||
|
|
442cece081 | ||
|
|
14e08457b9 | ||
|
|
fabcc08cd5 | ||
|
|
7750843b04 | ||
|
|
c03188e976 | ||
|
|
d7da1ce333 | ||
|
|
dd9ee8c3f8 | ||
|
|
34bbd8f439 | ||
|
|
053f57cff5 | ||
|
|
365d2e2844 | ||
|
|
c0ac01a34a | ||
|
|
22c0bc0adb | ||
|
|
1b88f2ddf0 | ||
|
|
b4f2a1eb89 | ||
|
|
e9fc9cbc2a | ||
|
|
b809180a1b | ||
|
|
ecc75df3a1 | ||
|
|
d1b6863143 | ||
|
|
faf27143aa | ||
|
|
aee58a4475 | ||
|
|
c4cbf07d78 | ||
|
|
c5e07f643f | ||
|
|
818f5820d5 | ||
|
|
77c6e28876 | ||
|
|
fb7f66012d | ||
|
|
0f7f7bbe6c | ||
|
|
8dedb8deb4 | ||
|
|
dbb2c10bba | ||
|
|
67a5eef7d6 | ||
|
|
979f651251 | ||
|
|
3b93cbb009 | ||
|
|
30c63bfc4b | ||
|
|
bed40324a1 | ||
|
|
bc035de377 | ||
|
|
4db3cb6936 | ||
|
|
ed4b91f4bd | ||
|
|
24c7151276 | ||
|
|
804a9c07b8 | ||
|
|
528ea56821 | ||
|
|
7ba9c69ff8 | ||
|
|
0fc34da08a | ||
|
|
3fbf8cdbc8 | ||
|
|
e21f20d818 | ||
|
|
9fd9a60ca3 | ||
|
|
78b683d724 | ||
|
|
ad2f5036e1 | ||
|
|
4afbad8faa | ||
|
|
d284db4d3c | ||
|
|
82450c0ae8 | ||
|
|
8dd6c33901 | ||
|
|
f920d40db5 | ||
|
|
19be6c1acc | ||
|
|
7d9d8ad0e4 | ||
|
|
85f8237d5f | ||
|
|
c542894734 | ||
|
|
d348987077 | ||
|
|
3718610595 | ||
|
|
9c36ec0623 | ||
|
|
c6917b5d74 | ||
|
|
ae8b1c0c29 | ||
|
|
27978c459c | ||
|
|
1dc7f5c666 | ||
|
|
12ac870d3a | ||
|
|
dd1baa0224 | ||
|
|
4eaa179789 | ||
|
|
9008cd4549 | ||
|
|
bb27ef41cc | ||
|
|
2d35ac1df8 | ||
|
|
589ffc0c06 | ||
|
|
1f7f38c7d3 | ||
|
|
83817a2dc0 | ||
|
|
11af9da66f | ||
|
|
af3926acf3 | ||
|
|
ab40c2b3fd | ||
|
|
fd05670dbc | ||
|
|
1ac094bfae | ||
|
|
fdf052cddb | ||
|
|
9a8d50ba6f | ||
|
|
136c97c312 | ||
|
|
bf00b88ef3 | ||
|
|
bafd1ea549 | ||
|
|
982618511b | ||
|
|
a4ad7ca3b1 | ||
|
|
99d71b57a4 | ||
|
|
1b2d8502e0 | ||
|
|
53e4ea9334 | ||
|
|
3ce704155c |
27
CHANGELOG
27
CHANGELOG
@@ -1,3 +1,28 @@
|
||||
KeePassDX(3.0.4)
|
||||
* Fix autofill inline bugs #1173 #1165
|
||||
* Small UI change
|
||||
|
||||
KeePassDX(3.0.3)
|
||||
* Change default Argon2 parameters #1098
|
||||
* Add & edit custom icon name #976
|
||||
* Fix templates #1128 #1133 #1138
|
||||
* Update Autofill compatibility list #725 #1154
|
||||
* Improve fingerprint usage #1137 #1145
|
||||
* Change backup configuration #1144
|
||||
* Add lock button in database notification
|
||||
|
||||
KeePassDX(3.0.2)
|
||||
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
|
||||
|
||||
KeePassDX(3.0.1)
|
||||
* Fix text size and smallest margin #1085
|
||||
* Fix number of lines during an edition #1073
|
||||
* Fix Magikeyboard URL auto action #1100
|
||||
* Fix exception after group name change and save #1112
|
||||
* Fix timeout reset #1107
|
||||
* Fix search actions #1091 #1092
|
||||
* Small changes #1106 #1085
|
||||
|
||||
KeePassDX(3.0.0)
|
||||
* Add / Manage dynamic templates #191
|
||||
* Manually select RecycleBin group and Templates group #191
|
||||
@@ -5,7 +30,7 @@ KeePassDX(3.0.0)
|
||||
* Fix timeout in dialogs #716
|
||||
* Check URI permissions #626
|
||||
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
|
||||
* Improvements #680 #1035 #1043 #942 #1021 #1027
|
||||
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
|
||||
|
||||
KeePassDX(2.10.5)
|
||||
* Increase the saving speed of database #1028
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- Material design with **themes**.
|
||||
- **Auto-Fill** and Integration.
|
||||
- Field filling **keyboard**.
|
||||
- Dynamic **templates**
|
||||
- **History** of each entry.
|
||||
- Precise management of **settings**.
|
||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 30
|
||||
versionCode = 86
|
||||
versionName = "3.0.0_beta03"
|
||||
versionCode = 91
|
||||
versionName = "3.0.4"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -99,33 +99,33 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
def room_version = "2.2.6"
|
||||
def room_version = "2.3.0"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.biometric:biometric:1.1.0'
|
||||
implementation 'androidx.media:media:1.4.3'
|
||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||
implementation "androidx.core:core-ktx:1.3.2"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||
// WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation "androidx.core:core-ktx:$android_core_version"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
||||
implementation "com.google.android.material:material:$android_material_version"
|
||||
// Database
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
// Autofill
|
||||
implementation "androidx.autofill:autofill:1.1.0"
|
||||
// Time
|
||||
implementation 'joda-time:joda-time:2.10.6'
|
||||
implementation 'joda-time:joda-time:2.10.13'
|
||||
// Color
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
||||
// Education
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
|
||||
// Apache Commons
|
||||
implementation 'commons-io:commons-io:2.8.0'
|
||||
implementation 'commons-codec:commons-codec:1.15'
|
||||
@@ -136,6 +136,6 @@ dependencies {
|
||||
implementation project(path: ':icon-pack-material')
|
||||
|
||||
// Tests
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||
androidTestImplementation "androidx.test:rules:$android_test_version"
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:exported="true"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
||||
<intent-filter>
|
||||
@@ -53,6 +54,7 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
||||
<intent-filter>
|
||||
@@ -111,6 +113,7 @@
|
||||
<!-- Main Activity -->
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||
android:exported="false"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
<meta-data
|
||||
@@ -154,7 +157,8 @@
|
||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent">
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -173,7 +177,8 @@
|
||||
android:theme="@style/Theme.Transparent" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||
android:label="@string/keyboard_setting_label">
|
||||
android:label="@string/keyboard_setting_label"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
</intent-filter>
|
||||
@@ -199,6 +204,7 @@
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
||||
android:label="@string/autofill_service_name"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||
<meta-data
|
||||
android:name="android.autofill"
|
||||
@@ -210,6 +216,7 @@
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
||||
android:label="@string/keyboard_label"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||
<meta-data android:name="android.view.im"
|
||||
android:resource="@xml/keyboard_method"/>
|
||||
@@ -221,6 +228,14 @@
|
||||
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.app.action.ENTER_KNOX_DESKTOP_MODE" />
|
||||
<action android:name="android.app.action.EXIT_KNOX_DESKTOP_MODE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
</application>
|
||||
|
||||
@@ -23,28 +23,33 @@ import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this, true)
|
||||
else null
|
||||
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
}
|
||||
@@ -60,17 +65,37 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||
when (specialMode) {
|
||||
SpecialMode.SELECTION -> {
|
||||
// Build search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
||||
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false)
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchSelection(database, searchInfo)
|
||||
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
|
||||
// To pass extra inline request
|
||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION)
|
||||
}
|
||||
// Build search param
|
||||
bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||
SearchInfo.getConcreteWebDomain(
|
||||
this,
|
||||
searchInfo.webDomain
|
||||
) { concreteWebDomain ->
|
||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||
val assistStructure = AutofillHelper
|
||||
.retrieveAutofillComponent(intent)
|
||||
?.assistStructure
|
||||
val newAutofillComponent = if (assistStructure != null) {
|
||||
AutofillComponent(
|
||||
assistStructure,
|
||||
compatInlineSuggestionsRequest
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchSelection(database, newAutofillComponent, searchInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove bundle
|
||||
intent.removeExtra(KEY_SELECTION_BUNDLE)
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
// To register info
|
||||
@@ -91,10 +116,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
|
||||
private fun launchSelection(database: Database?,
|
||||
autofillComponent: AutofillComponent?,
|
||||
searchInfo: SearchInfo) {
|
||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
||||
|
||||
if (autofillComponent == null) {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
@@ -119,6 +142,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForAutofillResult(this,
|
||||
openedDatabase,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false)
|
||||
@@ -126,6 +150,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
@@ -186,55 +211,49 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
|
||||
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
||||
// Close the database
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION"
|
||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
||||
|
||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||
|
||||
fun getPendingIntentForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent {
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
// Doesn't work with Parcelable (don't know why?)
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
searchInfo?.let {
|
||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
||||
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
|
||||
}
|
||||
// Doesn't work with direct extra Parcelable (don't know why?)
|
||||
// Wrap into a bundle to bypass the problem
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
||||
}
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
})
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// TODO Mutable
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
})
|
||||
}
|
||||
|
||||
fun getPendingIntentForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): PendingIntent {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// TODO Mutable
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
})
|
||||
}
|
||||
|
||||
fun launchForRegistration(context: Context,
|
||||
|
||||
@@ -32,6 +32,7 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
@@ -62,7 +63,6 @@ import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
@@ -84,8 +84,13 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
private var mEntryLoaded = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
private var mAttachmentSelected: Attachment? = null
|
||||
|
||||
private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) {
|
||||
// Reload the current id from database
|
||||
mEntryViewModel.loadDatabase(mDatabase)
|
||||
}
|
||||
|
||||
private var mIcon: IconImage? = null
|
||||
private var mIconColor: Int = 0
|
||||
@@ -133,6 +138,15 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
// Init SAF manager
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
||||
mAttachmentSelected?.let { attachment ->
|
||||
if (createdFileUri != null) {
|
||||
mAttachmentFileBinderManager
|
||||
?.startDownloadAttachment(createdFileUri, attachment)
|
||||
}
|
||||
mAttachmentSelected = null
|
||||
}
|
||||
}
|
||||
// Init attachment service binder manager
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
|
||||
@@ -209,9 +223,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
||||
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentSelected
|
||||
}
|
||||
mAttachmentSelected = attachmentSelected
|
||||
mExternalFileHelper?.createDocument(attachmentSelected.name)
|
||||
}
|
||||
|
||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||
@@ -220,7 +233,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
this,
|
||||
database,
|
||||
historySelected.nodeId,
|
||||
historySelected.historyPosition
|
||||
historySelected.historyPosition,
|
||||
mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -290,26 +304,6 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
// Reload the current id from database
|
||||
mEntryViewModel.loadDatabase(mDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||
if (createdFileUri != null) {
|
||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
||||
mAttachmentFileBinderManager
|
||||
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
if (mEntryLoaded) {
|
||||
@@ -391,7 +385,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
EntryEditActivity.launchToUpdate(
|
||||
this,
|
||||
database,
|
||||
entryId
|
||||
entryId,
|
||||
mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -432,7 +427,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
// Transit data in previous Activity after an update
|
||||
Intent().apply {
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||
setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this)
|
||||
setResult(Activity.RESULT_OK, this)
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
@@ -450,15 +445,13 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
database: Database,
|
||||
entryId: NodeId<UUID>) {
|
||||
entryId: NodeId<UUID>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
activity.startActivityForResult(
|
||||
intent,
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
||||
)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -469,16 +462,14 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
fun launch(activity: Activity,
|
||||
database: Database,
|
||||
entryId: NodeId<UUID>,
|
||||
historyPosition: Int) {
|
||||
historyPosition: Int,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||
activity.startActivityForResult(
|
||||
intent,
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
||||
)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,12 +33,17 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
@@ -96,6 +101,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
private var mTemplate: Template? = null
|
||||
private var mIsTemplate: Boolean = false
|
||||
private var mEntryLoaded: Boolean = false
|
||||
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
|
||||
|
||||
private var mAllowCustomFields = false
|
||||
private var mAllowOTP = false
|
||||
@@ -106,6 +112,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
// Education
|
||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||
|
||||
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
|
||||
mEntryEditViewModel.selectIcon(icon)
|
||||
}
|
||||
|
||||
// To ask data lost only one time
|
||||
private var backPressedAlreadyApproved = false
|
||||
|
||||
@@ -154,6 +164,21 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
|
||||
// To retrieve attachment
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let { attachmentToUploadUri ->
|
||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||
documentFile.name?.let { fileName ->
|
||||
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
||||
.show(supportFragmentManager, "fileTooBigFragment")
|
||||
} else {
|
||||
mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
// Verify the education views
|
||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||
@@ -175,11 +200,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
templateSelectorSpinner?.apply {
|
||||
// Build template selector
|
||||
if (templates.isNotEmpty()) {
|
||||
adapter = TemplatesSelectorAdapter(
|
||||
mTemplatesSelectorAdapter = TemplatesSelectorAdapter(
|
||||
this@EntryEditActivity,
|
||||
mIconDrawableFactory,
|
||||
templates
|
||||
)
|
||||
).apply {
|
||||
iconDrawableFactory = mIconDrawableFactory
|
||||
}
|
||||
adapter = mTemplatesSelectorAdapter
|
||||
val selectedTemplate = if (mTemplate != null)
|
||||
mTemplate
|
||||
else
|
||||
@@ -213,7 +240,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
|
||||
// View model listeners
|
||||
mEntryEditViewModel.requestIconSelection.observe(this) { iconImage ->
|
||||
IconPickerActivity.launch(this@EntryEditActivity, iconImage)
|
||||
IconPickerActivity.launch(this@EntryEditActivity, iconImage, mIconSelectionActivityResultLauncher)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
||||
@@ -321,6 +348,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
||||
mAllowOTP = database?.allowOTP == true
|
||||
mEntryEditViewModel.loadDatabase(database)
|
||||
mTemplatesSelectorAdapter?.apply {
|
||||
iconDrawableFactory = mIconDrawableFactory
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
@@ -472,29 +503,6 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
|
||||
mEntryEditViewModel.selectIcon(icon)
|
||||
}
|
||||
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
uri?.let { attachmentToUploadUri ->
|
||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||
documentFile.name?.let { fileName ->
|
||||
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
||||
.show(supportFragmentManager, "fileTooBigFragment")
|
||||
} else {
|
||||
mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up OTP (HOTP or TOTP) and add it as extra field
|
||||
*/
|
||||
@@ -585,7 +593,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||
attachmentView,
|
||||
{
|
||||
mExternalFileHelper?.openDocument()
|
||||
addNewAttachment()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
@@ -686,7 +694,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
val intentEntry = Intent()
|
||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
||||
intentEntry.putExtras(bundle)
|
||||
setResult(ADD_OR_UPDATE_ENTRY_RESULT_CODE, intentEntry)
|
||||
setResult(Activity.RESULT_OK, intentEntry)
|
||||
super.finish()
|
||||
} catch (e: Exception) {
|
||||
// Exception when parcelable can't be done
|
||||
@@ -701,23 +709,46 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
// Keys for current Activity
|
||||
const val KEY_ENTRY = "entry"
|
||||
const val KEY_PARENT = "parent"
|
||||
|
||||
// Keys for callback
|
||||
const val ADD_OR_UPDATE_ENTRY_RESULT_CODE = 31
|
||||
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
|
||||
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
||||
|
||||
fun registerForEntryResult(fragment: Fragment,
|
||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
||||
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
entryAddedOrUpdatedListener.invoke(
|
||||
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
)
|
||||
} else {
|
||||
entryAddedOrUpdatedListener.invoke(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun registerForEntryResult(activity: FragmentActivity,
|
||||
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
||||
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
entryAddedOrUpdatedListener.invoke(
|
||||
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
)
|
||||
} else {
|
||||
entryAddedOrUpdatedListener.invoke(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
||||
*/
|
||||
fun launchToUpdate(activity: Activity,
|
||||
database: Database,
|
||||
entryId: NodeId<UUID>) {
|
||||
entryId: NodeId<UUID>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -727,12 +758,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
*/
|
||||
fun launchToCreate(activity: Activity,
|
||||
database: Database,
|
||||
groupId: NodeId<*>) {
|
||||
groupId: NodeId<*>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,8 +827,9 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
* Launch EntryEditActivity to add a new entry in autofill selection
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
database: Database,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
groupId: NodeId<*>,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
@@ -807,6 +840,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo
|
||||
)
|
||||
|
||||
@@ -31,8 +31,10 @@ import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -85,9 +87,20 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this)
|
||||
else null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Enabling/disabling MagikeyboardService is normally done by DexModeReceiver, but this
|
||||
// additional check will allow the keyboard to be reenabled more easily if the app crashes
|
||||
// or is force quit within DeX mode and then the user leaves DeX mode. Without this, the
|
||||
// user would need to enter and exit DeX mode once to reenable the service.
|
||||
MagikeyboardUtil.setEnabled(this, !DexUtil.isDexMode(resources.configuration))
|
||||
|
||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
|
||||
setContentView(R.layout.activity_file_selection)
|
||||
@@ -103,6 +116,22 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
// Open database button
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
}
|
||||
}
|
||||
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
||||
mDatabaseFileUri = databaseFileCreatedUri
|
||||
if (mDatabaseFileUri != null) {
|
||||
AssignMasterKeyDialogFragment.getInstance(true)
|
||||
.show(supportFragmentManager, "passwordDialog")
|
||||
} else {
|
||||
val error = getString(R.string.error_create_database)
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error)
|
||||
}
|
||||
}
|
||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
@@ -250,8 +279,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
* Create a new file by calling the content provider
|
||||
*/
|
||||
private fun createNewFile() {
|
||||
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||
mExternalFileHelper?.createDocument(
|
||||
getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default))
|
||||
}
|
||||
|
||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||
@@ -268,7 +298,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
fileNoFoundAction(exception)
|
||||
},
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher)
|
||||
}
|
||||
|
||||
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||
@@ -277,7 +308,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
database,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,33 +385,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
if (uri != null) {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the created URI from the file manager
|
||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
||||
mDatabaseFileUri = databaseFileCreatedUri
|
||||
if (mDatabaseFileUri != null) {
|
||||
AssignMasterKeyDialogFragment.getInstance(true)
|
||||
.show(supportFragmentManager, "passwordDialog")
|
||||
} else {
|
||||
val error = getString(R.string.error_create_database)
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
@@ -493,11 +498,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
AutofillHelper.startActivityForAutofillResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
@@ -33,8 +33,10 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
@@ -111,6 +113,16 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
|
||||
private var mOnSuggestionListener: SearchView.OnSuggestionListener? = null
|
||||
|
||||
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
|
||||
// To create tree dialog for icon
|
||||
mGroupEditViewModel.selectIcon(icon)
|
||||
}
|
||||
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this)
|
||||
else null
|
||||
|
||||
private var mIconColor: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -211,11 +223,14 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
mDatabase?.let { database ->
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
EntryEditActivity.launchToCreate(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
currentGroup.nodeId
|
||||
)
|
||||
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
||||
EntryEditActivity.launchToCreate(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
currentGroup.nodeId,
|
||||
resultLauncher
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
// Search not used
|
||||
@@ -243,6 +258,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
EntryEditActivity.launchForAutofillResult(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
currentGroup.nodeId,
|
||||
searchInfo
|
||||
@@ -277,7 +293,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
mGroupEditViewModel.requestIconSelection.observe(this) { iconImage ->
|
||||
IconPickerActivity.launch(this@GroupActivity, iconImage)
|
||||
IconPickerActivity.launch(this@GroupActivity, iconImage, mIconSelectionActivityResultLauncher)
|
||||
}
|
||||
|
||||
mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
||||
@@ -319,6 +335,29 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
return rootContainerView
|
||||
}
|
||||
|
||||
private fun loadGroup(database: Database?) {
|
||||
when {
|
||||
Intent.ACTION_SEARCH == intent.action -> {
|
||||
finishNodeAction()
|
||||
val searchString =
|
||||
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
||||
mGroupViewModel.loadGroupFromSearch(
|
||||
database,
|
||||
searchString,
|
||||
PreferencesUtil.omitBackup(this)
|
||||
)
|
||||
}
|
||||
mCurrentGroupState == null -> {
|
||||
mRootGroup?.let { rootGroup ->
|
||||
mGroupViewModel.loadGroup(database, rootGroup, 0)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
mGroupViewModel.loadGroup(database, mCurrentGroupState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
@@ -328,13 +367,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
&& database?.isRecycleBinEnabled == true
|
||||
|
||||
mRootGroup = database?.rootGroup
|
||||
if (mCurrentGroupState == null) {
|
||||
mRootGroup?.let { rootGroup ->
|
||||
mGroupViewModel.loadGroup(database, rootGroup, 0)
|
||||
}
|
||||
} else {
|
||||
mGroupViewModel.loadGroup(database, mCurrentGroupState)
|
||||
}
|
||||
loadGroup(database)
|
||||
|
||||
// Search suggestion
|
||||
database?.let {
|
||||
@@ -447,16 +480,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
|
||||
transformSearchInfoIntent(intent)
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
finishNodeAction()
|
||||
val searchString =
|
||||
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
||||
mGroupViewModel.loadGroupFromSearch(
|
||||
mDatabase,
|
||||
searchString,
|
||||
PreferencesUtil.omitBackup(this)
|
||||
)
|
||||
}
|
||||
loadGroup(mDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,11 +618,14 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
val entryVersioned = node as Entry
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
EntryActivity.launch(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
entryVersioned.nodeId
|
||||
)
|
||||
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
||||
EntryActivity.launch(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
entryVersioned.nodeId,
|
||||
resultLauncher
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
// Nothing here, a search is simply performed
|
||||
@@ -653,6 +680,8 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
Log.e(TAG, "Node can't be cast in Entry")
|
||||
}
|
||||
}
|
||||
|
||||
reloadGroupIfSearch()
|
||||
}
|
||||
|
||||
private fun entrySelectedForSave(database: Database, entry: Entry, searchInfo: SearchInfo) {
|
||||
@@ -738,6 +767,12 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
actionNodeMode?.finish()
|
||||
}
|
||||
|
||||
private fun reloadGroupIfSearch() {
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
reloadCurrentGroup()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNodeSelected(
|
||||
database: Database,
|
||||
nodes: List<Node>
|
||||
@@ -787,12 +822,18 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
GroupEditDialogFragment.TAG_CREATE_GROUP
|
||||
)
|
||||
}
|
||||
Type.ENTRY -> EntryEditActivity.launchToUpdate(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
(node as Entry).nodeId
|
||||
)
|
||||
Type.ENTRY -> {
|
||||
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
||||
EntryEditActivity.launchToUpdate(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
(node as Entry).nodeId,
|
||||
resultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
reloadGroupIfSearch()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -847,6 +888,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
): Boolean {
|
||||
deleteNodes(nodes)
|
||||
finishNodeAction()
|
||||
reloadGroupIfSearch()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -934,9 +976,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
) {
|
||||
|
||||
// If no node, show education to add new one
|
||||
val addNodeButtonEducationPerformed = mGroupFragment != null
|
||||
&& mGroupFragment!!.isEmpty
|
||||
&& actionNodeMode == null
|
||||
val addNodeButtonEducationPerformed = actionNodeMode == null
|
||||
&& addNodeButtonView?.addButtonView != null
|
||||
&& addNodeButtonView!!.isEnable
|
||||
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation(
|
||||
@@ -976,7 +1016,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
|
||||
if (!sortMenuEducationPerformed) {
|
||||
// lockMenuEducationPerformed
|
||||
val lockButtonView = findViewById<View>(R.id.lock_button_icon)
|
||||
val lockButtonView = findViewById<View>(R.id.lock_button)
|
||||
lockButtonView != null
|
||||
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
|
||||
lockButtonView,
|
||||
@@ -1049,37 +1089,6 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
|
||||
/*
|
||||
* ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in
|
||||
* another app such as Files or GoogleDrive and then Search for an entry. Here we remove the
|
||||
* FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task.
|
||||
*/
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
var flags = intent.flags
|
||||
flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
|
||||
intent.flags = flags
|
||||
}
|
||||
|
||||
super.startActivityForResult(intent, requestCode, options)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// To create tree dialog for icon
|
||||
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
|
||||
mGroupEditViewModel.selectIcon(icon)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
// Directly used the onActivityResult in fragment
|
||||
mGroupFragment?.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun removeSearch() {
|
||||
intent.removeExtra(AUTO_SEARCH_KEY)
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
@@ -1095,7 +1104,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
try {
|
||||
mGroupViewModel.loadGroup(mDatabase, mCurrentGroupState)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to rebuild the list after deletion", e)
|
||||
Log.e(TAG, "Unable to rebuild the group", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1284,8 +1293,9 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
* -------------------------
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
database: Database,
|
||||
activityResultLaunch: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo? = null,
|
||||
autoSearch: Boolean = false) {
|
||||
@@ -1295,6 +1305,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
activityResultLaunch,
|
||||
autofillComponent,
|
||||
searchInfo
|
||||
)
|
||||
@@ -1327,11 +1338,12 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
* Global Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
fun launch(activity: AppCompatActivity,
|
||||
database: Database,
|
||||
onValidateSpecialMode: () -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit) {
|
||||
onLaunchActivitySpecialMode: () -> Unit,
|
||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
GroupActivity.launch(
|
||||
@@ -1443,6 +1455,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
// Here no search info found, disable auto search
|
||||
GroupActivity.launchForAutofillResult(activity,
|
||||
database,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false)
|
||||
|
||||
@@ -27,12 +27,16 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconEditDialogFragment
|
||||
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
@@ -81,6 +85,9 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
addCustomIcon(uri)
|
||||
}
|
||||
|
||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||
|
||||
@@ -139,6 +146,16 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
}
|
||||
uploadButton.isEnabled = true
|
||||
}
|
||||
iconPickerViewModel.customIconUpdated.observe(this) { iconCustomUpdated ->
|
||||
if (iconCustomUpdated.error && !iconCustomUpdated.errorConsumed) {
|
||||
Snackbar.make(coordinatorLayout, iconCustomUpdated.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||
iconCustomUpdated.errorConsumed = true
|
||||
}
|
||||
iconCustomUpdated.iconCustom?.let {
|
||||
mDatabase?.updateCustomIcon(it)
|
||||
}
|
||||
iconPickerViewModel.deselectAllCustomIcons()
|
||||
}
|
||||
}
|
||||
|
||||
override fun viewToInvalidateTimeout(): View? {
|
||||
@@ -197,6 +214,10 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
menu?.findItem(R.id.menu_edit)?.apply {
|
||||
isEnabled = mIconsSelected.size == 1
|
||||
isVisible = isEnabled
|
||||
}
|
||||
menu?.findItem(R.id.menu_delete)?.apply {
|
||||
isEnabled = mCustomIconsSelectionMode
|
||||
isVisible = isEnabled
|
||||
@@ -213,6 +234,9 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
updateCustomIcon(mIconsSelected[0])
|
||||
}
|
||||
R.id.menu_delete -> {
|
||||
mIconsSelected.forEach { iconToRemove ->
|
||||
removeCustomIcon(iconToRemove)
|
||||
@@ -277,6 +301,11 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCustomIcon(iconImageCustom: IconImageCustom) {
|
||||
IconEditDialogFragment.update(iconImageCustom)
|
||||
.show(supportFragmentManager, IconEditDialogFragment.TAG_UPDATE_ICON)
|
||||
}
|
||||
|
||||
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
|
||||
uploadButton.isEnabled = false
|
||||
iconPickerViewModel.deselectAllCustomIcons()
|
||||
@@ -286,14 +315,6 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
addCustomIcon(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setResult() {
|
||||
setResult(Activity.RESULT_OK, Intent().apply {
|
||||
putExtra(EXTRA_ICON, mIconImage)
|
||||
@@ -308,30 +329,28 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
companion object {
|
||||
|
||||
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
|
||||
|
||||
private const val ICON_SELECTED_REQUEST = 15861
|
||||
private const val EXTRA_ICON = "EXTRA_ICON"
|
||||
|
||||
private const val MAX_ICON_SIZE = 5242880
|
||||
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) {
|
||||
if (requestCode == ICON_SELECTED_REQUEST) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
||||
fun registerIconSelectionForResult(context: FragmentActivity,
|
||||
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
|
||||
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(context: Activity,
|
||||
previousIcon: IconImage?) {
|
||||
fun launch(context: FragmentActivity,
|
||||
previousIcon: IconImage?,
|
||||
resultLauncher: ActivityResultLauncher<Intent>) {
|
||||
// Create an instance to return the picker icon
|
||||
context.startActivityForResult(
|
||||
Intent(context,
|
||||
IconPickerActivity::class.java).apply {
|
||||
resultLauncher.launch(
|
||||
Intent(context, IconPickerActivity::class.java).apply {
|
||||
if (previousIcon != null)
|
||||
putExtra(EXTRA_ICON, previousIcon)
|
||||
},
|
||||
ICON_SELECTED_REQUEST)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,10 @@ import android.view.KeyEvent.KEYCODE_ENTER
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.app.ActivityCompat
|
||||
@@ -71,11 +72,12 @@ import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
|
||||
open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||
class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
@@ -89,7 +91,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||
|
||||
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
|
||||
|
||||
private var mDefaultDatabase: Boolean = false
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
@@ -111,7 +114,10 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
field = value
|
||||
}
|
||||
|
||||
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this)
|
||||
else null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -142,6 +148,12 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mDatabaseKeyFileUri = uri
|
||||
populateKeyFileTextView(uri)
|
||||
}
|
||||
}
|
||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||
@@ -170,9 +182,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||
}
|
||||
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
|
||||
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
||||
}
|
||||
|
||||
// Init Biometric elements
|
||||
advancedUnlockFragment = supportFragmentManager
|
||||
@@ -188,17 +197,17 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
|
||||
// Listen password checkbox to init advanced unlock and confirmation button
|
||||
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
|
||||
advancedUnlockFragment?.checkUnlockAvailability()
|
||||
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableOrNotTheConfirmationButton()
|
||||
}
|
||||
|
||||
// Observe if default database
|
||||
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||
mDefaultDatabase = isDefaultDatabase
|
||||
}
|
||||
|
||||
// Observe database file change
|
||||
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||
mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||
// Force read only if the file does not exists
|
||||
mForceReadOnly = databaseFile?.let {
|
||||
!it.databaseFileExists
|
||||
@@ -232,12 +241,12 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
}
|
||||
|
||||
// Don't allow auto open prompt if lock become when UI visible
|
||||
mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
||||
false
|
||||
else
|
||||
mAllowAutoOpenBiometricPrompt
|
||||
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
}
|
||||
|
||||
mDatabaseFileUri?.let { databaseFileUri ->
|
||||
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
}
|
||||
|
||||
checkPermission()
|
||||
@@ -263,7 +272,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
// Recheck advanced unlock if error
|
||||
advancedUnlockFragment?.initAdvancedUnlockMode()
|
||||
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
|
||||
|
||||
if (result.isSuccess) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
@@ -311,7 +320,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
databaseFileViewModel.removeDefaultDatabase()
|
||||
mDatabaseFileViewModel.removeDefaultDatabase()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,7 +353,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
||||
}
|
||||
mDatabaseFileUri?.let {
|
||||
databaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +370,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
database,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() }
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -435,8 +445,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||
} else {
|
||||
// Init Biometric elements
|
||||
advancedUnlockFragment?.loadDatabase(databaseFileUri,
|
||||
mAllowAutoOpenBiometricPrompt)
|
||||
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||
}
|
||||
|
||||
enableOrNotTheConfirmationButton()
|
||||
@@ -496,7 +505,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
override fun onPause() {
|
||||
// Reinit locking activity UI variable
|
||||
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||
mAllowAutoOpenBiometricPrompt = true
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
@@ -507,7 +515,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
outState.putString(KEY_KEYFILE, it.toString())
|
||||
}
|
||||
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
@@ -709,45 +716,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mAllowAutoOpenBiometricPrompt = false
|
||||
|
||||
// To get device credential unlock result
|
||||
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// To get entry in result
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
var keyFileResult = false
|
||||
mExternalFileHelper?.let {
|
||||
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
if (uri != null) {
|
||||
mDatabaseKeyFileUri = uri
|
||||
populateKeyFileTextView(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!keyFileResult) {
|
||||
// this block if not a key file response
|
||||
when (resultCode) {
|
||||
DatabaseLockActivity.RESULT_EXIT_LOCK -> {
|
||||
clearCredentialsViews()
|
||||
closeDatabase()
|
||||
}
|
||||
Activity.RESULT_CANCELED -> {
|
||||
clearCredentialsViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = PasswordActivity::class.java.name
|
||||
@@ -764,8 +732,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
||||
|
||||
private const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
val intent = Intent(activity, PasswordActivity::class.java)
|
||||
@@ -855,15 +821,17 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
@@ -891,12 +859,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
* Global Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
fun launch(activity: AppCompatActivity,
|
||||
databaseUri: Uri,
|
||||
keyFile: Uri?,
|
||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit) {
|
||||
onLaunchActivitySpecialMode: () -> Unit,
|
||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
@@ -926,6 +895,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
PasswordActivity.launchForAutofillResult(activity,
|
||||
databaseUri, keyFile,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
@@ -133,6 +132,18 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let { pathUri ->
|
||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView?.error = null
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
if (lengthFile <= 0L) {
|
||||
showEmptyKeyFileConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
val dialog = builder.create()
|
||||
@@ -208,7 +219,11 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
||||
}
|
||||
|
||||
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|
||||
if ((mMasterPassword == null
|
||||
|| mMasterPassword!!.isEmpty())
|
||||
&& (keyFileCheckBox == null
|
||||
|| !keyFileCheckBox!!.isChecked
|
||||
|| keyFileSelectionView?.uri == null)) {
|
||||
error = true
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
}
|
||||
@@ -282,23 +297,6 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
uri?.let { pathUri ->
|
||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView?.error = null
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
if (lengthFile <= 0L) {
|
||||
showEmptyKeyFileConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||
|
||||
@@ -29,6 +29,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||
|
||||
class IconEditDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private val mIconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||
|
||||
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||
private lateinit var iconView: ImageView
|
||||
private lateinit var nameTextLayoutView: TextInputLayout
|
||||
private lateinit var nameTextView: TextView
|
||||
|
||||
private var mCustomIcon: IconImageCustom? = null
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
||||
}
|
||||
mCustomIcon?.let { customIcon ->
|
||||
populateViewsWithCustomIcon(customIcon)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_edit, null)
|
||||
iconView = root.findViewById(R.id.icon_edit_image)
|
||||
nameTextLayoutView = root.findViewById(R.id.icon_edit_name_container)
|
||||
nameTextView = root.findViewById(R.id.icon_edit_name)
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||
mCustomIcon = savedInstanceState.getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||
mCustomIcon = getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
retrieveIconInfoFromViews()
|
||||
mCustomIcon?.let { customIcon ->
|
||||
mIconPickerViewModel.updateCustomIcon(
|
||||
IconPickerViewModel.IconCustomState(customIcon, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// Do nothing
|
||||
mIconPickerViewModel.updateCustomIcon(
|
||||
IconPickerViewModel.IconCustomState(null, false)
|
||||
)
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun populateViewsWithCustomIcon(customIcon: IconImageCustom) {
|
||||
mPopulateIconMethod?.invoke(iconView, customIcon.getIconImageToDraw())
|
||||
nameTextView.text = customIcon.name
|
||||
}
|
||||
|
||||
private fun retrieveIconInfoFromViews() {
|
||||
mCustomIcon?.name = nameTextView.text.toString()
|
||||
mCustomIcon?.lastModificationTime = DateInstant()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
retrieveIconInfoFromViews()
|
||||
outState.putParcelable(KEY_CUSTOM_ICON_ID, mCustomIcon)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG_UPDATE_ICON = "TAG_UPDATE_ICON"
|
||||
const val KEY_CUSTOM_ICON_ID = "KEY_CUSTOM_ICON_ID"
|
||||
|
||||
fun update(customIcon: IconImageCustom): IconEditDialogFragment {
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(KEY_CUSTOM_ICON_ID, IconImageCustom(customIcon))
|
||||
val fragment = IconEditDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,7 +309,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
s?.toString()?.let { userString ->
|
||||
try {
|
||||
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH))
|
||||
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
||||
otpSecretContainer?.error = null
|
||||
} catch (exception: Exception) {
|
||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||
|
||||
@@ -74,8 +74,18 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
private var mRecycleBinEnable: Boolean = false
|
||||
private var mRecycleBin: Group? = null
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||
var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
|
||||
entryId?.let {
|
||||
// Simply refresh the list
|
||||
rebuildList()
|
||||
// Scroll to the new entry
|
||||
mDatabase?.getEntryById(it)?.let { entry ->
|
||||
mAdapter?.indexOf(entry)?.let { position ->
|
||||
mNodesRecyclerView?.scrollToPosition(position)
|
||||
}
|
||||
}
|
||||
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
|
||||
}
|
||||
|
||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
@@ -402,27 +412,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
if (resultCode == EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE) {
|
||||
data?.getParcelableExtra<NodeId<UUID>>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let {
|
||||
// Simply refresh the list
|
||||
rebuildList()
|
||||
// Scroll to the new entry
|
||||
mDatabase?.getEntryById(it)?.let { entry ->
|
||||
mAdapter?.indexOf(entry)?.let { position ->
|
||||
mNodesRecyclerView?.scrollToPosition(position)
|
||||
}
|
||||
}
|
||||
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
|
||||
@@ -55,8 +55,10 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||
iconCustomAdded?.iconCustom?.let { icon ->
|
||||
iconPickerAdapter.addIcon(icon)
|
||||
iconCustomAdded.iconCustom = null
|
||||
try {
|
||||
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
|
||||
} catch (ignore: Exception) {}
|
||||
}
|
||||
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
|
||||
}
|
||||
}
|
||||
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
|
||||
@@ -67,6 +69,14 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||
}
|
||||
}
|
||||
}
|
||||
iconPickerViewModel.customIconUpdated.observe(viewLifecycleOwner) { iconCustomUpdated ->
|
||||
if (!iconCustomUpdated.error) {
|
||||
iconCustomUpdated?.iconCustom?.let { icon ->
|
||||
iconPickerAdapter.updateIcon(icon)
|
||||
iconCustomUpdated.iconCustom = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIconClickListener(icon: IconImageCustom) {
|
||||
|
||||
@@ -20,14 +20,16 @@
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||
@@ -38,6 +40,10 @@ class ExternalFileHelper {
|
||||
private var activity: FragmentActivity? = null
|
||||
private var fragment: Fragment? = null
|
||||
|
||||
private var getContentResultLauncher: ActivityResultLauncher<String>? = null
|
||||
private var openDocumentResultLauncher: ActivityResultLauncher<Array<String>>? = null
|
||||
private var createDocumentResultLauncher: ActivityResultLauncher<String>? = null
|
||||
|
||||
constructor(context: FragmentActivity) {
|
||||
this.activity = context
|
||||
this.fragment = null
|
||||
@@ -48,94 +54,81 @@ class ExternalFileHelper {
|
||||
this.fragment = context
|
||||
}
|
||||
|
||||
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
|
||||
|
||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||
result?.let { uri ->
|
||||
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
||||
onFileSelected?.invoke(uri)
|
||||
}
|
||||
}
|
||||
|
||||
getContentResultLauncher = if (fragment != null) {
|
||||
fragment?.registerForActivityResult(
|
||||
GetContent(),
|
||||
resultCallback
|
||||
)
|
||||
} else {
|
||||
activity?.registerForActivityResult(
|
||||
GetContent(),
|
||||
resultCallback
|
||||
)
|
||||
}
|
||||
|
||||
openDocumentResultLauncher = if (fragment != null) {
|
||||
fragment?.registerForActivityResult(
|
||||
OpenDocument(),
|
||||
resultCallback
|
||||
)
|
||||
} else {
|
||||
activity?.registerForActivityResult(
|
||||
OpenDocument(),
|
||||
resultCallback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun buildCreateDocument(typeString: String = "application/octet-stream",
|
||||
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||
|
||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||
onFileCreated.invoke(result)
|
||||
}
|
||||
|
||||
createDocumentResultLauncher = if (fragment != null) {
|
||||
fragment?.registerForActivityResult(
|
||||
CreateDocument(typeString),
|
||||
resultCallback
|
||||
)
|
||||
} else {
|
||||
activity?.registerForActivityResult(
|
||||
CreateDocument(typeString),
|
||||
resultCallback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openDocument(getContent: Boolean = false,
|
||||
typeString: String = "*/*") {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
try {
|
||||
if (getContent) {
|
||||
openActivityWithActionGetContent(typeString)
|
||||
} else {
|
||||
openActivityWithActionOpenDocument(typeString)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open document", e)
|
||||
showFileManagerDialogFragment()
|
||||
try {
|
||||
if (getContent) {
|
||||
getContentResultLauncher?.launch(typeString)
|
||||
} else {
|
||||
openDocumentResultLauncher?.launch(arrayOf(typeString))
|
||||
}
|
||||
} else {
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open document", e)
|
||||
showFileManagerDialogFragment()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun openActivityWithActionOpenDocument(typeString: String) {
|
||||
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
}
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
fun createDocument(titleString: String) {
|
||||
try {
|
||||
createDocumentResultLauncher?.launch(titleString)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create document", e)
|
||||
showFileManagerDialogFragment()
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
else
|
||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun openActivityWithActionGetContent(typeString: String) {
|
||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
}
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
else
|
||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* To use in onActivityResultCallback in Fragment or Activity
|
||||
* @param onFileSelected Callback retrieve from data
|
||||
* @return true if requestCode was captured, false elsewhere
|
||||
*/
|
||||
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
|
||||
|
||||
when (requestCode) {
|
||||
FILE_BROWSE -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
val filename = data?.dataString
|
||||
var keyUri: Uri? = null
|
||||
if (filename != null) {
|
||||
keyUri = UriUtil.parse(filename)
|
||||
}
|
||||
onFileSelected?.invoke(keyUri)
|
||||
}
|
||||
return true
|
||||
}
|
||||
GET_CONTENT, OPEN_DOC -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (data != null) {
|
||||
val uri = data.data
|
||||
if (uri != null) {
|
||||
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
||||
onFileSelected?.invoke(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,62 +148,50 @@ class ExternalFileHelper {
|
||||
}
|
||||
}
|
||||
|
||||
fun createDocument(titleString: String,
|
||||
typeString: String = "application/octet-stream"): Int? {
|
||||
val idCode = getUnusedCreateFileRequestCode()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
putExtra(Intent.EXTRA_TITLE, titleString)
|
||||
class OpenDocument : ActivityResultContracts.OpenDocument() {
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun createIntent(context: Context, input: Array<out String>): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intent, idCode)
|
||||
else
|
||||
activity?.startActivityForResult(intent, idCode)
|
||||
return idCode
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create document", e)
|
||||
showFileManagerDialogFragment()
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
} else {
|
||||
showFileManagerDialogFragment()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* To use in onActivityResultCallback in Fragment or Activity
|
||||
* @param onFileCreated Callback retrieve from data
|
||||
* @return true if requestCode was captured, false elsewhere
|
||||
*/
|
||||
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||
// Retrieve the created URI from the file manager
|
||||
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) {
|
||||
onFileCreated.invoke(data?.data)
|
||||
fileRequestCodes.remove(requestCode)
|
||||
class GetContent : ActivityResultContracts.GetContent() {
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
}
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "OpenFileHelper"
|
||||
|
||||
private const val GET_CONTENT = 25745
|
||||
private const val OPEN_DOC = 25845
|
||||
private const val FILE_BROWSE = 25645
|
||||
|
||||
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
|
||||
private var fileRequestCodes = ArrayList<Int>()
|
||||
|
||||
private fun getUnusedCreateFileRequestCode(): Int {
|
||||
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
|
||||
fileRequestCodes.add(newCreateFileRequestCode)
|
||||
return newCreateFileRequestCode
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
||||
typeString: String = "application/octet-stream"): Boolean {
|
||||
@@ -231,7 +212,7 @@ class ExternalFileHelper {
|
||||
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||
externalFileHelper?.let { fileHelper ->
|
||||
setOnClickListener {
|
||||
fileHelper.openDocument()
|
||||
fileHelper.openDocument(false)
|
||||
}
|
||||
setOnLongClickListener {
|
||||
fileHelper.openDocument(true)
|
||||
|
||||
@@ -100,7 +100,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
||||
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.saveColor.observe(this) {
|
||||
@@ -180,8 +180,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
closeDatabase(database)
|
||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||
// Add onActivityForResult response
|
||||
setResult(RESULT_EXIT_LOCK)
|
||||
mExitLock = true
|
||||
closeOptionsMenu()
|
||||
finish()
|
||||
}
|
||||
@@ -353,14 +352,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == RESULT_EXIT_LOCK) {
|
||||
mExitLock = true
|
||||
lockAndExit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkRegister() {
|
||||
// If in ave or registration mode, don't allow read only
|
||||
if ((mSpecialMode == SpecialMode.SAVE
|
||||
@@ -440,8 +431,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
|
||||
const val TAG = "LockingActivity"
|
||||
|
||||
const val RESULT_EXIT_LOCK = 1450
|
||||
|
||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||
|
||||
|
||||
@@ -39,7 +39,11 @@ object Stylish {
|
||||
*/
|
||||
fun load(context: Context) {
|
||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
||||
themeString = PreferencesUtil.getStyle(context)
|
||||
try {
|
||||
themeString = PreferencesUtil.getStyle(context)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Stylish", "Unable to get preference style", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
||||
|
||||
@@ -23,12 +23,12 @@ import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
abstract class StylishFragment : Fragment() {
|
||||
|
||||
@@ -42,7 +42,6 @@ abstract class StylishFragment : Fragment() {
|
||||
contextThemed = ContextThemeWrapper(context, themeId)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
// To fix status bar color
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
@@ -58,6 +57,7 @@ abstract class StylishFragment : Fragment() {
|
||||
try {
|
||||
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
|
||||
if (taWindowStatusLight?.getBoolean(0, false) == true) {
|
||||
@Suppress("DEPRECATION")
|
||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||
}
|
||||
taWindowStatusLight?.recycle()
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -41,9 +42,11 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.setTextSize
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
import java.util.*
|
||||
@@ -64,8 +67,10 @@ class NodeAdapter (private val context: Context,
|
||||
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
|
||||
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||
private var mPrefSizeMultiplier: Float = 0F
|
||||
private var mSubtextDefaultDimension: Float = 0F
|
||||
private var mInfoTextDefaultDimension: Float = 0F
|
||||
private var mTextDefaultDimension: Float = 0F
|
||||
private var mSubTextDefaultDimension: Float = 0F
|
||||
private var mMetaTextDefaultDimension: Float = 0F
|
||||
private var mOtpTokenTextDefaultDimension: Float = 0F
|
||||
private var mNumberChildrenTextDefaultDimension: Float = 0F
|
||||
private var mIconDefaultDimension: Float = 0F
|
||||
|
||||
@@ -77,6 +82,7 @@ class NodeAdapter (private val context: Context,
|
||||
|
||||
private var mActionNodesList = LinkedList<Node>()
|
||||
private var mNodeClickCallback: NodeClickCallback? = null
|
||||
private var mClipboardHelper = ClipboardHelper(context)
|
||||
|
||||
@ColorInt
|
||||
private val mContentSelectionColor: Int
|
||||
@@ -299,8 +305,10 @@ class NodeAdapter (private val context: Context,
|
||||
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
||||
}
|
||||
val nodeViewHolder = NodeViewHolder(view)
|
||||
mInfoTextDefaultDimension = nodeViewHolder.text.textSize
|
||||
mSubtextDefaultDimension = nodeViewHolder.subText.textSize
|
||||
mTextDefaultDimension = nodeViewHolder.text.textSize
|
||||
mSubTextDefaultDimension = nodeViewHolder.subText?.textSize ?: mSubTextDefaultDimension
|
||||
mMetaTextDefaultDimension = nodeViewHolder.meta.textSize
|
||||
mOtpTokenTextDefaultDimension = nodeViewHolder.otpToken?.textSize ?: mOtpTokenTextDefaultDimension
|
||||
nodeViewHolder.numberChildren?.let {
|
||||
mNumberChildrenTextDefaultDimension = it.textSize
|
||||
}
|
||||
@@ -311,7 +319,9 @@ class NodeAdapter (private val context: Context,
|
||||
val subNode = mNodeSortedList.get(position)
|
||||
|
||||
// Node selection
|
||||
holder.container.isSelected = mActionNodesList.contains(subNode)
|
||||
holder.container.apply {
|
||||
isSelected = mActionNodesList.contains(subNode)
|
||||
}
|
||||
|
||||
// Assign image
|
||||
val iconColor = if (holder.container.isSelected)
|
||||
@@ -333,19 +343,18 @@ class NodeAdapter (private val context: Context,
|
||||
// Assign text
|
||||
holder.text.apply {
|
||||
text = subNode.title
|
||||
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier)
|
||||
setTextSize(mTextSizeUnit, mTextDefaultDimension, mPrefSizeMultiplier)
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
}
|
||||
// Add subText with username
|
||||
holder.subText.apply {
|
||||
text = ""
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
visibility = View.GONE
|
||||
}
|
||||
// Add meta text to show UUID
|
||||
holder.meta.apply {
|
||||
text = subNode.nodeId.toString()
|
||||
visibility = if (mShowUUID) View.VISIBLE else View.GONE
|
||||
if (mShowUUID) {
|
||||
text = subNode.nodeId.toString()
|
||||
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
// Specific elements for entry
|
||||
@@ -354,12 +363,16 @@ class NodeAdapter (private val context: Context,
|
||||
database.startManageEntry(entry)
|
||||
|
||||
holder.text.text = entry.getVisualTitle()
|
||||
holder.subText.apply {
|
||||
// Add subText with username
|
||||
holder.subText?.apply {
|
||||
val username = entry.username
|
||||
if (mShowUserNames && username.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
text = username
|
||||
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier)
|
||||
setTextSize(mTextSizeUnit, mSubTextDefaultDimension, mPrefSizeMultiplier)
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +440,21 @@ class NodeAdapter (private val context: Context,
|
||||
}
|
||||
}
|
||||
}
|
||||
holder?.otpToken?.text = otpElement?.token
|
||||
holder?.otpToken?.apply {
|
||||
text = otpElement?.token
|
||||
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
||||
}
|
||||
holder?.otpContainer?.setOnClickListener {
|
||||
otpElement?.token?.let { token ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.copy_field,
|
||||
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
mClipboardHelper.copyToClipboard(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OtpRunnable(val view: View?): Runnable {
|
||||
@@ -468,7 +495,7 @@ class NodeAdapter (private val context: Context,
|
||||
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
|
||||
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||
var subText: TextView? = itemView.findViewById(R.id.node_subtext)
|
||||
var meta: TextView = itemView.findViewById(R.id.node_meta)
|
||||
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
|
||||
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)
|
||||
|
||||
@@ -16,9 +16,9 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
|
||||
|
||||
class TemplatesSelectorAdapter(private val context: Context,
|
||||
private val iconDrawableFactory: IconDrawableFactory?,
|
||||
private var templates: List<Template>): BaseAdapter() {
|
||||
|
||||
var iconDrawableFactory: IconDrawableFactory? = null
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var mIconColor = Color.BLACK
|
||||
|
||||
|
||||
@@ -4,4 +4,4 @@ import android.app.assist.AssistStructure
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
|
||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
||||
val inlineSuggestionsRequest: InlineSuggestionsRequest?)
|
||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
||||
@@ -25,7 +25,6 @@ import android.app.PendingIntent
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
@@ -35,11 +34,13 @@ import android.service.autofill.InlinePresentation
|
||||
import android.util.Log
|
||||
import android.view.autofill.AutofillManager
|
||||
import android.view.autofill.AutofillValue
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.Toast
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -49,21 +50,19 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import kotlin.collections.ArrayList
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
object AutofillHelper {
|
||||
|
||||
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
||||
|
||||
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||
const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||
|
||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
||||
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||
@@ -112,7 +111,7 @@ object AutofillHelper {
|
||||
database: Database,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
inlinePresentation: InlinePresentation?): Dataset? {
|
||||
additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
|
||||
val title = makeEntryTitle(entryInfo)
|
||||
val views = newRemoteViews(context, database, title, entryInfo.icon)
|
||||
val builder = Dataset.Builder(views)
|
||||
@@ -201,11 +200,7 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlinePresentation?.let {
|
||||
builder.setInlinePresentation(it)
|
||||
}
|
||||
}
|
||||
additionalBuild?.invoke(builder)
|
||||
|
||||
return try {
|
||||
builder.build()
|
||||
@@ -236,40 +231,51 @@ object AutofillHelper {
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun buildInlinePresentationForEntry(context: Context,
|
||||
database: Database,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||
positionItem: Int,
|
||||
entryInfo: EntryInfo): InlinePresentation? {
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||
|
||||
if (positionItem <= maxSuggestion - 1
|
||||
&& inlinePresentationSpecs.size > positionItem) {
|
||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||
if (positionItem <= maxSuggestion - 1
|
||||
&& inlinePresentationSpecs.size > positionItem
|
||||
) {
|
||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||
|
||||
// Make sure that the IME spec claims support for v1 UI template.
|
||||
val imeStyle = inlinePresentationSpec.style
|
||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||
return null
|
||||
// Make sure that the IME spec claims support for v1 UI template.
|
||||
val imeStyle = inlinePresentationSpec.style
|
||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||
return null
|
||||
|
||||
// Build the content for IME UI
|
||||
val pendingIntent = PendingIntent.getActivity(context,
|
||||
// Build the content for IME UI
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, AutofillSettingsActivity::class.java),
|
||||
0)
|
||||
return InlinePresentation(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
return InlinePresentation(
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
||||
setTitle(entryInfo.title)
|
||||
setSubtitle(entryInfo.username)
|
||||
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
setStartIcon(
|
||||
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
||||
setEndIcon(icon.apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
}
|
||||
}.build().slice, inlinePresentationSpec, false)
|
||||
}.build().slice, inlinePresentationSpec, false
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -299,7 +305,7 @@ object AutofillHelper {
|
||||
database: Database,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
// Add Header
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -320,7 +326,7 @@ object AutofillHelper {
|
||||
// Add inline suggestion for new IME and dataset
|
||||
var numberInlineSuggestions = 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||
@@ -332,14 +338,19 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
entriesInfo.forEachIndexed { _, entry ->
|
||||
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry)
|
||||
}
|
||||
if (numberInlineSuggestions > 0
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& compatInlineSuggestionsRequest != null) {
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
|
||||
buildInlinePresentationForEntry(context, database,
|
||||
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
|
||||
)?.let { inlinePresentation ->
|
||||
builder.setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
null
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
|
||||
}
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
|
||||
}
|
||||
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
@@ -351,14 +362,14 @@ object AutofillHelper {
|
||||
}
|
||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
||||
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||
searchInfo, inlineSuggestionsRequest)
|
||||
searchInfo, compatInlineSuggestionsRequest)
|
||||
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
autofillIds.forEach { id ->
|
||||
val builder = Dataset.Builder(manualSelectionView)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
||||
inlinePresentation?.let {
|
||||
@@ -403,11 +414,11 @@ object AutofillHelper {
|
||||
StructureParser(structure).parse()?.let { result ->
|
||||
// New Response
|
||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
if (inlineSuggestionsRequest != null) {
|
||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<CompatInlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
if (compatInlineSuggestionsRequest != null) {
|
||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
buildResponse(activity, database, entriesInfo, result, inlineSuggestionsRequest)
|
||||
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
|
||||
} else {
|
||||
buildResponse(activity, database, entriesInfo, result, null)
|
||||
}
|
||||
@@ -427,37 +438,44 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
|
||||
fun buildActivityResultLauncher(activity: AppCompatActivity,
|
||||
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
|
||||
return activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
// Utility method to loop and close each activity with return data
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
activity.setResult(it.resultCode, it.data)
|
||||
}
|
||||
if (it.resultCode == Activity.RESULT_CANCELED) {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
activity.finish()
|
||||
|
||||
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
|
||||
// Close the database
|
||||
activity.sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to start an activity with an Autofill for result
|
||||
*/
|
||||
fun startActivityForAutofillResult(activity: Activity,
|
||||
fun startActivityForAutofillResult(activity: AppCompatActivity,
|
||||
intent: Intent,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?) {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
||||
autofillComponent.inlineSuggestionsRequest?.let {
|
||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
}
|
||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to loop and close each activity with return data
|
||||
*/
|
||||
fun onActivityResultSetResultAndFinish(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
activity.setResult(resultCode, data)
|
||||
}
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
}
|
||||
activity.finish()
|
||||
}
|
||||
activityResultLauncher?.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.autofill
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.service.autofill.FillRequest
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
/**
|
||||
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class CompatInlineSuggestionsRequest : Parcelable {
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.R)
|
||||
var inlineSuggestionsRequest: InlineSuggestionsRequest? = null
|
||||
private set
|
||||
|
||||
constructor(fillRequest: FillRequest) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
this.inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
|
||||
} else {
|
||||
this.inlineSuggestionsRequest = null
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
constructor(inlineSuggestionsRequest: InlineSuggestionsRequest?) {
|
||||
this.inlineSuggestionsRequest = inlineSuggestionsRequest
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
this.inlineSuggestionsRequest =
|
||||
parcel.readParcelable(FillRequest::class.java.classLoader)
|
||||
}
|
||||
else {
|
||||
this.inlineSuggestionsRequest = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
parcel.writeParcelable(inlineSuggestionsRequest, flags)
|
||||
}
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<CompatInlineSuggestionsRequest> {
|
||||
override fun createFromParcel(parcel: Parcel): CompatInlineSuggestionsRequest {
|
||||
return CompatInlineSuggestionsRequest(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<CompatInlineSuggestionsRequest?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -109,7 +109,7 @@ class KeeAutofillService : AutofillService() {
|
||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
request.inlineSuggestionsRequest
|
||||
CompatInlineSuggestionsRequest(request)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -127,7 +127,7 @@ class KeeAutofillService : AutofillService() {
|
||||
private fun launchSelection(database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
@@ -155,7 +155,7 @@ class KeeAutofillService : AutofillService() {
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
database: Database?,
|
||||
searchInfo: SearchInfo,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
@@ -249,7 +249,7 @@ class KeeAutofillService : AutofillService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
inlineSuggestionsRequest?.let {
|
||||
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||
&& inlinePresentationSpecs.size > 0) {
|
||||
@@ -262,9 +262,13 @@ class KeeAutofillService : AutofillService() {
|
||||
inlinePresentation = InlinePresentation(
|
||||
InlineSuggestionUi.newContentBuilder(
|
||||
PendingIntent.getActivity(this,
|
||||
0,
|
||||
Intent(this, AutofillSettingsActivity::class.java),
|
||||
0)
|
||||
0,
|
||||
Intent(this, AutofillSettingsActivity::class.java),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
})
|
||||
).apply {
|
||||
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
||||
setTitle(getString(R.string.autofill_sign_in_prompt))
|
||||
@@ -277,8 +281,9 @@ class KeeAutofillService : AutofillService() {
|
||||
}
|
||||
// Build response
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
||||
} else {
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||
}
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||
callback.onSuccess(responseBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +53,10 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
applicationId = windowNode.title.toString().split("/")[0]
|
||||
Log.d(TAG, "Autofill applicationId: $applicationId")
|
||||
|
||||
if (parseViewNode(windowNode.rootViewNode))
|
||||
break@mainLoop
|
||||
if (applicationId?.contains("PopupWindow:") == false) {
|
||||
if (parseViewNode(windowNode.rootViewNode))
|
||||
break@mainLoop
|
||||
}
|
||||
}
|
||||
// If not explicit username field found, add the field just before password field.
|
||||
if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
|
||||
@@ -270,12 +272,12 @@ class StructureParser(private val structure: AssistStructure) {
|
||||
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
|
||||
val autofillId = node.autofillId
|
||||
val nodHtml = node.htmlInfo
|
||||
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
|
||||
when (nodHtml?.tag?.lowercase(Locale.ENGLISH)) {
|
||||
"input" -> {
|
||||
nodHtml.attributes?.forEach { pairAttribute ->
|
||||
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
|
||||
when (pairAttribute.first.lowercase(Locale.ENGLISH)) {
|
||||
"type" -> {
|
||||
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
||||
when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
|
||||
"tel", "email" -> {
|
||||
result?.usernameId = autofillId
|
||||
result?.usernameValue = node.autofillValue
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -27,9 +28,11 @@ import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -39,6 +42,7 @@ import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -59,9 +63,12 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
/**
|
||||
* Manage setting to auto open biometric prompt
|
||||
*/
|
||||
private var mAutoOpenPrompt: Boolean = false
|
||||
private var mAutoOpenPrompt: Boolean
|
||||
get() {
|
||||
return field && mAutoOpenPromptEnabled
|
||||
return mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt && mAutoOpenPromptEnabled
|
||||
}
|
||||
set(value) {
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = value
|
||||
}
|
||||
|
||||
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
||||
@@ -72,6 +79,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
|
||||
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
||||
|
||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
|
||||
|
||||
// Only to fix multiple fingerprint menu #332
|
||||
private var mAllowAdvancedUnlockMenu = false
|
||||
private var mAddBiometricMenuInProgress = false
|
||||
@@ -79,6 +88,15 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
// Only keep connection when we request a device credential activity
|
||||
private var keepConnection = false
|
||||
|
||||
private var mDeviceCredentialResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
// To wait resume
|
||||
if (keepConnection) {
|
||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = result.resultCode == Activity.RESULT_OK
|
||||
}
|
||||
keepConnection = false
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
@@ -97,10 +115,21 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
retainInstance = true
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
|
||||
|
||||
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
|
||||
initAdvancedUnlockMode()
|
||||
}
|
||||
|
||||
mAdvancedUnlockViewModel.onUnlockAvailabilityCheckRequested.observe(this) {
|
||||
checkUnlockAvailability()
|
||||
}
|
||||
|
||||
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
|
||||
onDatabaseLoaded(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
@@ -114,17 +143,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
return rootView
|
||||
}
|
||||
|
||||
private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
|
||||
private var activityResult: ActivityResult? = null
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
// To wait resume
|
||||
if (keepConnection) {
|
||||
activityResult = ActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
keepConnection = false
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
context?.let {
|
||||
@@ -154,32 +172,38 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) {
|
||||
private fun onDatabaseLoaded(databaseUri: Uri?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// To get device credential unlock result, only if same database uri
|
||||
if (databaseUri != null
|
||||
&& mAdvancedUnlockEnabled) {
|
||||
activityResult?.let {
|
||||
val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded
|
||||
deviceCredentialAuthSucceeded?.let {
|
||||
if (databaseUri == databaseFileUri) {
|
||||
advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode)
|
||||
if (deviceCredentialAuthSucceeded == true) {
|
||||
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||
} else {
|
||||
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
|
||||
}
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
} ?: run {
|
||||
this.mAutoOpenPrompt = autoOpenPrompt
|
||||
connect(databaseUri)
|
||||
if (databaseUri != databaseFileUri) {
|
||||
connect(databaseUri)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
activityResult = null
|
||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check unlock availability and change the current mode depending of device's state
|
||||
*/
|
||||
fun checkUnlockAvailability() {
|
||||
private fun checkUnlockAvailability() {
|
||||
context?.let { context ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
allowOpenBiometricPrompt = true
|
||||
@@ -317,7 +341,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
if (cryptoPrompt.isDeviceCredentialOperation)
|
||||
keepConnection = true
|
||||
try {
|
||||
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt)
|
||||
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
|
||||
mDeviceCredentialResultLauncher)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open advanced unlock prompt", e)
|
||||
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
||||
@@ -369,8 +394,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun initAdvancedUnlockMode() {
|
||||
private fun initAdvancedUnlockMode() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mAllowAdvancedUnlockMenu = false
|
||||
try {
|
||||
@@ -444,6 +468,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun deleteEncryptedDatabaseKey() {
|
||||
mAllowAdvancedUnlockMenu = false
|
||||
advancedUnlockManager?.closeBiometricPrompt()
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
||||
@@ -516,6 +541,11 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||
|
||||
@@ -19,15 +19,18 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.biometric
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.*
|
||||
@@ -35,6 +38,7 @@ import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.security.KeyStore
|
||||
import java.security.UnrecoverableKeyException
|
||||
@@ -136,18 +140,24 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
// and the constrains (purposes) in the constructor of the Builder
|
||||
keyGenerator?.init(
|
||||
KeyGenParameterSpec.Builder(
|
||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
.apply {
|
||||
// Require the user to authenticate with a fingerprint to authorize every use
|
||||
// of the key, don't use it for device credential because it's the user authentication
|
||||
.apply {
|
||||
if (biometricUnlockEnable) {
|
||||
setUserAuthenticationRequired(true)
|
||||
}
|
||||
if (biometricUnlockEnable) {
|
||||
setUserAuthenticationRequired(true)
|
||||
}
|
||||
.build())
|
||||
// To store in the security chip
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
&& retrieveContext().packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
|
||||
setIsStrongBoxBacked(true)
|
||||
}
|
||||
}
|
||||
.build())
|
||||
keyGenerator?.generateKey()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -164,8 +174,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
return null
|
||||
}
|
||||
|
||||
fun initEncryptData(actionIfCypherInit
|
||||
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||
fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
|
||||
initEncryptData(actionIfCypherInit, true)
|
||||
}
|
||||
|
||||
private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
firstLaunch: Boolean) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
@@ -185,10 +199,15 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||
advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
|
||||
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
||||
initEncryptData(actionIfCypherInit, false)
|
||||
} else {
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
@@ -214,8 +233,14 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
||||
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||
fun initDecryptData(ivSpecValue: String,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, true)
|
||||
}
|
||||
|
||||
private fun initDecryptData(ivSpecValue: String,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
firstLaunch: Boolean = true) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
@@ -239,10 +264,20 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
||||
deleteKeystoreKey()
|
||||
if (firstLaunch) {
|
||||
deleteKeystoreKey()
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
||||
} else {
|
||||
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
||||
}
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
if (firstLaunch) {
|
||||
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
||||
} else {
|
||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", e)
|
||||
advancedUnlockCallback?.onGenericException(e)
|
||||
@@ -278,9 +313,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Synchronized
|
||||
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
|
||||
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
|
||||
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
// Init advanced unlock prompt
|
||||
if (biometricPrompt == null) {
|
||||
biometricPrompt = BiometricPrompt(retrieveContext(),
|
||||
@@ -311,20 +346,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
else if (cryptoPrompt.isDeviceCredentialOperation) {
|
||||
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
|
||||
retrieveContext().startActivityForResult(
|
||||
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription),
|
||||
REQUEST_DEVICE_CREDENTIAL)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int) {
|
||||
if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||
} else {
|
||||
advancedUnlockCallback?.onAuthenticationFailed()
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
deviceCredentialResultLauncher.launch(
|
||||
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +358,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
|
||||
interface AdvancedUnlockErrorCallback {
|
||||
fun onUnrecoverableKeyException(e: Exception)
|
||||
fun onInvalidKeyException(e: Exception)
|
||||
fun onGenericException(e: Exception)
|
||||
}
|
||||
@@ -355,8 +381,6 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||
|
||||
private const val REQUEST_DEVICE_CREDENTIAL = 556
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun canAuthenticate(context: Context): Int {
|
||||
return try {
|
||||
@@ -449,6 +473,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
|
||||
override fun handleDecryptedResult(decryptedValue: String) {}
|
||||
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
advancedCallback.onUnrecoverableKeyException(e)
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
advancedCallback.onInvalidKeyException(e)
|
||||
}
|
||||
@@ -460,6 +488,33 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
deleteKeystoreKey()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
deleteEntryKeyInKeystoreForBiometric(
|
||||
activity,
|
||||
object : AdvancedUnlockErrorCallback {
|
||||
fun showException(e: Exception) {
|
||||
Toast.makeText(activity,
|
||||
activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onUnrecoverableKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onGenericException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -192,17 +192,17 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
||||
private val MIN_VERSION = UnsignedInt(0x10)
|
||||
private val MAX_VERSION = UnsignedInt(0x13)
|
||||
|
||||
private val DEFAULT_ITERATIONS = UnsignedLong(2L)
|
||||
private val DEFAULT_ITERATIONS = UnsignedLong(3L)
|
||||
private val MIN_ITERATIONS = UnsignedLong(1L)
|
||||
private val MAX_ITERATIONS = UnsignedLong(4294967295L)
|
||||
|
||||
private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L))
|
||||
private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L * 16L))
|
||||
private val MIN_MEMORY = UnsignedLong(1024L * 8L)
|
||||
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||
private const val MEMORY_BLOCK_SIZE: Long = 1024L
|
||||
|
||||
private val DEFAULT_PARALLELISM = UnsignedInt(2)
|
||||
private val DEFAULT_PARALLELISM = UnsignedInt(4)
|
||||
private val MIN_PARALLELISM = UnsignedInt.fromKotlinLong(1L)
|
||||
private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1))
|
||||
private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
|
||||
pwEntry.notes = getString(getColumnIndex(COLUMN_INDEX_NOTES))
|
||||
pwEntry.expiryTime = DateInstant(getString(getColumnIndex(COLUMN_INDEX_EXPIRY_TIME)))
|
||||
pwEntry.expires = getString(getColumnIndex(COLUMN_INDEX_EXPIRES))
|
||||
.toLowerCase(Locale.ENGLISH) != "false"
|
||||
.lowercase(Locale.ENGLISH) != "false"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -147,6 +147,10 @@ class Database {
|
||||
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
|
||||
}
|
||||
|
||||
fun updateCustomIcon(customIcon: IconImageCustom) {
|
||||
iconsManager.getIcon(customIcon.uuid).updateWith(customIcon)
|
||||
}
|
||||
|
||||
fun getTemplates(templateCreation: Boolean): List<Template> {
|
||||
return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf()
|
||||
}
|
||||
|
||||
@@ -177,16 +177,20 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
||||
fun addChildrenFrom(group: Group) {
|
||||
group.groupKDB?.getChildEntries()?.forEach { entryToAdd ->
|
||||
groupKDB?.addChildEntry(entryToAdd)
|
||||
entryToAdd.parent = groupKDB
|
||||
}
|
||||
group.groupKDB?.getChildGroups()?.forEach { groupToAdd ->
|
||||
groupKDB?.addChildGroup(groupToAdd)
|
||||
groupToAdd.parent = groupKDB
|
||||
}
|
||||
|
||||
group.groupKDBX?.getChildEntries()?.forEach { entryToAdd ->
|
||||
groupKDBX?.addChildEntry(entryToAdd)
|
||||
entryToAdd.parent = groupKDBX
|
||||
}
|
||||
group.groupKDBX?.getChildGroups()?.forEach { groupToAdd ->
|
||||
groupKDBX?.addChildGroup(groupToAdd)
|
||||
groupToAdd.parent = groupKDBX
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(bi
|
||||
return newUUID
|
||||
}
|
||||
|
||||
fun getCustomIcon(key: UUID): IconImageCustom? {
|
||||
return customIcons[key]
|
||||
}
|
||||
|
||||
fun any(predicate: (IconImageCustom)-> Boolean): Boolean {
|
||||
return customIcons.any { predicate(it.value) }
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||
|
||||
override val version: String
|
||||
get() = "KeePass 1"
|
||||
get() = "V1"
|
||||
|
||||
init {
|
||||
kdfListV3.add(KdfFactory.aesKdf)
|
||||
|
||||
@@ -156,7 +156,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
FILE_VERSION_41 -> "4.1"
|
||||
else -> "UNKNOWN"
|
||||
}
|
||||
return "KeePass 2 - KDBX$kdbxStringVersion"
|
||||
return "V2 - KDBX$kdbxStringVersion"
|
||||
}
|
||||
|
||||
override val kdfEngine: KdfEngine?
|
||||
|
||||
@@ -124,25 +124,29 @@ abstract class DatabaseVersioned<
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
||||
val keyData = keyInputStream.readBytes()
|
||||
try {
|
||||
val keyData = keyInputStream.readBytes()
|
||||
|
||||
// Check XML key file
|
||||
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
|
||||
if (xmlKeyByteArray != null) {
|
||||
return xmlKeyByteArray
|
||||
}
|
||||
|
||||
// Check 32 bytes key file
|
||||
when (keyData.size) {
|
||||
32 -> return keyData
|
||||
64 -> try {
|
||||
return Hex.decodeHex(String(keyData).toCharArray())
|
||||
} catch (ignoredException: Exception) {
|
||||
// Key is not base 64, treat it as binary data
|
||||
// Check XML key file
|
||||
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
|
||||
if (xmlKeyByteArray != null) {
|
||||
return xmlKeyByteArray
|
||||
}
|
||||
|
||||
// Check 32 bytes key file
|
||||
when (keyData.size) {
|
||||
32 -> return keyData
|
||||
64 -> try {
|
||||
return Hex.decodeHex(String(keyData).toCharArray())
|
||||
} catch (ignoredException: Exception) {
|
||||
// Key is not base 64, treat it as binary data
|
||||
}
|
||||
}
|
||||
// Hash file as binary data
|
||||
return HashManager.hashSha256(keyData)
|
||||
} catch (outOfMemoryError: OutOfMemoryError) {
|
||||
throw IOException("Keyfile data is too large", outOfMemoryError)
|
||||
}
|
||||
// Hash file as binary data
|
||||
return HashManager.hashSha256(keyData)
|
||||
}
|
||||
|
||||
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||
|
||||
@@ -32,6 +32,16 @@ class IconImageCustom : IconImageDraw {
|
||||
var name: String = ""
|
||||
var lastModificationTime: DateInstant? = null
|
||||
|
||||
fun updateWith(icon: IconImageCustom) {
|
||||
this.name = icon.name
|
||||
this.lastModificationTime = icon.lastModificationTime
|
||||
}
|
||||
|
||||
constructor(copy: IconImageCustom) {
|
||||
this.uuid = copy.uuid
|
||||
updateWith(copy)
|
||||
}
|
||||
|
||||
constructor(name: String = "", lastModificationTime: DateInstant? = null) {
|
||||
this.uuid = DatabaseVersioned.UUID_ZERO
|
||||
this.name = name
|
||||
|
||||
@@ -65,7 +65,7 @@ class IconsManager(binaryCache: BinaryCache) {
|
||||
}
|
||||
|
||||
fun getIcon(iconUuid: UUID): IconImageCustom {
|
||||
return IconImageCustom(iconUuid)
|
||||
return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid)
|
||||
}
|
||||
|
||||
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
|
||||
|
||||
@@ -208,16 +208,8 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
||||
when (attribute.type) {
|
||||
TemplateAttributeType.TEXT -> {
|
||||
try {
|
||||
when (attribute.options.getNumberLines()) {
|
||||
1 -> {
|
||||
// If one line, default attribute option is number of chars
|
||||
attribute.options.setNumberChars(defaultOption.toInt())
|
||||
}
|
||||
else -> {
|
||||
// else it's number of lines
|
||||
attribute.options.setNumberLines(defaultOption.toInt())
|
||||
}
|
||||
}
|
||||
// It's always a number of lines...
|
||||
attribute.options.setNumberLines(defaultOption.toInt())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to transform default text option", e)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ object TemplateField {
|
||||
const val LABEL_DATE_OF_ISSUE = "Date of issue"
|
||||
const val LABEL_EMAIL = "Email"
|
||||
const val LABEL_EMAIL_ADDRESS = "Email address"
|
||||
const val LABEL_WIRELESS = "Wifi"
|
||||
const val LABEL_WIRELESS = "Wi-Fi"
|
||||
const val LABEL_SSID = "SSID"
|
||||
const val LABEL_TYPE = "Type"
|
||||
const val LABEL_CRYPTOCURRENCY = "Cryptocurrency wallet"
|
||||
|
||||
@@ -130,6 +130,6 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val EndHeaderValue = byteArrayOf('\r'.toByte(), '\n'.toByte(), '\r'.toByte(), '\n'.toByte())
|
||||
private val EndHeaderValue = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte(), '\r'.code.toByte(), '\n'.code.toByte())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,7 +765,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
var character: Char
|
||||
for (element in text) {
|
||||
character = element
|
||||
val hexChar = character.toInt()
|
||||
val hexChar = character.code
|
||||
if (
|
||||
hexChar in 0x20..0xD7FF ||
|
||||
hexChar == 0x9 ||
|
||||
|
||||
@@ -119,7 +119,7 @@ class GroupActivityEducation(activity: Activity)
|
||||
.outerCircleColorInt(getCircleColor())
|
||||
.outerCircleAlpha(getCircleAlpha())
|
||||
.textColorInt(getTextColor())
|
||||
.tintTarget(true)
|
||||
.tintTarget(false)
|
||||
.cancelable(true),
|
||||
object : TapTargetView.Listener() {
|
||||
override fun onTargetClick(view: TapTargetView) {
|
||||
|
||||
@@ -178,6 +178,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
assignKeyboardView()
|
||||
}
|
||||
|
||||
override fun onEvaluateFullscreenMode(): Boolean {
|
||||
return resources.getBoolean(R.bool.magikeyboard_allow_fullscreen_mode)
|
||||
&& super.onEvaluateFullscreenMode()
|
||||
}
|
||||
|
||||
private fun playVibration(keyCode: Int) {
|
||||
when (keyCode) {
|
||||
Keyboard.KEYCODE_DELETE -> {}
|
||||
@@ -267,7 +272,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
if (entryInfoKey != null) {
|
||||
currentInputConnection.commitText(entryInfoKey!!.url, 1)
|
||||
}
|
||||
actionTabAutomatically()
|
||||
actionGoAutomatically()
|
||||
}
|
||||
KEY_FIELDS -> {
|
||||
if (entryInfoKey != null) {
|
||||
|
||||
@@ -234,7 +234,7 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
|
||||
|
||||
fun replaceBase32Chars(parameter: String): String {
|
||||
// Add padding '=' at end if not Base32 length
|
||||
var parameterNewSize = parameter.toUpperCase(Locale.ENGLISH).removeSpaceChars()
|
||||
var parameterNewSize = parameter.uppercase(Locale.ENGLISH).removeSpaceChars()
|
||||
while (parameterNewSize.length % 8 != 0) {
|
||||
parameterNewSize += '='
|
||||
}
|
||||
@@ -264,7 +264,7 @@ enum class OtpTokenType {
|
||||
|
||||
companion object {
|
||||
fun getFromString(tokenType: String): OtpTokenType {
|
||||
return when (tokenType.toLowerCase(Locale.ENGLISH)) {
|
||||
return when (tokenType.lowercase(Locale.ENGLISH)) {
|
||||
"s", "steam" -> STEAM
|
||||
"hotp" -> RFC4226
|
||||
else -> RFC6238
|
||||
|
||||
@@ -143,7 +143,7 @@ object OtpEntryFields {
|
||||
if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) {
|
||||
val uri = Uri.parse(otpPlainText.removeSpaceChars())
|
||||
|
||||
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) {
|
||||
if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.lowercase(Locale.ENGLISH)) {
|
||||
Log.e(TAG, "Invalid or missing scheme in uri")
|
||||
return false
|
||||
}
|
||||
@@ -309,7 +309,7 @@ object OtpEntryFields {
|
||||
}
|
||||
if (algorithmField != null) {
|
||||
otpElement.algorithm =
|
||||
when (algorithmField.toUpperCase(Locale.ENGLISH)) {
|
||||
when (algorithmField.uppercase(Locale.ENGLISH)) {
|
||||
TIMEOTP_ALGORITHM_SHA1_VALUE -> HashAlgorithm.SHA1
|
||||
TIMEOTP_ALGORITHM_SHA256_VALUE -> HashAlgorithm.SHA256
|
||||
TIMEOTP_ALGORITHM_SHA512_VALUE -> HashAlgorithm.SHA512
|
||||
@@ -417,7 +417,7 @@ object OtpEntryFields {
|
||||
val output = HashMap<String, String>()
|
||||
for (element in elements) {
|
||||
val pair = element.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
output[pair[0].toLowerCase(Locale.ENGLISH)] = pair[1]
|
||||
output[pair[0].lowercase(Locale.ENGLISH)] = pair[1]
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.kunzisoft.keepass.receivers
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.utils.DexUtil
|
||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||
|
||||
class DexModeReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val enabled = when (intent?.action) {
|
||||
"android.app.action.ENTER_KNOX_DESKTOP_MODE" -> {
|
||||
Log.i(TAG, "Entered DeX mode")
|
||||
false
|
||||
}
|
||||
"android.app.action.EXIT_KNOX_DESKTOP_MODE" -> {
|
||||
Log.i(TAG, "Left DeX mode")
|
||||
true
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
|
||||
MagikeyboardUtil.setEnabled(context!!, enabled)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = DexModeReceiver::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.kunzisoft.keepass.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
@@ -50,11 +52,20 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
||||
mTempCipherDao = ArrayList()
|
||||
}
|
||||
|
||||
// It's simpler to use pendingIntent to perform REMOVE_ADVANCED_UNLOCK_KEY_ACTION
|
||||
// because can be directly broadcast to another module or app
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
|
||||
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
|
||||
4577, Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION), 0)
|
||||
4577,
|
||||
Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
})
|
||||
val biometricUnlockEnabled = PreferencesUtil.isBiometricUnlockEnable(this)
|
||||
val notificationBuilder = buildNewNotification().apply {
|
||||
setSmallIcon(if (biometricUnlockEnabled) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.ContentResolver
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -188,20 +189,30 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
||||
private fun newNotification(attachmentNotification: AttachmentNotification) {
|
||||
|
||||
val pendingContentIntent = PendingIntent.getActivity(this,
|
||||
0,
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(attachmentNotification.uri,
|
||||
contentResolver.getType(attachmentNotification.uri))
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
0,
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(attachmentNotification.uri,
|
||||
contentResolver.getType(attachmentNotification.uri))
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
}
|
||||
)
|
||||
|
||||
val pendingDeleteIntent = PendingIntent.getService(this,
|
||||
0,
|
||||
Intent(this, AttachmentFileNotificationService::class.java).apply {
|
||||
// No action to delete the service
|
||||
putExtra(FILE_URI_KEY, attachmentNotification.uri)
|
||||
}, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
}, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
}
|
||||
)
|
||||
|
||||
val fileName = UriUtil.getFileData(this, attachmentNotification.uri)?.name
|
||||
?: attachmentNotification.uri.path
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.services
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
@@ -112,7 +113,13 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
putParcelableArrayListExtra(EXTRA_CLIPBOARD_FIELDS, fieldsToAdd)
|
||||
}
|
||||
return PendingIntent.getService(
|
||||
this, 0, copyIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
this, 0, copyIntent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun newNotification(title: String?, fieldsToAdd: ArrayList<ClipboardEntryNotificationField>) {
|
||||
@@ -162,7 +169,13 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
val cleanIntent = Intent(this, ClipboardEntryNotificationService::class.java)
|
||||
cleanIntent.action = ACTION_CLEAN_CLIPBOARD
|
||||
val cleanPendingIntent = PendingIntent.getService(
|
||||
this, 0, cleanIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
this, 0, cleanIntent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
)
|
||||
builder.setDeleteIntent(cleanPendingIntent)
|
||||
|
||||
//Get settings
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.media.app.NotificationCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
@@ -407,11 +408,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
this,
|
||||
0,
|
||||
Intent(this, GroupActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
)
|
||||
val pendingDeleteIntent = PendingIntent.getBroadcast(
|
||||
this,
|
||||
4576, Intent(LOCK_ACTION), 0
|
||||
4576,
|
||||
Intent(LOCK_ACTION),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
// Add actions in notifications
|
||||
notificationBuilder.apply {
|
||||
@@ -420,9 +431,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
// Unfortunately swipe is disabled in lollipop+
|
||||
setDeleteIntent(pendingDeleteIntent)
|
||||
addAction(
|
||||
R.drawable.ic_lock_white_24dp, getString(R.string.lock),
|
||||
R.drawable.ic_lock_database_white_32dp, getString(R.string.lock),
|
||||
pendingDeleteIntent
|
||||
)
|
||||
// Won't work with Xiaomi and Kitkat
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
||||
setStyle(
|
||||
NotificationCompat.MediaStyle()
|
||||
.setShowActionsInCompactView(0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.services
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.preference.PreferenceManager
|
||||
@@ -93,7 +94,13 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
||||
val deleteIntent = Intent(this, KeyboardEntryNotificationService::class.java).apply {
|
||||
action = ACTION_CLEAN_KEYBOARD_ENTRY
|
||||
}
|
||||
pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
pendingDeleteIntent = PendingIntent.getService(this, 0, deleteIntent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
)
|
||||
|
||||
val builder = buildNewNotification()
|
||||
.setSmallIcon(R.drawable.notification_ic_keyboard_key_24dp)
|
||||
|
||||
@@ -57,6 +57,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
if (dialogFragment != null) {
|
||||
@Suppress("DEPRECATION")
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ class MagikeyboardSettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
if (dialogFragment != null) {
|
||||
@Suppress("DEPRECATION")
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||
import com.kunzisoft.keepass.education.Education
|
||||
@@ -157,7 +156,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
|
||||
intent.data = Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService")
|
||||
Log.d(javaClass.name, "Autofill enable service: intent=$intent")
|
||||
startActivityForResult(intent, REQUEST_CODE_AUTOFILL)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
Log.d(javaClass.name, "Autofill service already enabled.")
|
||||
}
|
||||
@@ -366,26 +365,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
) { _, _ ->
|
||||
validate?.invoke()
|
||||
deleteKeysAlertDialog?.setOnDismissListener(null)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
AdvancedUnlockManager.deleteEntryKeyInKeystoreForBiometric(
|
||||
activity,
|
||||
object : AdvancedUnlockManager.AdvancedUnlockErrorCallback {
|
||||
fun showException(e: Exception) {
|
||||
Toast.makeText(context,
|
||||
getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
|
||||
override fun onGenericException(e: Exception) {
|
||||
showException(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
|
||||
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
||||
}
|
||||
.setNegativeButton(resources.getString(android.R.string.cancel)
|
||||
) { _, _ ->}
|
||||
@@ -494,6 +474,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
|
||||
if (dialogFragment != null) {
|
||||
@Suppress("DEPRECATION")
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||
}
|
||||
@@ -533,7 +514,6 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CODE_AUTOFILL = 5201
|
||||
private const val TAG_PREF_FRAGMENT = "TAG_PREF_FRAGMENT"
|
||||
|
||||
var DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
|
||||
|
||||
@@ -57,7 +57,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
|
||||
private var dbNamePref: InputTextPreference? = null
|
||||
private var dbDescriptionPref: InputTextPreference? = null
|
||||
private var dbDefaultUsername: InputTextPreference? = null
|
||||
private var dbDefaultUsernamePref: InputTextPreference? = null
|
||||
private var dbCustomColorPref: DialogColorPreference? = null
|
||||
private var dbDataCompressionPref: Preference? = null
|
||||
private var recycleBinGroupPref: DialogListExplanationPreference? = null
|
||||
@@ -164,11 +164,11 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
|
||||
// Database default username
|
||||
dbDefaultUsername = findPreference(getString(R.string.database_default_username_key))
|
||||
dbDefaultUsernamePref = findPreference(getString(R.string.database_default_username_key))
|
||||
if (database.allowDefaultUsername) {
|
||||
dbDefaultUsername?.summary = database.defaultUsername
|
||||
dbDefaultUsernamePref?.summary = database.defaultUsername
|
||||
} else {
|
||||
dbDefaultUsername?.isEnabled = false
|
||||
dbDefaultUsernamePref?.isEnabled = false
|
||||
// TODO dbGeneralPrefCategory?.removePreference(dbDefaultUsername)
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
mDatabase?.defaultUsername = oldDefaultUsername
|
||||
oldDefaultUsername
|
||||
}
|
||||
dbDefaultUsername?.summary = defaultUsernameToShow
|
||||
dbDefaultUsernamePref?.summary = defaultUsernameToShow
|
||||
}
|
||||
DatabaseTaskNotificationService.ACTION_DATABASE_UPDATE_COLOR_TASK -> {
|
||||
val oldColor = data.getString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY)!!
|
||||
@@ -632,6 +632,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
|
||||
if (dialogFragment != null && !mDatabaseReadOnly) {
|
||||
@Suppress("DEPRECATION")
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
dialogFragment.show(parentFragmentManager, TAG_PREF_FRAGMENT)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ open class SettingsActivity
|
||||
|
||||
private var backupManager: BackupManager? = null
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
private var appPropertiesFileCreationRequestCode: Int? = null
|
||||
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
@@ -64,6 +63,41 @@ open class SettingsActivity
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { selectedFileUri ->
|
||||
// Import app properties result
|
||||
try {
|
||||
selectedFileUri?.let { uri ->
|
||||
val appProperties = Properties()
|
||||
contentResolver?.openInputStream(uri)?.use { inputStream ->
|
||||
appProperties.load(inputStream)
|
||||
}
|
||||
PreferencesUtil.setAppProperties(this, appProperties)
|
||||
|
||||
// Restart the current activity
|
||||
reloadActivity()
|
||||
Toast.makeText(this, R.string.success_import_app_properties, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, "Unable to import app properties", e)
|
||||
}
|
||||
}
|
||||
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
||||
// Export app properties result
|
||||
try {
|
||||
createdFileUri?.let { uri ->
|
||||
contentResolver?.openOutputStream(uri)?.use { outputStream ->
|
||||
PreferencesUtil
|
||||
.getAppProperties(this)
|
||||
.store(outputStream, getString(R.string.description_app_properties))
|
||||
}
|
||||
Toast.makeText(this, R.string.success_export_app_properties, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
|
||||
Log.e(DatabaseLockActivity.TAG, "Unable to export app properties", e)
|
||||
}
|
||||
}
|
||||
|
||||
if (savedInstanceState?.getString(TITLE_KEY).isNullOrEmpty())
|
||||
toolbar?.setTitle(R.string.settings)
|
||||
@@ -217,54 +251,10 @@ open class SettingsActivity
|
||||
}
|
||||
|
||||
fun exportAppProperties() {
|
||||
appPropertiesFileCreationRequestCode = mExternalFileHelper?.createDocument(getString(R.string.app_properties_file_name,
|
||||
mExternalFileHelper?.createDocument(getString(R.string.app_properties_file_name,
|
||||
DateTime.now().toLocalDateTime().toString("yyyy-MM-dd'_'HH-mm")))
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// Import app properties result
|
||||
try {
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { selectedFileUri ->
|
||||
selectedFileUri?.let { uri ->
|
||||
val appProperties = Properties()
|
||||
contentResolver?.openInputStream(uri)?.use { inputStream ->
|
||||
appProperties.load(inputStream)
|
||||
}
|
||||
PreferencesUtil.setAppProperties(this, appProperties)
|
||||
|
||||
// Restart the current activity
|
||||
reloadActivity()
|
||||
Toast.makeText(this, R.string.success_import_app_properties, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, R.string.error_import_app_properties, Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, "Unable to import app properties", e)
|
||||
}
|
||||
|
||||
// Export app properties result
|
||||
try {
|
||||
if (requestCode == appPropertiesFileCreationRequestCode) {
|
||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||
createdFileUri?.let { uri ->
|
||||
contentResolver?.openOutputStream(uri)?.use { outputStream ->
|
||||
PreferencesUtil
|
||||
.getAppProperties(this)
|
||||
.store(outputStream, getString(R.string.description_app_properties))
|
||||
}
|
||||
Toast.makeText(this, R.string.success_export_app_properties, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
appPropertiesFileCreationRequestCode = null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, R.string.error_export_app_properties, Toast.LENGTH_LONG).show()
|
||||
Log.e(DatabaseLockActivity.TAG, "Unable to export app properties", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
|
||||
@@ -64,11 +64,13 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
|
||||
|
||||
private fun durationToDaysHoursMinutesSeconds(duration: Long) {
|
||||
if (duration < 0) {
|
||||
mEnabled = false
|
||||
mDays = 0
|
||||
mHours = 0
|
||||
mMinutes = 0
|
||||
mSeconds = 0
|
||||
} else {
|
||||
mEnabled = true
|
||||
mDays = (duration / (24L * 60L * 60L * 1000L)).toInt()
|
||||
val daysMilliseconds = mDays * 24L * 60L * 60L * 1000L
|
||||
mHours = ((duration - daysMilliseconds) / (60L * 60L * 1000L)).toInt()
|
||||
@@ -125,10 +127,9 @@ class DurationDialogFragmentCompat : InputPreferenceDialogFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
mEnabled = isSwitchActivated()
|
||||
setSwitchAction({ isChecked ->
|
||||
mEnabled = isChecked
|
||||
}, mDays + mHours + mMinutes + mSeconds > 0)
|
||||
}, mEnabled)
|
||||
|
||||
assignValuesInViews()
|
||||
}
|
||||
|
||||
@@ -31,9 +31,13 @@ abstract class ActionRunnable: Runnable {
|
||||
var result: Result = Result()
|
||||
|
||||
override fun run() {
|
||||
onStartRun()
|
||||
onActionRun()
|
||||
onFinishRun()
|
||||
try {
|
||||
onStartRun()
|
||||
onActionRun()
|
||||
onFinishRun()
|
||||
} catch (runException: Exception) {
|
||||
setError(runException)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onStartRun()
|
||||
|
||||
@@ -43,9 +43,14 @@ object TimeoutHelper {
|
||||
|
||||
private fun getLockPendingIntent(context: Context): PendingIntent {
|
||||
return PendingIntent.getBroadcast(context.applicationContext,
|
||||
REQUEST_ID,
|
||||
Intent(LOCK_ACTION),
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
REQUEST_ID,
|
||||
Intent(LOCK_ACTION),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,11 +60,16 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
|
||||
mLockPendingIntent = PendingIntent.getBroadcast(context,
|
||||
4575,
|
||||
Intent(intent).apply {
|
||||
action = LOCK_ACTION
|
||||
},
|
||||
0)
|
||||
4575,
|
||||
Intent(intent).apply {
|
||||
action = LOCK_ACTION
|
||||
},
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
// Launch the effective action after a small time
|
||||
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
|
||||
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
|
||||
|
||||
27
app/src/main/java/com/kunzisoft/keepass/utils/DexUtil.kt
Normal file
27
app/src/main/java/com/kunzisoft/keepass/utils/DexUtil.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.kunzisoft.keepass.utils
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
|
||||
object DexUtil {
|
||||
private val TAG = DexUtil::class.java.name
|
||||
|
||||
// Determine if the current environment is in DeX mode. Always returns false on non-Samsung
|
||||
// devices.
|
||||
fun isDexMode(config: Configuration): Boolean {
|
||||
// This is the documented way to check this: https://developer.samsung.com/samsung-dex/modify-optimizing.html
|
||||
return try {
|
||||
val configClass = config.javaClass
|
||||
val enabledConstant = configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass)
|
||||
val enabledField = configClass.getField("semDesktopModeEnabled").getInt(config)
|
||||
val isEnabled = enabledConstant == enabledField
|
||||
|
||||
Log.d(TAG, "DeX currently enabled: $isEnabled")
|
||||
|
||||
isEnabled
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Failed to check for DeX mode; likely not Samsung device: $e")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.kunzisoft.keepass.utils
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
|
||||
object MagikeyboardUtil {
|
||||
private val TAG = MagikeyboardUtil::class.java.name
|
||||
|
||||
// Set whether MagikeyboardService is enabled. This change is persistent and survives app
|
||||
// crashes and device restarts. The state is changed immediately and does not require an app
|
||||
// restart.
|
||||
fun setEnabled(context: Context, enabled: Boolean) {
|
||||
val componentState = if (enabled) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
} else {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
||||
}
|
||||
|
||||
Log.d(TAG, "Setting service state: $enabled")
|
||||
|
||||
val component = ComponentName(context, MagikeyboardService::class.java)
|
||||
context.packageManager.setComponentEnabledSetting(component, componentState, PackageManager.DONT_KILL_APP)
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ object UriUtil {
|
||||
|
||||
private fun isFileScheme(fileUri: Uri): Boolean {
|
||||
val scheme = fileUri.scheme
|
||||
if (scheme == null || scheme.isEmpty() || scheme.toLowerCase(Locale.ENGLISH) == "file") {
|
||||
if (scheme == null || scheme.isEmpty() || scheme.lowercase(Locale.ENGLISH) == "file") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -94,7 +94,7 @@ object UriUtil {
|
||||
|
||||
private fun isContentScheme(fileUri: Uri): Boolean {
|
||||
val scheme = fileUri.scheme
|
||||
if (scheme != null && scheme.toLowerCase(Locale.ENGLISH) == "content") {
|
||||
if (scheme != null && scheme.lowercase(Locale.ENGLISH) == "content") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -104,10 +104,12 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
||||
return unlockMessageTextView?.text?.toString() ?: ""
|
||||
}
|
||||
set(value) {
|
||||
if (value == null || value.isEmpty())
|
||||
if (value == null || value.isEmpty()) {
|
||||
unlockMessageTextView?.visibility = GONE
|
||||
else
|
||||
} else {
|
||||
unlockMessageTextView?.visibility = VISIBLE
|
||||
stopIconViewAnimation()
|
||||
}
|
||||
unlockMessageTextView?.text = value ?: ""
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,6 @@ class DateTimeFieldView @JvmOverloads constructor(context: Context,
|
||||
|
||||
private var mDefault: DateInstant = DateInstant.NEVER_EXPIRES
|
||||
|
||||
var setOnDateClickListener: ((DateInstant) -> Unit)? = null
|
||||
|
||||
init {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_date_time, this)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.TextView
|
||||
@@ -9,6 +10,9 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable.Creator
|
||||
|
||||
|
||||
class KeyFileSelectionView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@@ -54,4 +58,45 @@ class KeyFileSelectionView @JvmOverloads constructor(context: Context,
|
||||
UriUtil.getFileData(context, value)?.name ?: value.path
|
||||
} ?: ""
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superState = super.onSaveInstanceState()
|
||||
val saveState = SavedState(superState)
|
||||
saveState.mUri = this.mUri
|
||||
return saveState
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||
if (state !is SavedState) {
|
||||
super.onRestoreInstanceState(state)
|
||||
return
|
||||
}
|
||||
super.onRestoreInstanceState(state.superState)
|
||||
this.mUri = state.mUri
|
||||
}
|
||||
|
||||
internal class SavedState : BaseSavedState {
|
||||
var mUri: Uri? = null
|
||||
|
||||
constructor(superState: Parcelable?) : super(superState) {}
|
||||
|
||||
private constructor(parcel: Parcel) : super(parcel) {
|
||||
mUri = parcel.readParcelable(Uri::class.java.classLoader)
|
||||
}
|
||||
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
super.writeToParcel(out, flags)
|
||||
out.writeParcelable(mUri, flags)
|
||||
}
|
||||
|
||||
companion object CREATOR : Creator<SavedState> {
|
||||
override fun createFromParcel(parcel: Parcel): SavedState {
|
||||
return SavedState(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<SavedState?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,8 @@ abstract class TemplateAbstractView<
|
||||
if (mTemplate != template) {
|
||||
mTemplate = template
|
||||
if (mEntryInfo != null) {
|
||||
populateEntryInfoWithViews(true)
|
||||
populateEntryInfoWithViews(templateFieldNotEmpty = true,
|
||||
retrieveDefaultValues = false)
|
||||
}
|
||||
buildTemplateAndPopulateInfo()
|
||||
clearFocus()
|
||||
@@ -203,9 +204,7 @@ abstract class TemplateAbstractView<
|
||||
setNumberLines(20)
|
||||
},
|
||||
TemplateAttributeAction.CUSTOM_EDITION
|
||||
).apply {
|
||||
default = field.protectedValue.stringValue
|
||||
}
|
||||
)
|
||||
return buildViewForTemplateField(customFieldTemplateAttribute, field, FIELD_CUSTOM_TAG)
|
||||
}
|
||||
|
||||
@@ -275,22 +274,26 @@ abstract class TemplateAbstractView<
|
||||
templateAttribute: TemplateAttribute,
|
||||
entryInfoValue: String,
|
||||
showEmptyFields: Boolean) {
|
||||
var fieldView: TEntryFieldView? = findViewWithTag(fieldTag)
|
||||
if (!showEmptyFields && entryInfoValue.isEmpty()) {
|
||||
fieldView?.isFieldVisible = false
|
||||
} else if (fieldView == null && entryInfoValue.isNotEmpty()) {
|
||||
// Add new not referenced view if standard field not in template
|
||||
fieldView = buildViewForNotReferencedField(
|
||||
Field(templateAttribute.label,
|
||||
ProtectedString(templateAttribute.protected, "")),
|
||||
templateAttribute
|
||||
) as? TEntryFieldView?
|
||||
fieldView?.let {
|
||||
addNotReferencedView(it as View)
|
||||
try {
|
||||
var fieldView: TEntryFieldView? = findViewWithTag(fieldTag)
|
||||
if (!showEmptyFields && entryInfoValue.isEmpty()) {
|
||||
fieldView?.isFieldVisible = false
|
||||
} else if (fieldView == null && entryInfoValue.isNotEmpty()) {
|
||||
// Add new not referenced view if standard field not in template
|
||||
fieldView = buildViewForNotReferencedField(
|
||||
Field(templateAttribute.label,
|
||||
ProtectedString(templateAttribute.protected, "")),
|
||||
templateAttribute
|
||||
) as? TEntryFieldView?
|
||||
fieldView?.let {
|
||||
addNotReferencedView(it as View)
|
||||
}
|
||||
}
|
||||
fieldView?.value = entryInfoValue
|
||||
fieldView?.applyFontVisibility(mFontInVisibility)
|
||||
} catch(e: Exception) {
|
||||
Log.e(TAG, "Unable to populate entry field view", e)
|
||||
}
|
||||
fieldView?.value = entryInfoValue
|
||||
fieldView?.applyFontVisibility(mFontInVisibility)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -299,22 +302,25 @@ abstract class TemplateAbstractView<
|
||||
expires: Boolean,
|
||||
expiryTime: DateInstant,
|
||||
showEmptyFields: Boolean) {
|
||||
|
||||
var fieldView: TDateTimeView? = findViewWithTag(fieldTag)
|
||||
if (!showEmptyFields && !expires) {
|
||||
fieldView?.isFieldVisible = false
|
||||
} else if (fieldView == null && expires) {
|
||||
fieldView = buildViewForNotReferencedField(
|
||||
Field(templateAttribute.label,
|
||||
ProtectedString(templateAttribute.protected, "")),
|
||||
templateAttribute
|
||||
) as? TDateTimeView?
|
||||
fieldView?.let {
|
||||
addNotReferencedView(it as View)
|
||||
try {
|
||||
var fieldView: TDateTimeView? = findViewWithTag(fieldTag)
|
||||
if (!showEmptyFields && !expires) {
|
||||
fieldView?.isFieldVisible = false
|
||||
} else if (fieldView == null && expires) {
|
||||
fieldView = buildViewForNotReferencedField(
|
||||
Field(templateAttribute.label,
|
||||
ProtectedString(templateAttribute.protected, "")),
|
||||
templateAttribute
|
||||
) as? TDateTimeView?
|
||||
fieldView?.let {
|
||||
addNotReferencedView(it as View)
|
||||
}
|
||||
}
|
||||
fieldView?.activation = expires
|
||||
fieldView?.dateTime = expiryTime
|
||||
} catch(e: Exception) {
|
||||
Log.e(TAG, "Unable to populate date time view", e)
|
||||
}
|
||||
fieldView?.activation = expires
|
||||
fieldView?.dateTime = expiryTime
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,7 +389,8 @@ abstract class TemplateAbstractView<
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
protected open fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean) {
|
||||
protected open fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
|
||||
retrieveDefaultValues: Boolean) {
|
||||
if (mEntryInfo == null)
|
||||
mEntryInfo = EntryInfo()
|
||||
|
||||
@@ -422,11 +429,12 @@ abstract class TemplateAbstractView<
|
||||
mEntryInfo?.notes = it
|
||||
}
|
||||
|
||||
retrieveCustomFieldsFromView(templateFieldNotEmpty)
|
||||
retrieveCustomFieldsFromView(templateFieldNotEmpty, retrieveDefaultValues)
|
||||
}
|
||||
|
||||
fun getEntryInfo(): EntryInfo {
|
||||
populateEntryInfoWithViews(true)
|
||||
populateEntryInfoWithViews(templateFieldNotEmpty = true,
|
||||
retrieveDefaultValues = true)
|
||||
return mEntryInfo ?: EntryInfo()
|
||||
}
|
||||
|
||||
@@ -472,23 +480,31 @@ abstract class TemplateAbstractView<
|
||||
return mViewFields.indexOfFirst { it.field.name.equals(name, true) }
|
||||
}
|
||||
|
||||
private fun retrieveCustomFieldsFromView(templateFieldNotEmpty: Boolean = false) {
|
||||
private fun retrieveCustomFieldsFromView(templateFieldNotEmpty: Boolean = false,
|
||||
retrieveDefaultValues: Boolean = false) {
|
||||
mEntryInfo?.customFields = mViewFields.mapNotNull {
|
||||
getCustomField(it.field.name, templateFieldNotEmpty)
|
||||
getCustomField(it.field.name, templateFieldNotEmpty, retrieveDefaultValues)
|
||||
}.toMutableList()
|
||||
}
|
||||
|
||||
protected fun getCustomField(fieldName: String): Field {
|
||||
return getCustomField(fieldName, false)
|
||||
?: Field(fieldName, ProtectedString(false))
|
||||
return getCustomField(fieldName,
|
||||
templateFieldNotEmpty = false,
|
||||
retrieveDefaultValues = false
|
||||
) ?: Field(fieldName, ProtectedString(false))
|
||||
}
|
||||
|
||||
private fun getCustomField(fieldName: String, templateFieldNotEmpty: Boolean): Field? {
|
||||
private fun getCustomField(fieldName: String,
|
||||
templateFieldNotEmpty: Boolean,
|
||||
retrieveDefaultValues: Boolean): Field? {
|
||||
getViewFieldByName(fieldName)?.let { fieldId ->
|
||||
val editView: View? = fieldId.view
|
||||
val editView: View = fieldId.view
|
||||
if (editView is GenericFieldView) {
|
||||
// Do not return field with a default value
|
||||
val defaultViewValue = if (editView.value == editView.default) "" else editView.value
|
||||
val defaultViewValue =
|
||||
if (retrieveDefaultValues || editView.value != editView.default) {
|
||||
editView.value
|
||||
} else ""
|
||||
if (!templateFieldNotEmpty
|
||||
|| (editView.tag == FIELD_CUSTOM_TAG && defaultViewValue.isNotEmpty())) {
|
||||
return Field(
|
||||
@@ -634,7 +650,8 @@ abstract class TemplateAbstractView<
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superSave = super.onSaveInstanceState()
|
||||
val saveState = SavedState(superSave)
|
||||
populateEntryInfoWithViews(false)
|
||||
populateEntryInfoWithViews(templateFieldNotEmpty = false,
|
||||
retrieveDefaultValues = false)
|
||||
saveState.template = this.mTemplate
|
||||
saveState.entryInfo = this.mEntryInfo
|
||||
onSaveEntryInstanceState(saveState)
|
||||
|
||||
@@ -64,6 +64,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
||||
TextEditFieldView(it).apply {
|
||||
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
|
||||
setProtection(field.protectedValue.isProtected)
|
||||
default = templateAttribute.default
|
||||
setMaxChars(templateAttribute.options.getNumberChars())
|
||||
setMaxLines(templateAttribute.options.getNumberLines())
|
||||
setActionClick(templateAttribute, field, this)
|
||||
@@ -79,7 +80,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
||||
return context?.let {
|
||||
TextSelectFieldView(it).apply {
|
||||
setItems(templateAttribute.options.getListItems())
|
||||
default = field.protectedValue.stringValue
|
||||
default = templateAttribute.default
|
||||
setActionClick(templateAttribute, field, this)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
|
||||
@@ -198,8 +199,9 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
||||
return super.populateViewsWithEntryInfo(showEmptyFields)
|
||||
}
|
||||
|
||||
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean) {
|
||||
super.populateEntryInfoWithViews(templateFieldNotEmpty)
|
||||
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
|
||||
retrieveDefaultValues: Boolean) {
|
||||
super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues)
|
||||
mEntryInfo?.otpModel = OtpEntryFields.parseFields { key ->
|
||||
getCustomField(key).protectedValue.toString()
|
||||
}?.otpModel
|
||||
|
||||
@@ -53,7 +53,6 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
||||
label = templateAttribute.alias
|
||||
?: TemplateField.getLocalizedName(context, field.name)
|
||||
setMaxChars(templateAttribute.options.getNumberChars())
|
||||
setMaxLines(templateAttribute.options.getNumberLines())
|
||||
// TODO Linkify
|
||||
value = field.protectedValue.stringValue
|
||||
// Here the value is often empty
|
||||
|
||||
@@ -187,6 +187,6 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||
|
||||
companion object {
|
||||
const val MAX_CHARS_LIMIT = Integer.MAX_VALUE
|
||||
const val MAX_LINES_LIMIT = 40
|
||||
const val MAX_LINES_LIMIT = Integer.MAX_VALUE
|
||||
}
|
||||
}
|
||||
@@ -214,18 +214,6 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
fun setMaxLines(numberLines: Int) {
|
||||
when {
|
||||
numberLines <= 0 -> {
|
||||
valueView.maxLines = MAX_LINES_LIMIT
|
||||
}
|
||||
else -> {
|
||||
val lines = if (numberLines > MAX_LINES_LIMIT) MAX_LINES_LIMIT else numberLines
|
||||
valueView.maxLines = lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean = false) {
|
||||
showButton.isVisible = protection
|
||||
showButton.isSelected = hiddenProtectedValue
|
||||
@@ -343,6 +331,5 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
||||
|
||||
companion object {
|
||||
const val MAX_CHARS_LIMIT = Integer.MAX_VALUE
|
||||
const val MAX_LINES_LIMIT = 40
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +194,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
|
||||
get() = valueSpinnerAdapter.getItem(mDefaultPosition)
|
||||
set(value) {
|
||||
mDefaultPosition = valueSpinnerAdapter.getPosition(value)
|
||||
valueSpinnerAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun setOnActionClickListener(onActionClickListener: OnClickListener?,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.kunzisoft.keepass.viewmodels
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class AdvancedUnlockViewModel : ViewModel() {
|
||||
|
||||
var allowAutoOpenBiometricPrompt : Boolean = true
|
||||
var deviceCredentialAuthSucceeded: Boolean? = null
|
||||
|
||||
val onInitAdvancedUnlockModeRequested : LiveData<Void?> get() = _onInitAdvancedUnlockModeRequested
|
||||
private val _onInitAdvancedUnlockModeRequested = SingleLiveEvent<Void?>()
|
||||
|
||||
val onUnlockAvailabilityCheckRequested : LiveData<Void?> get() = _onUnlockAvailabilityCheckRequested
|
||||
private val _onUnlockAvailabilityCheckRequested = SingleLiveEvent<Void?>()
|
||||
|
||||
val onDatabaseFileLoaded : LiveData<Uri?> get() = _onDatabaseFileLoaded
|
||||
private val _onDatabaseFileLoaded = SingleLiveEvent<Uri?>()
|
||||
|
||||
fun initAdvancedUnlockMode() {
|
||||
_onInitAdvancedUnlockModeRequested.call()
|
||||
}
|
||||
|
||||
fun checkUnlockAvailability() {
|
||||
_onUnlockAvailabilityCheckRequested.call()
|
||||
}
|
||||
|
||||
fun databaseFileLoaded(databaseUri: Uri?) {
|
||||
_onDatabaseFileLoaded.value = databaseUri
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.kunzisoft.keepass.viewmodels
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
@@ -30,6 +31,10 @@ class IconPickerViewModel: ViewModel() {
|
||||
MutableLiveData<IconCustomState>()
|
||||
}
|
||||
|
||||
val customIconUpdated : MutableLiveData<IconCustomState> by lazy {
|
||||
MutableLiveData<IconCustomState>()
|
||||
}
|
||||
|
||||
fun pickStandardIcon(icon: IconImageStandard) {
|
||||
standardIconPicked.value = icon
|
||||
}
|
||||
@@ -54,6 +59,10 @@ class IconPickerViewModel: ViewModel() {
|
||||
customIconRemoved.value = customIcon
|
||||
}
|
||||
|
||||
fun updateCustomIcon(customIcon: IconCustomState) {
|
||||
customIconUpdated.value = customIcon
|
||||
}
|
||||
|
||||
data class IconCustomState(var iconCustom: IconImageCustom? = null,
|
||||
var error: Boolean = true,
|
||||
var errorStringId: Int = -1,
|
||||
|
||||
@@ -144,7 +144,7 @@ internal class PublicSuffixListData(
|
||||
}
|
||||
|
||||
companion object {
|
||||
val WILDCARD_LABEL = byteArrayOf('*'.toByte())
|
||||
val WILDCARD_LABEL = byteArrayOf('*'.code.toByte())
|
||||
val PREVAILING_RULE = listOf("*")
|
||||
val EMPTY_RULE = listOf<String>()
|
||||
const val EXCEPTION_MARKER = '!'
|
||||
|
||||
@@ -36,7 +36,7 @@ internal fun ByteArray.binarySearch(labels: List<ByteArray>, labelIndex: Int): S
|
||||
while (true) {
|
||||
val byte0 = if (expectDot) {
|
||||
expectDot = false
|
||||
'.'.toByte()
|
||||
'.'.code.toByte()
|
||||
} else {
|
||||
labels[currentLabelIndex][currentLabelByteIndex] and BITMASK
|
||||
}
|
||||
@@ -103,7 +103,7 @@ internal fun ByteArray.binarySearch(labels: List<ByteArray>, labelIndex: Int): S
|
||||
*/
|
||||
private fun ByteArray.findStartOfLineFromIndex(start: Int): Int {
|
||||
var index = start
|
||||
while (index > -1 && this[index] != '\n'.toByte()) {
|
||||
while (index > -1 && this[index] != '\n'.code.toByte()) {
|
||||
index--
|
||||
}
|
||||
index++
|
||||
@@ -115,7 +115,7 @@ private fun ByteArray.findStartOfLineFromIndex(start: Int): Int {
|
||||
*/
|
||||
private fun ByteArray.findEndOfLineFromIndex(start: Int): Int {
|
||||
var end = 1
|
||||
while (this[start + end] != '\n'.toByte()) {
|
||||
while (this[start + end] != '\n'.code.toByte()) {
|
||||
end++
|
||||
}
|
||||
return end
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorAccentLight" android:state_pressed="true" />
|
||||
<item android:color="@color/white_grey_darker" android:state_enabled="false" />
|
||||
<item android:color="?attr/colorAccent" android:state_enabled="true" />
|
||||
</selector>
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:color="@color/white"
|
||||
tools:targetApi="lollipop">
|
||||
<item>
|
||||
<shape>
|
||||
<corners
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="40dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"/>
|
||||
<solid android:color="?attr/colorAccent"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:color="@color/white"
|
||||
tools:targetApi="lollipop">
|
||||
<item>
|
||||
<shape>
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="?attr/textColorInverse" />
|
||||
<corners
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="40dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"/>
|
||||
<solid
|
||||
android:color="@color/transparent"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:state_pressed="true">
|
||||
<shape>
|
||||
<corners
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="40dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"/>
|
||||
<padding
|
||||
android:left="4dp"
|
||||
android:right="12dp"
|
||||
android:top="18dp"
|
||||
android:bottom="8dp"/>
|
||||
<solid android:color="@color/orange_light"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<corners
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="40dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"/>
|
||||
<padding
|
||||
android:left="4dp"
|
||||
android:right="12dp"
|
||||
android:top="18dp"
|
||||
android:bottom="8dp"/>
|
||||
<solid android:color="@color/orange"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/white">
|
||||
<item>
|
||||
<shape>
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/white_grey" />
|
||||
<corners
|
||||
android:topLeftRadius="0dp"
|
||||
android:topRightRadius="40dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="0dp"/>
|
||||
<solid
|
||||
android:color="@color/transparent"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
10
app/src/main/res/drawable/ic_lock_database_white_32dp.xml
Normal file
10
app/src/main/res/drawable/ic_lock_database_white_32dp.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M 11 2.0566406 C 6.762335 2.4220229 3.0067094 5.7987155 2.203125 9.9785156 C 1.3601753 13.960549 3.1781148 18.394742 6.7089844 20.480469 C 9.6237318 22.368157 13.514425 22.492178 16.582031 20.892578 C 17.959775 20.180473 19.316015 19.099467 20.087891 17.808594 L 18.402344 16.791016 C 16.277892 19.724364 12.039121 20.844605 8.7519531 19.306641 C 5.481064 17.911182 3.4461934 14.150571 4.109375 10.648438 C 4.6649664 7.2806969 7.5784749 4.4226117 11 4.0839844 L 11 2.0566406 z M 13 2.0644531 L 13 4.09375 C 16.367309 4.4801387 19.308002 7.2166099 19.861328 10.574219 C 20.123352 12.069186 19.935398 13.632674 19.367188 15.037109 C 19.94644 15.387646 20.527063 15.73602 21.105469 16.087891 C 22.671737 12.714066 22.120988 8.4920871 19.708984 5.6542969 C 18.063396 3.6246553 15.604973 2.2995704 13 2.0644531 z M 12 6.7148438 C 10.737143 6.7148437 9.7148438 7.737143 9.7148438 9 L 9.7148438 10.142578 L 9.1425781 10.142578 C 8.5140068 10.142578 8 10.656585 8 11.285156 L 8 15.857422 C 8 16.491709 8.5140068 17 9.1425781 17 L 14.857422 17 C 15.491709 17 16 16.491709 16 15.857422 L 16 11.285156 C 16 10.656585 15.491709 10.142578 14.857422 10.142578 L 14.285156 10.142578 L 14.285156 9 C 14.285156 7.737143 13.262857 6.7148438 12 6.7148438 z M 12 7.8574219 C 12.634286 7.8574219 13.142578 8.3714294 13.142578 9 L 13.142578 10.142578 L 10.857422 10.142578 L 10.857422 9 C 10.857422 8.3714294 11.371429 7.8574219 12 7.8574219 z M 12 12.427734 C 12.634286 12.427734 13.142578 12.943693 13.142578 13.572266 C 13.142578 14.20655 12.634286 14.714844 12 14.714844 C 11.371429 14.714844 10.857422 14.20655 10.857422 13.572266 C 10.857422 12.943693 11.371429 12.427734 12 12.427734 z" />
|
||||
</vector>
|
||||
15
app/src/main/res/drawable/ic_lock_white_padding_24dp.xml
Normal file
15
app/src/main/res/drawable/ic_lock_white_padding_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<group
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8"
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -52,8 +52,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/biometric_message"
|
||||
tools:text="@string/advanced_unlock_prompt_store_credential_title"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Default.TextOnPrimary"
|
||||
android:textSize="14sp"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Secondary.TextOnPrimary"
|
||||
android:gravity="center" />
|
||||
|
||||
<TextView
|
||||
@@ -67,7 +66,6 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/biometric_title"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:text="Sample error"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Secondary.TextOnPrimary"
|
||||
android:textSize="12sp"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Warning.TextOnPrimary"
|
||||
android:gravity="center" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -138,8 +138,8 @@
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="@dimen/lock_button_size"
|
||||
android:layout_height="@dimen/lock_button_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|bottom" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -100,8 +100,8 @@
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="@dimen/lock_button_size"
|
||||
android:layout_height="@dimen/lock_button_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:layout_below="@+id/toolbar">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/nodes_list_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
@@ -168,8 +169,8 @@
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="@dimen/lock_button_size"
|
||||
android:layout_height="@dimen/lock_button_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"/>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -59,8 +59,8 @@
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="@dimen/lock_button_size"
|
||||
android:layout_height="@dimen/lock_button_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/windowBackground"
|
||||
tools:targetApi="o">
|
||||
|
||||
<com.kunzisoft.keepass.view.SpecialModeView
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="@dimen/lock_button_size"
|
||||
android:layout_height="@dimen/lock_button_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="start|bottom" />
|
||||
|
||||
|
||||
56
app/src/main/res/layout/fragment_icon_edit.xml
Normal file
56
app/src/main/res/layout/fragment_icon_edit.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
|
||||
This file is part of KeePassDX.
|
||||
|
||||
KeePassDX is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
KeePassDX is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<androidx.core.widget.NestedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/default_margin"
|
||||
android:importantForAutofill="noExcludeDescendants"
|
||||
tools:targetApi="o">
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/icon_edit_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginRight="@dimen/default_margin"
|
||||
android:layout_marginEnd="@dimen/default_margin"
|
||||
android:src="@drawable/ic_blank_32dp"/>
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/icon_edit_name_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/icon_edit_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:hint="@string/hint_icon_name"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -33,6 +33,7 @@
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Title"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColor"/>
|
||||
|
||||
<TextView
|
||||
@@ -55,14 +56,14 @@
|
||||
android:layout_marginRight="20dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
style="@style/KeepassDXStyle.TextAppearance.WarningTextStyle"/>
|
||||
style="@style/KeepassDXStyle.TextAppearance.Warning"/>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress_dialog_bar"
|
||||
app:indicatorColor="?attr/colorAccent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:indeterminate="true"
|
||||
android:max="100"/>
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
android:layout_marginEnd="@dimen/card_view_margin_horizontal"
|
||||
android:layout_marginRight="@dimen/card_view_margin_horizontal"
|
||||
android:text="@string/error_otp_type"
|
||||
style="@style/KeepassDXStyle.TextAppearance.WarningTextStyle"/>
|
||||
style="@style/KeepassDXStyle.TextAppearance.Warning"/>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card_view_otp_selection"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:minHeight="36dp"
|
||||
android:minHeight="48dp"
|
||||
android:orientation="horizontal">
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:minHeight="56dp"
|
||||
android:maxHeight="72dp"
|
||||
android:minHeight="48dp"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@@ -104,10 +103,12 @@
|
||||
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/node_options"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="end"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
@@ -118,9 +119,11 @@
|
||||
android:id="@+id/node_otp_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:padding="4dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/node_attachment_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
@@ -162,7 +165,7 @@
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/node_otp_container" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:minHeight="56dp"
|
||||
android:maxHeight="72dp"
|
||||
android:minHeight="48dp"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@@ -89,18 +88,6 @@
|
||||
android:maxLines="2"
|
||||
tools:text="Node Title" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/node_subtext"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Group.SubTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:lines="1"
|
||||
android:singleLine="true"
|
||||
android:visibility="gone"
|
||||
tools:text="Node SubTitle" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/node_meta"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Group.Meta"
|
||||
|
||||
@@ -1,48 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<FrameLayout
|
||||
android:id="@+id/lock_button"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/lock_button_background"
|
||||
android:layout_width="@dimen/lock_button_size"
|
||||
android:layout_height="@dimen/lock_button_size"
|
||||
android:layout_marginStart="-2dp"
|
||||
android:layout_marginLeft="-2dp"
|
||||
android:layout_marginBottom="-2dp"
|
||||
tools:targetApi="lollipop"
|
||||
android:elevation="4dp"
|
||||
style="@style/KeepassDXStyle.Special.Button.Background"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:contentDescription="@string/menu_lock" />
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/lock_button_stroke"
|
||||
android:layout_width="@dimen/lock_button_size"
|
||||
android:layout_height="@dimen/lock_button_size"
|
||||
android:layout_marginStart="-2dp"
|
||||
android:layout_marginLeft="-2dp"
|
||||
android:layout_marginBottom="-2dp"
|
||||
tools:targetApi="lollipop"
|
||||
android:elevation="4dp"
|
||||
style="@style/KeepassDXStyle.Special.Button.Stroke"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:contentDescription="@string/menu_lock" />
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/lock_button_icon"
|
||||
android:layout_width="@dimen/lock_button_size"
|
||||
android:layout_height="@dimen/lock_button_size"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingRight="12dp"
|
||||
tools:targetApi="lollipop"
|
||||
android:elevation="4dp"
|
||||
android:src="@drawable/ic_lock_white_24dp"
|
||||
android:tint="@color/white"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:contentDescription="@string/menu_lock" />
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/lock_button"
|
||||
style="@style/KeepassDXStyle.Fab.Special"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:fabSize="mini"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/lock"
|
||||
android:src="@drawable/ic_lock_white_padding_24dp"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" />
|
||||
</FrameLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user