mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'develop' into feature/Tags
This commit is contained in:
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -24,6 +24,7 @@ A clear and concise description of what you expected to happen.
|
|||||||
- Created with: [e.g Windows KeePass 2.42]
|
- Created with: [e.g Windows KeePass 2.42]
|
||||||
- Version: [e.g. 2]
|
- Version: [e.g. 2]
|
||||||
- Location: [e.g. Remote file retrieved with GDrive app]
|
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||||
|
- File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`]
|
||||||
- Size: [e.g. 150Mo]
|
- Size: [e.g. 150Mo]
|
||||||
- Contains attachment: [e.g. Yes]
|
- Contains attachment: [e.g. Yes]
|
||||||
|
|
||||||
|
|||||||
28
CHANGELOG
28
CHANGELOG
@@ -1,7 +1,33 @@
|
|||||||
|
KeePassDX(3.2.0)
|
||||||
|
* Manage data merge #840 #977
|
||||||
|
* Manage Tags #633
|
||||||
|
* Inherit colors and icon from template #1213 #1130
|
||||||
|
* Setting to keep the screen on when watching the entry #1119
|
||||||
|
* Add path in quick search
|
||||||
|
* Small fixes
|
||||||
|
|
||||||
KeePassDX(3.1.0)
|
KeePassDX(3.1.0)
|
||||||
|
* Add breadcrumb
|
||||||
|
* Add path in search results #1148
|
||||||
|
* Add group info dialog #1177
|
||||||
|
* Manage colors #64 #913
|
||||||
|
* Fix UI in Android 8 #509
|
||||||
|
* Upgrade libs and SDK to 31 #833
|
||||||
|
* Fix parser of database v1 #1201
|
||||||
|
* Stop asking WRITE_EXTERNAL_STORAGE permission
|
||||||
|
|
||||||
|
KeePassDX(3.0.4)
|
||||||
|
* Fix autofill inline bugs #1173 #1165
|
||||||
|
* Small UI change
|
||||||
|
|
||||||
|
KeePassDX(3.0.3)
|
||||||
* Change default Argon2 parameters #1098
|
* Change default Argon2 parameters #1098
|
||||||
* Add & edit custom icon name #976
|
* Add & edit custom icon name #976
|
||||||
* Manage Tags #633
|
* 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)
|
KeePassDX(3.0.2)
|
||||||
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
|
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
- Material design with **themes**.
|
- Material design with **themes**.
|
||||||
- **Auto-Fill** and Integration.
|
- **Auto-Fill** and Integration.
|
||||||
- Field filling **keyboard**.
|
- Field filling **keyboard**.
|
||||||
|
- Dynamic **templates**
|
||||||
- **History** of each entry.
|
- **History** of each entry.
|
||||||
- Precise management of **settings**.
|
- Precise management of **settings**.
|
||||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||||
@@ -71,7 +72,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||||
|
|
||||||
This file is part of KeePassDX.
|
This file is part of KeePassDX.
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ apply plugin: 'kotlin-android'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 31
|
||||||
buildToolsVersion "30.0.3"
|
buildToolsVersion "31.0.0"
|
||||||
ndkVersion "21.4.7075529"
|
ndkVersion "21.4.7075529"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion 30
|
targetSdkVersion 31
|
||||||
versionCode = 90
|
versionCode = 93
|
||||||
versionName = "3.1.0"
|
versionName = "3.2.0"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -99,22 +99,22 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def room_version = "2.2.6"
|
def room_version = "2.4.1"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
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.preference:preference-ktx:1.1.1'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.1.0'
|
implementation 'androidx.biometric:biometric:1.1.0'
|
||||||
|
implementation 'androidx.media:media:1.4.3'
|
||||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "androidx.core:core-ktx:$android_core_version"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
implementation 'androidx.fragment:fragment-ktx:1.4.0'
|
||||||
// WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
|
implementation "com.google.android.material:material:$android_material_version"
|
||||||
implementation 'com.google.android.material:material:1.1.0'
|
|
||||||
// Token auto complete
|
// Token auto complete
|
||||||
implementation "com.splitwise:tokenautocomplete:4.0.0-beta04"
|
implementation "com.splitwise:tokenautocomplete:4.0.0-beta04"
|
||||||
// Database
|
// Database
|
||||||
@@ -123,11 +123,11 @@ dependencies {
|
|||||||
// Autofill
|
// Autofill
|
||||||
implementation "androidx.autofill:autofill:1.1.0"
|
implementation "androidx.autofill:autofill:1.1.0"
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.10.6'
|
implementation 'joda-time:joda-time:2.10.13'
|
||||||
// Color
|
// Color
|
||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
|
||||||
// Education
|
// Education
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
|
||||||
// Apache Commons
|
// Apache Commons
|
||||||
implementation 'commons-io:commons-io:2.8.0'
|
implementation 'commons-io:commons-io:2.8.0'
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
@@ -138,6 +138,6 @@ dependencies {
|
|||||||
implementation project(path: ':icon-pack-material')
|
implementation project(path: ':icon-pack-material')
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
androidTestImplementation "androidx.test:rules:$android_test_version"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,12 @@
|
|||||||
android:anyDensity="true" />
|
android:anyDensity="true" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.USE_BIOMETRIC" />
|
android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.VIBRATE"/>
|
android:name="android.permission.VIBRATE"/>
|
||||||
<!-- Write permission until Android 10 -->
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
||||||
android:maxSdkVersion="28"
|
|
||||||
tools:ignore="ScopedStorage" />
|
|
||||||
<!-- Open apps from links -->
|
<!-- Open apps from links -->
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
@@ -30,12 +27,13 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:name="com.kunzisoft.keepass.app.App"
|
android:name="com.kunzisoft.keepass.app.App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/old_backup_rules"
|
||||||
|
android:dataExtractionRules="@xml/backup_rules"
|
||||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:theme="@style/KeepassDXStyle.Night"
|
android:theme="@style/KeepassDXStyle.Night"
|
||||||
tools:targetApi="n">
|
tools:targetApi="s">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="${googleAndroidBackupAPIKey}" />
|
android:value="${googleAndroidBackupAPIKey}" />
|
||||||
@@ -44,6 +42,7 @@
|
|||||||
android:theme="@style/KeepassDXStyle.SplashScreen"
|
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
android:exported="true"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -53,6 +52,7 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||||
|
android:exported="true"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -111,6 +111,7 @@
|
|||||||
<!-- Main Activity -->
|
<!-- Main Activity -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||||
|
android:exported="false"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustPan">
|
android:windowSoftInputMode="adjustPan">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -154,7 +155,8 @@
|
|||||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent">
|
android:theme="@style/Theme.Transparent"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -173,7 +175,8 @@
|
|||||||
android:theme="@style/Theme.Transparent" />
|
android:theme="@style/Theme.Transparent" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||||
android:label="@string/keyboard_setting_label">
|
android:label="@string/keyboard_setting_label"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -199,6 +202,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
||||||
android:label="@string/autofill_service_name"
|
android:label="@string/autofill_service_name"
|
||||||
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.autofill"
|
android:name="android.autofill"
|
||||||
@@ -210,6 +214,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
||||||
android:label="@string/keyboard_label"
|
android:label="@string/keyboard_label"
|
||||||
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||||
<meta-data android:name="android.view.im"
|
<meta-data android:name="android.view.im"
|
||||||
android:resource="@xml/keyboard_method"/>
|
android:resource="@xml/keyboard_method"/>
|
||||||
|
|||||||
@@ -23,28 +23,33 @@ import android.app.Activity
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentSender
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.inputmethod.InlineSuggestionsRequest
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
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
|
||||||
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.autofill.KeeAutofillService
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
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 {
|
override fun applyCustomStyle(): Boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -60,17 +65,37 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||||
when (specialMode) {
|
when (specialMode) {
|
||||||
SpecialMode.SELECTION -> {
|
SpecialMode.SELECTION -> {
|
||||||
// Build search param
|
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
|
||||||
val searchInfo = SearchInfo().apply {
|
// To pass extra inline request
|
||||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION)
|
||||||
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false)
|
}
|
||||||
}
|
// Build search param
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
SearchInfo.getConcreteWebDomain(
|
||||||
launchSelection(database, searchInfo)
|
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 -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
// To register info
|
// To register info
|
||||||
@@ -91,10 +116,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchSelection(database: Database?,
|
private fun launchSelection(database: Database?,
|
||||||
|
autofillComponent: AutofillComponent?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
|
||||||
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
|
||||||
|
|
||||||
if (autofillComponent == null) {
|
if (autofillComponent == null) {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
@@ -119,6 +142,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForAutofillResult(this,
|
GroupActivity.launchForAutofillResult(this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
|
mAutofillActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
@@ -126,6 +150,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
{
|
{
|
||||||
// If database not open
|
// If database not open
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||||
|
mAutofillActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
@@ -186,55 +211,47 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
|
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 {
|
companion object {
|
||||||
|
|
||||||
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION"
|
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
||||||
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
|
||||||
|
|
||||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||||
|
|
||||||
fun getPendingIntentForSelection(context: Context,
|
fun getPendingIntentForSelection(context: Context,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent {
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
|
||||||
return PendingIntent.getActivity(context, 0,
|
return PendingIntent.getActivity(context, 0,
|
||||||
// Doesn't work with Parcelable (don't know why?)
|
// Doesn't work with direct extra Parcelable (don't know why?)
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
// Wrap into a bundle to bypass the problem
|
||||||
searchInfo?.let {
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||||
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
|
||||||
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
inlineSuggestionsRequest?.let {
|
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
||||||
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
},
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPendingIntentForRegistration(context: Context,
|
fun getPendingIntentForRegistration(context: Context,
|
||||||
registerInfo: RegisterInfo): PendingIntent {
|
registerInfo: RegisterInfo): PendingIntent {
|
||||||
return PendingIntent.getActivity(context, 0,
|
return PendingIntent.getActivity(context, 0,
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||||
},
|
},
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(context: Context,
|
||||||
|
|||||||
@@ -32,12 +32,18 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||||
|
import androidx.core.graphics.BlendModeCompat
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
@@ -60,22 +66,26 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import com.kunzisoft.keepass.view.changeControlColor
|
||||||
|
import com.kunzisoft.keepass.view.changeTitleColor
|
||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.HashMap
|
|
||||||
|
|
||||||
class EntryActivity : DatabaseLockActivity() {
|
class EntryActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||||
|
private var appBarLayout: AppBarLayout? = null
|
||||||
private var titleIconView: ImageView? = null
|
private var titleIconView: ImageView? = null
|
||||||
private var historyView: View? = null
|
private var historyView: View? = null
|
||||||
private var tagsListView: RecyclerView? = null
|
private var tagsListView: RecyclerView? = null
|
||||||
private var tagsAdapter: TagsAdapter? = null
|
private var tagsAdapter: TagsAdapter? = null
|
||||||
private var entryProgress: ProgressBar? = null
|
private var entryProgress: LinearProgressIndicator? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var loadingView: ProgressBar? = null
|
private var loadingView: ProgressBar? = null
|
||||||
@@ -89,11 +99,21 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
private var mEntryLoaded = false
|
private var mEntryLoaded = false
|
||||||
|
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
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 mIcon: IconImage? = null
|
||||||
private var mIconColor: Int = 0
|
private var mColorAccent: Int = 0
|
||||||
|
private var mControlColor: Int = 0
|
||||||
|
private var mColorPrimary: Int = 0
|
||||||
|
private var mColorBackground: Int = 0
|
||||||
|
private var mBackgroundColor: Int? = null
|
||||||
|
private var mForegroundColor: Int? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -108,6 +128,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
// Get views
|
// Get views
|
||||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||||
|
appBarLayout = findViewById(R.id.app_bar)
|
||||||
titleIconView = findViewById(R.id.entry_icon)
|
titleIconView = findViewById(R.id.entry_icon)
|
||||||
historyView = findViewById(R.id.history_container)
|
historyView = findViewById(R.id.history_container)
|
||||||
tagsListView = findViewById(R.id.entry_tags_list_view)
|
tagsListView = findViewById(R.id.entry_tags_list_view)
|
||||||
@@ -119,10 +140,19 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
collapsingToolbarLayout?.title = " "
|
collapsingToolbarLayout?.title = " "
|
||||||
toolbar?.title = " "
|
toolbar?.title = " "
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the toolbar
|
||||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||||
mIconColor = taIconColor.getColor(0, Color.BLACK)
|
val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
|
||||||
taIconColor.recycle()
|
val taColorPrimary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorPrimary))
|
||||||
|
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||||
|
mColorAccent = taColorAccent.getColor(0, Color.BLACK)
|
||||||
|
mControlColor = taControlColor.getColor(0, Color.BLACK)
|
||||||
|
mColorPrimary = taColorPrimary.getColor(0, Color.BLACK)
|
||||||
|
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
|
||||||
|
taColorAccent.recycle()
|
||||||
|
taControlColor.recycle()
|
||||||
|
taColorPrimary.recycle()
|
||||||
|
taColorBackground.recycle()
|
||||||
|
|
||||||
// Init Tags adapter
|
// Init Tags adapter
|
||||||
tagsAdapter = TagsAdapter(this)
|
tagsAdapter = TagsAdapter(this)
|
||||||
@@ -146,6 +176,15 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
// Init SAF manager
|
// Init SAF manager
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
||||||
|
mAttachmentSelected?.let { attachment ->
|
||||||
|
if (createdFileUri != null) {
|
||||||
|
mAttachmentFileBinderManager
|
||||||
|
?.startDownloadAttachment(createdFileUri, attachment)
|
||||||
|
}
|
||||||
|
mAttachmentSelected = null
|
||||||
|
}
|
||||||
|
}
|
||||||
// Init attachment service binder manager
|
// Init attachment service binder manager
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
@@ -165,10 +204,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
// Assign history dedicated view
|
// Assign history dedicated view
|
||||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||||
if (entryIsHistory) {
|
if (entryIsHistory) {
|
||||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
|
||||||
collapsingToolbarLayout?.contentScrim =
|
collapsingToolbarLayout?.contentScrim =
|
||||||
ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
ColorDrawable(mColorAccent)
|
||||||
taColorAccent.recycle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val entryInfo = entryInfoHistory.entryInfo
|
val entryInfo = entryInfoHistory.entryInfo
|
||||||
@@ -183,12 +220,9 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
// Assign title icon
|
// Assign title icon
|
||||||
mIcon = entryInfo.icon
|
mIcon = entryInfo.icon
|
||||||
titleIconView?.let { iconView ->
|
|
||||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
|
|
||||||
}
|
|
||||||
// Assign title text
|
// Assign title text
|
||||||
val entryTitle =
|
val entryTitle =
|
||||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
|
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
mUrl = entryInfo.url
|
mUrl = entryInfo.url
|
||||||
@@ -196,6 +230,9 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
val tags = entryInfo.tags
|
val tags = entryInfo.tags
|
||||||
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
||||||
tagsAdapter?.setTags(tags)
|
tagsAdapter?.setTags(tags)
|
||||||
|
// Assign colors
|
||||||
|
mBackgroundColor = entryInfo.backgroundColor
|
||||||
|
mForegroundColor = entryInfo.foregroundColor
|
||||||
|
|
||||||
loadingView?.hideByFading()
|
loadingView?.hideByFading()
|
||||||
mEntryLoaded = true
|
mEntryLoaded = true
|
||||||
@@ -207,9 +244,9 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
||||||
if (otpElement == null)
|
if (otpElement == null) {
|
||||||
entryProgress?.visibility = View.GONE
|
entryProgress?.visibility = View.GONE
|
||||||
when (otpElement?.type) {
|
} else when (otpElement.type) {
|
||||||
// Only add token if HOTP
|
// Only add token if HOTP
|
||||||
OtpType.HOTP -> {
|
OtpType.HOTP -> {
|
||||||
entryProgress?.visibility = View.GONE
|
entryProgress?.visibility = View.GONE
|
||||||
@@ -218,7 +255,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
OtpType.TOTP -> {
|
OtpType.TOTP -> {
|
||||||
entryProgress?.apply {
|
entryProgress?.apply {
|
||||||
max = otpElement.period
|
max = otpElement.period
|
||||||
progress = otpElement.secondsRemaining
|
setProgressCompat(otpElement.secondsRemaining, true)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,9 +263,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
||||||
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode ->
|
mAttachmentSelected = attachmentSelected
|
||||||
mAttachmentsToDownload[requestCode] = attachmentSelected
|
mExternalFileHelper?.createDocument(attachmentSelected.name)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||||
@@ -237,7 +273,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
this,
|
this,
|
||||||
database,
|
database,
|
||||||
historySelected.nodeId,
|
historySelected.nodeId,
|
||||||
historySelected.historyPosition
|
historySelected.historyPosition,
|
||||||
|
mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,13 +292,6 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
mEntryViewModel.loadDatabase(database)
|
mEntryViewModel.loadDatabase(database)
|
||||||
|
|
||||||
// Assign title icon
|
|
||||||
mIcon?.let { icon ->
|
|
||||||
titleIconView?.let { iconView ->
|
|
||||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
@@ -299,6 +329,11 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the screen on
|
||||||
|
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
|
||||||
|
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -307,24 +342,27 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
private fun applyToolbarColors() {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
|
||||||
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary)
|
||||||
when (requestCode) {
|
val backgroundDarker = if (mBackgroundColor != null) {
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
|
||||||
// Reload the current id from database
|
} else {
|
||||||
mEntryViewModel.loadDatabase(mDatabase)
|
mColorBackground
|
||||||
}
|
}
|
||||||
}
|
titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
|
||||||
|
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
|
||||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
mIcon?.let { icon ->
|
||||||
if (createdFileUri != null) {
|
titleIconView?.let { iconView ->
|
||||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
mIconDrawableFactory?.assignDatabaseIcon(
|
||||||
mAttachmentFileBinderManager
|
iconView,
|
||||||
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
|
icon,
|
||||||
}
|
mForegroundColor ?: mColorAccent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
|
||||||
|
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
@@ -358,11 +396,17 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
if (mEntryIsHistory || mDatabaseReadOnly) {
|
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
if (!mMergeDataAllowed) {
|
||||||
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
|
}
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
applyToolbarColors()
|
||||||
return super.onPrepareOptionsMenu(menu)
|
return super.onPrepareOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +452,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
EntryEditActivity.launchToUpdate(
|
EntryEditActivity.launchToUpdate(
|
||||||
this,
|
this,
|
||||||
database,
|
database,
|
||||||
entryId
|
entryId,
|
||||||
|
mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -437,6 +482,9 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
saveDatabase()
|
saveDatabase()
|
||||||
}
|
}
|
||||||
|
R.id.menu_merge_database -> {
|
||||||
|
mergeDatabase()
|
||||||
|
}
|
||||||
R.id.menu_reload_database -> {
|
R.id.menu_reload_database -> {
|
||||||
reloadDatabase()
|
reloadDatabase()
|
||||||
}
|
}
|
||||||
@@ -449,7 +497,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||||
setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this)
|
setResult(Activity.RESULT_OK, this)
|
||||||
}
|
}
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
@@ -467,15 +515,13 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
*/
|
*/
|
||||||
fun launch(activity: Activity,
|
fun launch(activity: Activity,
|
||||||
database: Database,
|
database: Database,
|
||||||
entryId: NodeId<UUID>) {
|
entryId: NodeId<UUID>,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
activity.startActivityForResult(
|
activityResultLauncher.launch(intent)
|
||||||
intent,
|
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,16 +532,14 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
fun launch(activity: Activity,
|
fun launch(activity: Activity,
|
||||||
database: Database,
|
database: Database,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
historyPosition: Int) {
|
historyPosition: Int,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||||
activity.startActivityForResult(
|
activityResultLauncher.launch(intent)
|
||||||
intent,
|
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,17 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.*
|
import com.kunzisoft.keepass.activities.dialogs.*
|
||||||
@@ -69,6 +74,7 @@ import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
|||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.*
|
import com.kunzisoft.keepass.view.*
|
||||||
|
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -96,6 +102,9 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
private var mTemplate: Template? = null
|
private var mTemplate: Template? = null
|
||||||
private var mIsTemplate: Boolean = false
|
private var mIsTemplate: Boolean = false
|
||||||
private var mEntryLoaded: Boolean = false
|
private var mEntryLoaded: Boolean = false
|
||||||
|
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
|
||||||
|
|
||||||
|
private val mColorPickerViewModel: ColorPickerViewModel by viewModels()
|
||||||
|
|
||||||
private var mAllowCustomFields = false
|
private var mAllowCustomFields = false
|
||||||
private var mAllowOTP = false
|
private var mAllowOTP = false
|
||||||
@@ -106,6 +115,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
// Education
|
// Education
|
||||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||||
|
|
||||||
|
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
|
||||||
|
mEntryEditViewModel.selectIcon(icon)
|
||||||
|
}
|
||||||
|
|
||||||
// To ask data lost only one time
|
// To ask data lost only one time
|
||||||
private var backPressedAlreadyApproved = false
|
private var backPressedAlreadyApproved = false
|
||||||
|
|
||||||
@@ -154,6 +167,21 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// To retrieve attachment
|
// To retrieve attachment
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
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)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
// Verify the education views
|
// Verify the education views
|
||||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||||
@@ -175,11 +203,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
templateSelectorSpinner?.apply {
|
templateSelectorSpinner?.apply {
|
||||||
// Build template selector
|
// Build template selector
|
||||||
if (templates.isNotEmpty()) {
|
if (templates.isNotEmpty()) {
|
||||||
adapter = TemplatesSelectorAdapter(
|
mTemplatesSelectorAdapter = TemplatesSelectorAdapter(
|
||||||
this@EntryEditActivity,
|
this@EntryEditActivity,
|
||||||
mIconDrawableFactory,
|
|
||||||
templates
|
templates
|
||||||
)
|
).apply {
|
||||||
|
iconDrawableFactory = mIconDrawableFactory
|
||||||
|
}
|
||||||
|
adapter = mTemplatesSelectorAdapter
|
||||||
val selectedTemplate = if (mTemplate != null)
|
val selectedTemplate = if (mTemplate != null)
|
||||||
mTemplate
|
mTemplate
|
||||||
else
|
else
|
||||||
@@ -213,7 +243,16 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// View model listeners
|
// View model listeners
|
||||||
mEntryEditViewModel.requestIconSelection.observe(this) { iconImage ->
|
mEntryEditViewModel.requestIconSelection.observe(this) { iconImage ->
|
||||||
IconPickerActivity.launch(this@EntryEditActivity, iconImage)
|
IconPickerActivity.launch(this@EntryEditActivity, iconImage, mIconSelectionActivityResultLauncher)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.requestColorSelection.observe(this) { color ->
|
||||||
|
ColorPickerDialogFragment.newInstance(color)
|
||||||
|
.show(supportFragmentManager, "ColorPickerFragment")
|
||||||
|
}
|
||||||
|
|
||||||
|
mColorPickerViewModel.colorPicked.observe(this) { color ->
|
||||||
|
mEntryEditViewModel.selectColor(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
||||||
@@ -321,6 +360,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
||||||
mAllowOTP = database?.allowOTP == true
|
mAllowOTP = database?.allowOTP == true
|
||||||
mEntryEditViewModel.loadDatabase(database)
|
mEntryEditViewModel.loadDatabase(database)
|
||||||
|
mTemplatesSelectorAdapter?.apply {
|
||||||
|
iconDrawableFactory = mIconDrawableFactory
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
@@ -472,29 +515,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
|
* Set up OTP (HOTP or TOTP) and add it as extra field
|
||||||
*/
|
*/
|
||||||
@@ -585,7 +605,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||||
attachmentView,
|
attachmentView,
|
||||||
{
|
{
|
||||||
mExternalFileHelper?.openDocument()
|
addNewAttachment()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryEditActivityEducation)
|
performedNextEducation(entryEditActivityEducation)
|
||||||
@@ -686,7 +706,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
val intentEntry = Intent()
|
val intentEntry = Intent()
|
||||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
||||||
intentEntry.putExtras(bundle)
|
intentEntry.putExtras(bundle)
|
||||||
setResult(ADD_OR_UPDATE_ENTRY_RESULT_CODE, intentEntry)
|
setResult(Activity.RESULT_OK, intentEntry)
|
||||||
super.finish()
|
super.finish()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Exception when parcelable can't be done
|
// Exception when parcelable can't be done
|
||||||
@@ -701,23 +721,46 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
// Keys for current Activity
|
// Keys for current Activity
|
||||||
const val KEY_ENTRY = "entry"
|
const val KEY_ENTRY = "entry"
|
||||||
const val KEY_PARENT = "parent"
|
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"
|
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]
|
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
||||||
*/
|
*/
|
||||||
fun launchToUpdate(activity: Activity,
|
fun launchToUpdate(activity: Activity,
|
||||||
database: Database,
|
database: Database,
|
||||||
entryId: NodeId<UUID>) {
|
entryId: NodeId<UUID>,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
activityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,12 +770,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
*/
|
*/
|
||||||
fun launchToCreate(activity: Activity,
|
fun launchToCreate(activity: Activity,
|
||||||
database: Database,
|
database: Database,
|
||||||
groupId: NodeId<*>) {
|
groupId: NodeId<*>,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
activityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -795,8 +839,9 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
* Launch EntryEditActivity to add a new entry in autofill selection
|
* Launch EntryEditActivity to add a new entry in autofill selection
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
database: Database,
|
database: Database,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
@@ -807,6 +852,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
AutofillHelper.startActivityForAutofillResult(
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo
|
searchInfo
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ import android.util.Log
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -85,6 +87,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -109,6 +116,22 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
// Open database button
|
// Open database button
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
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 = findViewById(R.id.open_keyfile_button)
|
||||||
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
@@ -256,8 +279,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
* Create a new file by calling the content provider
|
* Create a new file by calling the content provider
|
||||||
*/
|
*/
|
||||||
private fun createNewFile() {
|
private fun createNewFile() {
|
||||||
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) +
|
mExternalFileHelper?.createDocument(
|
||||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
getString(R.string.database_file_name_default) +
|
||||||
|
getString(R.string.database_file_extension_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||||
@@ -274,7 +298,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
fileNoFoundAction(exception)
|
fileNoFoundAction(exception)
|
||||||
},
|
},
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() })
|
{ onLaunchActivitySpecialMode() },
|
||||||
|
mAutofillActivityResultLauncher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivityIfLoaded(database: Database) {
|
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||||
@@ -283,7 +308,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
database,
|
database,
|
||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() })
|
{ onLaunchActivitySpecialMode() },
|
||||||
|
mAutofillActivityResultLauncher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,33 +385,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
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 {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
@@ -499,11 +498,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import android.app.TimePickerDialog
|
|||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.PorterDuff
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@@ -33,18 +33,23 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.*
|
import com.kunzisoft.keepass.activities.dialogs.*
|
||||||
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
||||||
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
@@ -58,6 +63,7 @@ import com.kunzisoft.keepass.model.GroupInfo
|
|||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
@@ -75,6 +81,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
GroupFragment.NodeClickListener,
|
GroupFragment.NodeClickListener,
|
||||||
GroupFragment.NodesActionMenuListener,
|
GroupFragment.NodesActionMenuListener,
|
||||||
GroupFragment.OnScrollListener,
|
GroupFragment.OnScrollListener,
|
||||||
|
GroupFragment.GroupRefreshedListener,
|
||||||
SortDialogFragment.SortSelectionListener {
|
SortDialogFragment.SortSelectionListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
@@ -82,18 +89,25 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
|
private var databaseNameContainer: ViewGroup? = null
|
||||||
|
private var databaseColorView: ImageView? = null
|
||||||
|
private var databaseNameView: TextView? = null
|
||||||
|
private var searchContainer: ViewGroup? = null
|
||||||
|
private var searchNumbers: TextView? = null
|
||||||
|
private var searchString: TextView? = null
|
||||||
|
private var toolbarBreadcrumb: Toolbar? = null
|
||||||
private var searchTitleView: View? = null
|
private var searchTitleView: View? = null
|
||||||
private var toolbarAction: ToolbarAction? = null
|
private var toolbarAction: ToolbarAction? = null
|
||||||
private var iconView: ImageView? = null
|
|
||||||
private var numberChildrenView: TextView? = null
|
private var numberChildrenView: TextView? = null
|
||||||
private var addNodeButtonView: AddNodeButtonView? = null
|
private var addNodeButtonView: AddNodeButtonView? = null
|
||||||
private var groupNameView: TextView? = null
|
private var breadcrumbListView: RecyclerView? = null
|
||||||
private var groupMetaView: TextView? = null
|
|
||||||
private var loadingView: ProgressBar? = null
|
private var loadingView: ProgressBar? = null
|
||||||
|
|
||||||
private val mGroupViewModel: GroupViewModel by viewModels()
|
private val mGroupViewModel: GroupViewModel by viewModels()
|
||||||
private val mGroupEditViewModel: GroupEditViewModel by viewModels()
|
private val mGroupEditViewModel: GroupEditViewModel by viewModels()
|
||||||
|
|
||||||
|
private var mBreadcrumbAdapter: BreadcrumbAdapter? = null
|
||||||
|
|
||||||
private var mGroupFragment: GroupFragment? = null
|
private var mGroupFragment: GroupFragment? = null
|
||||||
private var mRecyclingBinEnabled = false
|
private var mRecyclingBinEnabled = false
|
||||||
private var mRecyclingBinIsCurrentGroup = false
|
private var mRecyclingBinIsCurrentGroup = false
|
||||||
@@ -111,7 +125,15 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
|
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
|
||||||
private var mOnSuggestionListener: SearchView.OnSuggestionListener? = null
|
private var mOnSuggestionListener: SearchView.OnSuggestionListener? = null
|
||||||
|
|
||||||
private var mIconColor: Int = 0
|
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
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -122,13 +144,18 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
// Initialize views
|
// Initialize views
|
||||||
rootContainerView = findViewById(R.id.activity_group_container_view)
|
rootContainerView = findViewById(R.id.activity_group_container_view)
|
||||||
coordinatorLayout = findViewById(R.id.group_coordinator)
|
coordinatorLayout = findViewById(R.id.group_coordinator)
|
||||||
iconView = findViewById(R.id.group_icon)
|
|
||||||
numberChildrenView = findViewById(R.id.group_numbers)
|
numberChildrenView = findViewById(R.id.group_numbers)
|
||||||
addNodeButtonView = findViewById(R.id.add_node_button)
|
addNodeButtonView = findViewById(R.id.add_node_button)
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
databaseNameContainer = findViewById(R.id.database_name_container)
|
||||||
|
databaseColorView = findViewById(R.id.database_color)
|
||||||
|
databaseNameView = findViewById(R.id.database_name)
|
||||||
|
searchContainer = findViewById(R.id.search_container)
|
||||||
|
searchNumbers = findViewById(R.id.search_numbers)
|
||||||
|
searchString = findViewById(R.id.search_string)
|
||||||
|
toolbarBreadcrumb = findViewById(R.id.toolbar_breadcrumb)
|
||||||
searchTitleView = findViewById(R.id.search_title)
|
searchTitleView = findViewById(R.id.search_title)
|
||||||
groupNameView = findViewById(R.id.group_name)
|
breadcrumbListView = findViewById(R.id.breadcrumb_list)
|
||||||
groupMetaView = findViewById(R.id.group_meta)
|
|
||||||
toolbarAction = findViewById(R.id.toolbar_action)
|
toolbarAction = findViewById(R.id.toolbar_action)
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
loadingView = findViewById(R.id.loading)
|
loadingView = findViewById(R.id.loading)
|
||||||
@@ -140,10 +167,42 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
toolbar?.title = ""
|
toolbar?.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
mBreadcrumbAdapter = BreadcrumbAdapter(this).apply {
|
||||||
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
// Open group on breadcrumb click
|
||||||
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
onItemClickListener = { node, _ ->
|
||||||
taTextColor.recycle()
|
// If last item & not a virtual root group
|
||||||
|
val currentGroup = mCurrentGroup
|
||||||
|
if (currentGroup != null && node == currentGroup
|
||||||
|
&& (currentGroup != mDatabase?.rootGroup
|
||||||
|
|| mDatabase?.rootGroupIsVirtual == false)
|
||||||
|
) {
|
||||||
|
finishNodeAction()
|
||||||
|
launchDialogToShowGroupInfo(currentGroup)
|
||||||
|
} else {
|
||||||
|
if (mGroupFragment?.nodeActionSelectionMode == true) {
|
||||||
|
finishNodeAction()
|
||||||
|
}
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
onNodeClick(database, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onLongItemClickListener = { node, position ->
|
||||||
|
val currentGroup = mCurrentGroup
|
||||||
|
if (currentGroup != null && node == currentGroup
|
||||||
|
&& (currentGroup != mDatabase?.rootGroup
|
||||||
|
|| mDatabase?.rootGroupIsVirtual == false)
|
||||||
|
) {
|
||||||
|
finishNodeAction()
|
||||||
|
launchDialogForGroupUpdate(currentGroup)
|
||||||
|
} else {
|
||||||
|
onItemClickListener?.invoke(node, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
breadcrumbListView?.apply {
|
||||||
|
adapter = mBreadcrumbAdapter
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve group if defined at launch
|
// Retrieve group if defined at launch
|
||||||
manageIntent(intent)
|
manageIntent(intent)
|
||||||
@@ -201,21 +260,22 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// Add listeners to the add buttons
|
// Add listeners to the add buttons
|
||||||
addNodeButtonView?.setAddGroupClickListener {
|
addNodeButtonView?.setAddGroupClickListener {
|
||||||
GroupEditDialogFragment.create(GroupInfo().apply {
|
launchDialogForGroupCreation(currentGroup)
|
||||||
if (currentGroup.allowAddNoteInGroup) {
|
|
||||||
notes = ""
|
|
||||||
}
|
|
||||||
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
|
|
||||||
}
|
}
|
||||||
addNodeButtonView?.setAddEntryClickListener {
|
addNodeButtonView?.setAddEntryClickListener {
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(intent,
|
||||||
{
|
{
|
||||||
EntryEditActivity.launchToCreate(
|
mCurrentGroup?.nodeId?.let { currentParentGroupId ->
|
||||||
this@GroupActivity,
|
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
||||||
database,
|
EntryEditActivity.launchToCreate(
|
||||||
currentGroup.nodeId
|
this@GroupActivity,
|
||||||
)
|
database,
|
||||||
|
currentParentGroupId,
|
||||||
|
resultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Search not used
|
// Search not used
|
||||||
@@ -243,6 +303,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
EntryEditActivity.launchForAutofillResult(
|
EntryEditActivity.launchForAutofillResult(
|
||||||
this@GroupActivity,
|
this@GroupActivity,
|
||||||
database,
|
database,
|
||||||
|
mAutofillActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
currentGroup.nodeId,
|
currentGroup.nodeId,
|
||||||
searchInfo
|
searchInfo
|
||||||
@@ -266,9 +327,6 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assignGroupViewElements(currentGroup)
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
|
|
||||||
loadingView?.hideByFading()
|
loadingView?.hideByFading()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +335,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
mGroupEditViewModel.requestIconSelection.observe(this) { iconImage ->
|
mGroupEditViewModel.requestIconSelection.observe(this) { iconImage ->
|
||||||
IconPickerActivity.launch(this@GroupActivity, iconImage)
|
IconPickerActivity.launch(this@GroupActivity, iconImage, mIconSelectionActivityResultLauncher)
|
||||||
}
|
}
|
||||||
|
|
||||||
mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
||||||
@@ -319,6 +377,29 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
return rootContainerView
|
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?) {
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
@@ -328,17 +409,23 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
&& database?.isRecycleBinEnabled == true
|
&& database?.isRecycleBinEnabled == true
|
||||||
|
|
||||||
mRootGroup = database?.rootGroup
|
mRootGroup = database?.rootGroup
|
||||||
if (mCurrentGroupState == null) {
|
loadGroup(database)
|
||||||
mRootGroup?.let { rootGroup ->
|
|
||||||
mGroupViewModel.loadGroup(database, rootGroup, 0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mGroupViewModel.loadGroup(database, mCurrentGroupState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search suggestion
|
// Search suggestion
|
||||||
database?.let {
|
database?.let {
|
||||||
|
databaseNameView?.text = if (it.name.isNotEmpty()) it.name else getString(R.string.database)
|
||||||
|
val customColor = it.customColor
|
||||||
|
if (customColor != null) {
|
||||||
|
databaseColorView?.visibility = View.VISIBLE
|
||||||
|
databaseColorView?.setColorFilter(
|
||||||
|
customColor,
|
||||||
|
PorterDuff.Mode.SRC_IN
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
databaseColorView?.visibility = View.GONE
|
||||||
|
}
|
||||||
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, it)
|
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, it)
|
||||||
|
mBreadcrumbAdapter?.iconDrawableFactory = it.iconDrawableFactory
|
||||||
mOnSuggestionListener = object : SearchView.OnSuggestionListener {
|
mOnSuggestionListener = object : SearchView.OnSuggestionListener {
|
||||||
override fun onSuggestionClick(position: Int): Boolean {
|
override fun onSuggestionClick(position: Int): Boolean {
|
||||||
mSearchSuggestionAdapter?.let { searchAdapter ->
|
mSearchSuggestionAdapter?.let { searchAdapter ->
|
||||||
@@ -413,16 +500,27 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
|
||||||
|
if (result.isSuccess) {
|
||||||
|
try {
|
||||||
|
if (mCurrentGroup == newNodes[0] as Group)
|
||||||
|
reloadCurrentGroup()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"Unable to perform action after group update",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coordinatorLayout?.showActionErrorIfNeeded(result)
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||||
if (!result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
reloadCurrentGroup()
|
reloadCurrentGroup()
|
||||||
}
|
}
|
||||||
|
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
|
|
||||||
refreshNumberOfChildren(mCurrentGroup)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -447,16 +545,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
|
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
|
||||||
transformSearchInfoIntent(intent)
|
transformSearchInfoIntent(intent)
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
loadGroup(mDatabase)
|
||||||
finishNodeAction()
|
|
||||||
val searchString =
|
|
||||||
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
|
||||||
mGroupViewModel.loadGroupFromSearch(
|
|
||||||
mDatabase,
|
|
||||||
searchString,
|
|
||||||
PreferencesUtil.omitBackup(this)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,62 +565,44 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onGroupRefreshed() {
|
||||||
|
mCurrentGroup?.let { currentGroup ->
|
||||||
|
assignGroupViewElements(currentGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun assignGroupViewElements(group: Group?) {
|
private fun assignGroupViewElements(group: Group?) {
|
||||||
// Assign title
|
// Assign title
|
||||||
if (group != null) {
|
|
||||||
if (groupNameView != null) {
|
|
||||||
val title = group.title
|
|
||||||
groupNameView?.text = if (title.isNotEmpty()) title else getText(R.string.root)
|
|
||||||
groupNameView?.invalidate()
|
|
||||||
}
|
|
||||||
if (groupMetaView != null) {
|
|
||||||
val meta = group.nodeId.toString()
|
|
||||||
groupMetaView?.text = meta
|
|
||||||
if (meta.isNotEmpty()
|
|
||||||
&& !group.isVirtual
|
|
||||||
&& PreferencesUtil.showUUID(this)) {
|
|
||||||
groupMetaView?.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
groupMetaView?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
groupMetaView?.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group?.isVirtual == true) {
|
if (group?.isVirtual == true) {
|
||||||
searchTitleView?.visibility = View.VISIBLE
|
searchContainer?.visibility = View.VISIBLE
|
||||||
if (toolbar != null) {
|
val title = group.title
|
||||||
toolbar?.navigationIcon = null
|
searchString?.text = if (title.isNotEmpty()) title else ""
|
||||||
}
|
searchNumbers?.text = group.numberOfChildEntries.toString()
|
||||||
iconView?.visibility = View.GONE
|
databaseNameContainer?.visibility = View.GONE
|
||||||
|
toolbarBreadcrumb?.navigationIcon = null
|
||||||
|
toolbarBreadcrumb?.collapse()
|
||||||
} else {
|
} else {
|
||||||
searchTitleView?.visibility = View.GONE
|
searchContainer?.visibility = View.GONE
|
||||||
// Assign the group icon depending of IconPack or custom icon
|
databaseNameContainer?.visibility = View.VISIBLE
|
||||||
iconView?.visibility = View.VISIBLE
|
// Refresh breadcrumb
|
||||||
group?.let { currentGroup ->
|
if (toolbarBreadcrumb?.isVisible != true) {
|
||||||
iconView?.let { imageView ->
|
toolbarBreadcrumb?.expand {
|
||||||
mIconDrawableFactory?.assignDatabaseIcon(
|
setBreadcrumbNode(group)
|
||||||
imageView,
|
|
||||||
currentGroup.icon,
|
|
||||||
mIconColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolbar != null) {
|
|
||||||
if (group.containsParent())
|
|
||||||
toolbar?.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp)
|
|
||||||
else {
|
|
||||||
toolbar?.navigationIcon = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Add breadcrumb
|
||||||
|
setBreadcrumbNode(group)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign number of children
|
|
||||||
refreshNumberOfChildren(group)
|
|
||||||
|
|
||||||
// Hide button
|
|
||||||
initAddButton(group)
|
initAddButton(group)
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setBreadcrumbNode(group: Group?) {
|
||||||
|
mBreadcrumbAdapter?.apply {
|
||||||
|
setNode(group)
|
||||||
|
breadcrumbListView?.scrollToPosition(itemCount -1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initAddButton(group: Group?) {
|
private fun initAddButton(group: Group?) {
|
||||||
@@ -553,18 +624,6 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshNumberOfChildren(group: Group?) {
|
|
||||||
numberChildrenView?.apply {
|
|
||||||
if (PreferencesUtil.showNumberEntries(context)) {
|
|
||||||
group?.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
|
|
||||||
text = group?.numberOfChildEntries?.toString() ?: ""
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScrolled(dy: Int) {
|
override fun onScrolled(dy: Int) {
|
||||||
if (actionNodeMode == null)
|
if (actionNodeMode == null)
|
||||||
addNodeButtonView?.hideOrShowButtonOnScrollListener(dy)
|
addNodeButtonView?.hideOrShowButtonOnScrollListener(dy)
|
||||||
@@ -594,11 +653,14 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
val entryVersioned = node as Entry
|
val entryVersioned = node as Entry
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(intent,
|
||||||
{
|
{
|
||||||
EntryActivity.launch(
|
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
||||||
this@GroupActivity,
|
EntryActivity.launch(
|
||||||
database,
|
this@GroupActivity,
|
||||||
entryVersioned.nodeId
|
database,
|
||||||
)
|
entryVersioned.nodeId,
|
||||||
|
resultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Nothing here, a search is simply performed
|
// Nothing here, a search is simply performed
|
||||||
@@ -788,23 +850,42 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
when (node.type) {
|
when (node.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
mOldGroupToUpdate = node as Group
|
launchDialogForGroupUpdate(node as Group)
|
||||||
GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo())
|
}
|
||||||
.show(
|
Type.ENTRY -> {
|
||||||
supportFragmentManager,
|
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
||||||
GroupEditDialogFragment.TAG_CREATE_GROUP
|
EntryEditActivity.launchToUpdate(
|
||||||
)
|
this@GroupActivity,
|
||||||
|
database,
|
||||||
|
(node as Entry).nodeId,
|
||||||
|
resultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Type.ENTRY -> EntryEditActivity.launchToUpdate(
|
|
||||||
this@GroupActivity,
|
|
||||||
database,
|
|
||||||
(node as Entry).nodeId
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
reloadGroupIfSearch()
|
reloadGroupIfSearch()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launchDialogToShowGroupInfo(group: Group) {
|
||||||
|
GroupDialogFragment.launch(group.getGroupInfo())
|
||||||
|
.show(supportFragmentManager, GroupDialogFragment.TAG_SHOW_GROUP)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchDialogForGroupCreation(group: Group) {
|
||||||
|
GroupEditDialogFragment.create(GroupInfo().apply {
|
||||||
|
if (group.allowAddNoteInGroup) {
|
||||||
|
notes = ""
|
||||||
|
}
|
||||||
|
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchDialogForGroupUpdate(group: Group) {
|
||||||
|
mOldGroupToUpdate = group
|
||||||
|
GroupEditDialogFragment.update(group.getGroupInfo())
|
||||||
|
.show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCopyMenuClick(
|
override fun onCopyMenuClick(
|
||||||
database: Database,
|
database: Database,
|
||||||
nodes: List<Node>
|
nodes: List<Node>
|
||||||
@@ -888,10 +969,15 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
inflater.inflate(R.menu.database, menu)
|
inflater.inflate(R.menu.database, menu)
|
||||||
if (mDatabaseReadOnly) {
|
if (mDatabaseReadOnly) {
|
||||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
|
menu.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
|
}
|
||||||
|
if (!mMergeDataAllowed) {
|
||||||
|
menu.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
}
|
}
|
||||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||||
} else {
|
} else {
|
||||||
|
menu.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,7 +1070,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
if (!sortMenuEducationPerformed) {
|
if (!sortMenuEducationPerformed) {
|
||||||
// lockMenuEducationPerformed
|
// lockMenuEducationPerformed
|
||||||
val lockButtonView = findViewById<View>(R.id.lock_button_icon)
|
val lockButtonView = findViewById<View>(R.id.lock_button)
|
||||||
lockButtonView != null
|
lockButtonView != null
|
||||||
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
|
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
|
||||||
lockButtonView,
|
lockButtonView,
|
||||||
@@ -1002,7 +1088,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
onBackPressed()
|
// TODO change database
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_search ->
|
R.id.menu_search ->
|
||||||
@@ -1012,6 +1098,10 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
saveDatabase()
|
saveDatabase()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
R.id.menu_merge_database -> {
|
||||||
|
mergeDatabase()
|
||||||
|
return true
|
||||||
|
}
|
||||||
R.id.menu_reload_database -> {
|
R.id.menu_reload_database -> {
|
||||||
reloadDatabase()
|
reloadDatabase()
|
||||||
return true
|
return true
|
||||||
@@ -1057,37 +1147,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() {
|
private fun removeSearch() {
|
||||||
intent.removeExtra(AUTO_SEARCH_KEY)
|
intent.removeExtra(AUTO_SEARCH_KEY)
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
@@ -1292,8 +1351,9 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
database: Database,
|
database: Database,
|
||||||
|
activityResultLaunch: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
autoSearch: Boolean = false) {
|
autoSearch: Boolean = false) {
|
||||||
@@ -1303,6 +1363,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
AutofillHelper.startActivityForAutofillResult(
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
|
activityResultLaunch,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo
|
searchInfo
|
||||||
)
|
)
|
||||||
@@ -1335,11 +1396,12 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
* Global Launch
|
* Global Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launch(activity: Activity,
|
fun launch(activity: AppCompatActivity,
|
||||||
database: Database,
|
database: Database,
|
||||||
onValidateSpecialMode: () -> Unit,
|
onValidateSpecialMode: () -> Unit,
|
||||||
onCancelSpecialMode: () -> Unit,
|
onCancelSpecialMode: () -> Unit,
|
||||||
onLaunchActivitySpecialMode: () -> Unit) {
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
|
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||||
{
|
{
|
||||||
GroupActivity.launch(
|
GroupActivity.launch(
|
||||||
@@ -1451,6 +1513,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
// Here no search info found, disable auto search
|
// Here no search info found, disable auto search
|
||||||
GroupActivity.launchForAutofillResult(activity,
|
GroupActivity.launchForAutofillResult(activity,
|
||||||
database,
|
database,
|
||||||
|
autofillActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -82,6 +85,9 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
addCustomIcon(uri)
|
||||||
|
}
|
||||||
|
|
||||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||||
|
|
||||||
@@ -309,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() {
|
private fun setResult() {
|
||||||
setResult(Activity.RESULT_OK, Intent().apply {
|
setResult(Activity.RESULT_OK, Intent().apply {
|
||||||
putExtra(EXTRA_ICON, mIconImage)
|
putExtra(EXTRA_ICON, mIconImage)
|
||||||
@@ -331,30 +329,28 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
|
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 EXTRA_ICON = "EXTRA_ICON"
|
||||||
|
|
||||||
private const val MAX_ICON_SIZE = 5242880
|
private const val MAX_ICON_SIZE = 5242880
|
||||||
|
|
||||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) {
|
fun registerIconSelectionForResult(context: FragmentActivity,
|
||||||
if (requestCode == ICON_SELECTED_REQUEST) {
|
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(context: Activity,
|
fun launch(context: FragmentActivity,
|
||||||
previousIcon: IconImage?) {
|
previousIcon: IconImage?,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
// Create an instance to return the picker icon
|
// Create an instance to return the picker icon
|
||||||
context.startActivityForResult(
|
resultLauncher.launch(
|
||||||
Intent(context,
|
Intent(context, IconPickerActivity::class.java).apply {
|
||||||
IconPickerActivity::class.java).apply {
|
|
||||||
if (previousIcon != null)
|
if (previousIcon != null)
|
||||||
putExtra(EXTRA_ICON, previousIcon)
|
putExtra(EXTRA_ICON, previousIcon)
|
||||||
},
|
}
|
||||||
ICON_SELECTED_REQUEST)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -35,12 +34,12 @@ import android.view.KeyEvent.KEYCODE_ENTER
|
|||||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import android.widget.TextView.OnEditorActionListener
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -71,11 +70,12 @@ import com.kunzisoft.keepass.utils.MenuUtil
|
|||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
|
||||||
open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
@@ -89,7 +89,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
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 mDefaultDatabase: Boolean = false
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
@@ -98,20 +99,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mPermissionAsked = false
|
|
||||||
private var mReadOnly: Boolean = false
|
private var mReadOnly: Boolean = false
|
||||||
private var mForceReadOnly: Boolean = false
|
private var mForceReadOnly: Boolean = false
|
||||||
set(value) {
|
|
||||||
infoContainerView?.visibility = if (value) {
|
|
||||||
mReadOnly = true
|
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
|
||||||
View.GONE
|
|
||||||
}
|
|
||||||
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -133,7 +127,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||||
|
|
||||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
|
||||||
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||||
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||||
} else {
|
} else {
|
||||||
@@ -142,6 +135,12 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
|
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
mDatabaseKeyFileUri = uri
|
||||||
|
populateKeyFileTextView(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||||
@@ -170,9 +169,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||||
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
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
|
// Init Biometric elements
|
||||||
advancedUnlockFragment = supportFragmentManager
|
advancedUnlockFragment = supportFragmentManager
|
||||||
@@ -188,21 +184,30 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
|
|
||||||
// Listen password checkbox to init advanced unlock and confirmation button
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
|
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
|
||||||
advancedUnlockFragment?.checkUnlockAvailability()
|
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||||
enableOrNotTheConfirmationButton()
|
enableOrNotTheConfirmationButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe if default database
|
// Observe if default database
|
||||||
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||||
mDefaultDatabase = isDefaultDatabase
|
mDefaultDatabase = isDefaultDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe database file change
|
// Observe database file change
|
||||||
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||||
|
|
||||||
// Force read only if the file does not exists
|
// Force read only if the file does not exists
|
||||||
mForceReadOnly = databaseFile?.let {
|
val databaseFileNotExists = databaseFile?.let {
|
||||||
!it.databaseFileExists
|
!it.databaseFileExists
|
||||||
} ?: true
|
} ?: true
|
||||||
|
infoContainerView?.visibility = if (databaseFileNotExists) {
|
||||||
|
mReadOnly = true
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
mForceReadOnly = databaseFileNotExists
|
||||||
|
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
// Post init uri with KeyFile only if needed
|
// Post init uri with KeyFile only if needed
|
||||||
@@ -232,15 +237,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow auto open prompt if lock become when UI visible
|
// Don't allow auto open prompt if lock become when UI visible
|
||||||
mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
|
||||||
false
|
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||||
else
|
|
||||||
mAllowAutoOpenBiometricPrompt
|
|
||||||
mDatabaseFileUri?.let { databaseFileUri ->
|
|
||||||
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkPermission()
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
|
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
@@ -263,7 +266,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
// Recheck advanced unlock if error
|
// Recheck advanced unlock if error
|
||||||
advancedUnlockFragment?.initAdvancedUnlockMode()
|
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
@@ -311,7 +314,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
is FileNotFoundDatabaseException -> {
|
is FileNotFoundDatabaseException -> {
|
||||||
// Remove this default database inaccessible
|
// Remove this default database inaccessible
|
||||||
if (mDefaultDatabase) {
|
if (mDefaultDatabase) {
|
||||||
databaseFileViewModel.removeDefaultDatabase()
|
mDatabaseFileViewModel.removeDefaultDatabase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,7 +347,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
||||||
}
|
}
|
||||||
mDatabaseFileUri?.let {
|
mDatabaseFileUri?.let {
|
||||||
databaseFileViewModel.checkIfIsDefaultDatabase(it)
|
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +364,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
database,
|
database,
|
||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() }
|
{ onLaunchActivitySpecialMode() },
|
||||||
|
mAutofillActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,8 +439,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||||
} else {
|
} else {
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
advancedUnlockFragment?.loadDatabase(databaseFileUri,
|
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||||
mAllowAutoOpenBiometricPrompt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enableOrNotTheConfirmationButton()
|
enableOrNotTheConfirmationButton()
|
||||||
@@ -496,18 +499,15 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
// Reinit locking activity UI variable
|
// Reinit locking activity UI variable
|
||||||
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||||
mAllowAutoOpenBiometricPrompt = true
|
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
|
||||||
mDatabaseKeyFileUri?.let {
|
mDatabaseKeyFileUri?.let {
|
||||||
outState.putString(KEY_KEYFILE, it.toString())
|
outState.putString(KEY_KEYFILE, it.toString())
|
||||||
}
|
}
|
||||||
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||||
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,35 +606,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check permission
|
|
||||||
private fun checkPermission() {
|
|
||||||
if (Build.VERSION.SDK_INT in 23..28
|
|
||||||
&& !mReadOnly
|
|
||||||
&& !mPermissionAsked) {
|
|
||||||
mPermissionAsked = true
|
|
||||||
// Check self permission to show or not the dialog
|
|
||||||
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
val permissions = arrayOf(writePermission)
|
|
||||||
if (toolbar != null
|
|
||||||
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
WRITE_EXTERNAL_STORAGE_REQUEST -> {
|
|
||||||
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
|
||||||
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// To fix multiple view education
|
// To fix multiple view education
|
||||||
private var performedEductionInProgress = false
|
private var performedEductionInProgress = false
|
||||||
private fun launchEducation(menu: Menu) {
|
private fun launchEducation(menu: Menu) {
|
||||||
@@ -709,45 +680,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
return super.onOptionsItemSelected(item)
|
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 {
|
companion object {
|
||||||
|
|
||||||
private val TAG = PasswordActivity::class.java.name
|
private val TAG = PasswordActivity::class.java.name
|
||||||
@@ -761,10 +693,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||||
private const val KEY_PASSWORD = "password"
|
private const val KEY_PASSWORD = "password"
|
||||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||||
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?,
|
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
@@ -855,15 +783,17 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
@@ -891,12 +821,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
* Global Launch
|
* Global Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launch(activity: Activity,
|
fun launch(activity: AppCompatActivity,
|
||||||
databaseUri: Uri,
|
databaseUri: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||||
onCancelSpecialMode: () -> Unit,
|
onCancelSpecialMode: () -> Unit,
|
||||||
onLaunchActivitySpecialMode: () -> Unit) {
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
|
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||||
@@ -926,6 +857,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
PasswordActivity.launchForAutofillResult(activity,
|
PasswordActivity.launchForAutofillResult(activity,
|
||||||
databaseUri, keyFile,
|
databaseUri, keyFile,
|
||||||
|
autofillActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
@@ -133,6 +132,18 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
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)
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
@@ -208,7 +219,11 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
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
|
error = true
|
||||||
showEmptyPasswordConfirmationDialog()
|
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 {
|
companion object {
|
||||||
|
|
||||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.kunzisoft.androidclearchroma.view.ChromaColorView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||||
|
|
||||||
|
class ColorPickerDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
private val mColorPickerViewModel: ColorPickerViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var enableSwitchView: CompoundButton
|
||||||
|
private lateinit var chromaColorView: ChromaColorView
|
||||||
|
|
||||||
|
private var mDefaultColor = Color.WHITE
|
||||||
|
private var mActivated = false
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_color_picker, null)
|
||||||
|
enableSwitchView = root.findViewById(R.id.switch_element)
|
||||||
|
chromaColorView = root.findViewById(R.id.chroma_color_view)
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
|
||||||
|
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
|
||||||
|
}
|
||||||
|
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
|
||||||
|
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(ARG_INITIAL_COLOR)) {
|
||||||
|
mDefaultColor = getInt(ARG_INITIAL_COLOR)
|
||||||
|
}
|
||||||
|
if (containsKey(ARG_ACTIVATED)) {
|
||||||
|
mActivated = getBoolean(ARG_ACTIVATED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enableSwitchView.isChecked = mActivated
|
||||||
|
chromaColorView.currentColor = mDefaultColor
|
||||||
|
|
||||||
|
chromaColorView.setOnColorChangedListener {
|
||||||
|
if (!enableSwitchView.isChecked)
|
||||||
|
enableSwitchView.isChecked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
val color: Int? = if (enableSwitchView.isChecked)
|
||||||
|
chromaColorView.currentColor
|
||||||
|
else
|
||||||
|
null
|
||||||
|
mColorPickerViewModel.pickColor(color)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
|
||||||
|
outState.putBoolean(ARG_ACTIVATED, mActivated)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
|
||||||
|
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
@ColorInt initialColor: Int?,
|
||||||
|
): ColorPickerDialogFragment {
|
||||||
|
return ColorPickerDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putInt(ARG_INITIAL_COLOR, initialColor ?: Color.WHITE)
|
||||||
|
putBoolean(ARG_ACTIVATED, initialColor != null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* 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.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import com.kunzisoft.keepass.view.DateTimeFieldView
|
||||||
|
|
||||||
|
class GroupDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||||
|
private var mGroupInfo = GroupInfo()
|
||||||
|
|
||||||
|
private lateinit var iconView: ImageView
|
||||||
|
private var mIconColor: Int = 0
|
||||||
|
private lateinit var nameTextView: TextView
|
||||||
|
private lateinit var notesTextLabelView: TextView
|
||||||
|
private lateinit var notesTextView: TextView
|
||||||
|
private lateinit var expirationView: DateTimeFieldView
|
||||||
|
private lateinit var creationView: TextView
|
||||||
|
private lateinit var modificationView: TextView
|
||||||
|
private lateinit var uuidContainerView: ViewGroup
|
||||||
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
|
}
|
||||||
|
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_group, null)
|
||||||
|
iconView = root.findViewById(R.id.group_icon)
|
||||||
|
nameTextView = root.findViewById(R.id.group_name)
|
||||||
|
notesTextLabelView = root.findViewById(R.id.group_note_label)
|
||||||
|
notesTextView = root.findViewById(R.id.group_note)
|
||||||
|
expirationView = root.findViewById(R.id.group_expiration)
|
||||||
|
creationView = root.findViewById(R.id.group_created)
|
||||||
|
modificationView = root.findViewById(R.id.group_modified)
|
||||||
|
uuidContainerView = root.findViewById(R.id.group_UUID_container)
|
||||||
|
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||||
|
mIconColor = ta.getColor(0, Color.WHITE)
|
||||||
|
ta.recycle()
|
||||||
|
|
||||||
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||||
|
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
|
} else {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(KEY_GROUP_INFO)) {
|
||||||
|
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate info in views
|
||||||
|
val title = mGroupInfo.title
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
nameTextView.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
nameTextView.text = title
|
||||||
|
nameTextView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
val notes = mGroupInfo.notes
|
||||||
|
if (notes == null || notes.isEmpty()) {
|
||||||
|
notesTextLabelView.visibility = View.GONE
|
||||||
|
notesTextView.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
notesTextView.text = notes
|
||||||
|
notesTextLabelView.visibility = View.VISIBLE
|
||||||
|
notesTextView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
expirationView.activation = mGroupInfo.expires
|
||||||
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
|
creationView.text = mGroupInfo.creationTime.getDateTimeString(resources)
|
||||||
|
modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources)
|
||||||
|
val uuid = UuidUtil.toHexString(mGroupInfo.id)
|
||||||
|
if (uuid == null || uuid.isEmpty()) {
|
||||||
|
uuidContainerView.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
uuidReferenceView.text = uuid
|
||||||
|
uuidContainerView.apply {
|
||||||
|
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok){ _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Error(val isError: Boolean, val messageId: Int?)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG_SHOW_GROUP = "TAG_SHOW_GROUP"
|
||||||
|
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||||
|
|
||||||
|
fun launch(groupInfo: GroupInfo): GroupDialogFragment {
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||||
|
val fragment = GroupDialogFragment()
|
||||||
|
fragment.arguments = bundle
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -246,8 +246,8 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||||
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
private const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||||
const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||||
|
|
||||||
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
s?.toString()?.let { userString ->
|
s?.toString()?.let { userString ->
|
||||||
try {
|
try {
|
||||||
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH))
|
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
||||||
otpSecretContainer?.error = null
|
otpSecretContainer?.error = null
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
setOnIconClickListener {
|
setOnIconClickListener {
|
||||||
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
|
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
|
||||||
}
|
}
|
||||||
|
setOnBackgroundColorClickListener {
|
||||||
|
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
|
||||||
|
}
|
||||||
|
setOnForegroundColorClickListener {
|
||||||
|
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
|
||||||
|
}
|
||||||
setOnCustomEditionActionClickListener { field ->
|
setOnCustomEditionActionClickListener { field ->
|
||||||
mEntryEditViewModel.requestCustomFieldEdition(field)
|
mEntryEditViewModel.requestCustomFieldEdition(field)
|
||||||
}
|
}
|
||||||
@@ -158,6 +164,14 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
templateView.setIcon(iconImage)
|
templateView.setIcon(iconImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onBackgroundColorSelected.observe(this) { color ->
|
||||||
|
templateView.setBackgroundColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onForegroundColorSelected.observe(this) { color ->
|
||||||
|
templateView.setForegroundColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
|
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
|
||||||
templateView.setPasswordField(passwordField)
|
templateView.setPasswordField(passwordField)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||||
|
|
||||||
private lateinit var uuidContainerView: View
|
private lateinit var uuidContainerView: View
|
||||||
private lateinit var uuidView: TextView
|
|
||||||
private lateinit var uuidReferenceView: TextView
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
private var mClipboardHelper: ClipboardHelper? = null
|
private var mClipboardHelper: ClipboardHelper? = null
|
||||||
@@ -88,7 +87,6 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
uuidContainerView.apply {
|
uuidContainerView.apply {
|
||||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
uuidView = view.findViewById(R.id.entry_UUID)
|
|
||||||
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||||
|
|
||||||
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
||||||
@@ -200,7 +198,6 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun assignUUID(uuid: UUID?) {
|
private fun assignUUID(uuid: UUID?) {
|
||||||
uuidView.text = uuid?.toString()
|
|
||||||
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.activities.fragments
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
@@ -34,12 +33,11 @@ import com.kunzisoft.keepass.activities.EntryEditActivity
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -50,10 +48,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
private var nodeClickListener: NodeClickListener? = null
|
private var nodeClickListener: NodeClickListener? = null
|
||||||
private var onScrollListener: OnScrollListener? = null
|
private var onScrollListener: OnScrollListener? = null
|
||||||
|
private var groupRefreshed: GroupRefreshedListener? = null
|
||||||
|
|
||||||
private var mNodesRecyclerView: RecyclerView? = null
|
private var mNodesRecyclerView: RecyclerView? = null
|
||||||
private var mLayoutManager: LinearLayoutManager? = null
|
private var mLayoutManager: LinearLayoutManager? = null
|
||||||
private var mAdapter: NodeAdapter? = null
|
private var mAdapter: NodesAdapter? = null
|
||||||
|
|
||||||
private val mGroupViewModel: GroupViewModel by activityViewModels()
|
private val mGroupViewModel: GroupViewModel by activityViewModels()
|
||||||
|
|
||||||
@@ -74,6 +73,19 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
private var mRecycleBinEnable: Boolean = false
|
private var mRecycleBinEnable: Boolean = false
|
||||||
private var mRecycleBin: Group? = null
|
private var mRecycleBin: Group? = null
|
||||||
|
|
||||||
|
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() {
|
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
super.onScrollStateChanged(recyclerView, newState)
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
@@ -89,12 +101,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
|
|
||||||
|
// TODO Change to ViewModel
|
||||||
try {
|
try {
|
||||||
nodeClickListener = context as NodeClickListener
|
nodeClickListener = context as NodeClickListener
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
// The activity doesn't implement the interface, throw exception
|
// The activity doesn't implement the interface, throw exception
|
||||||
throw ClassCastException(context.toString()
|
throw ClassCastException(context.toString()
|
||||||
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
|
+ " must implement " + NodesAdapter.NodeClickCallback::class.java.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -102,14 +116,24 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
onScrollListener = null
|
onScrollListener = null
|
||||||
// Context menu can be omit
|
// Context menu can be omit
|
||||||
Log.w(TAG, context.toString()
|
Log.w(
|
||||||
|
TAG, context.toString()
|
||||||
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
groupRefreshed = context as GroupRefreshedListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
// The activity doesn't implement the interface, throw exception
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + GroupRefreshedListener::class.java.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetach() {
|
override fun onDetach() {
|
||||||
nodeClickListener = null
|
nodeClickListener = null
|
||||||
onScrollListener = null
|
onScrollListener = null
|
||||||
|
groupRefreshed = null
|
||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,8 +149,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
contextThemed?.let { context ->
|
contextThemed?.let { context ->
|
||||||
database?.let { database ->
|
database?.let { database ->
|
||||||
mAdapter = NodeAdapter(context, database).apply {
|
mAdapter = NodesAdapter(context, database).apply {
|
||||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||||
override fun onNodeClick(database: Database, node: Node) {
|
override fun onNodeClick(database: Database, node: Node) {
|
||||||
if (nodeActionSelectionMode) {
|
if (nodeActionSelectionMode) {
|
||||||
if (listActionNodes.contains(node)) {
|
if (listActionNodes.contains(node)) {
|
||||||
@@ -182,7 +206,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
// To apply theme
|
// To apply theme
|
||||||
return inflater.cloneInContext(contextThemed)
|
return inflater.cloneInContext(contextThemed)
|
||||||
.inflate(R.layout.fragment_group, container, false)
|
.inflate(R.layout.fragment_nodes, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@@ -247,6 +271,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
} else {
|
} else {
|
||||||
notFoundView?.visibility = View.GONE
|
notFoundView?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupRefreshed?.onGroupRefreshed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||||
@@ -279,15 +305,17 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
val sortDialogFragment: SortDialogFragment =
|
val sortDialogFragment: SortDialogFragment =
|
||||||
if (mRecycleBinEnable) {
|
if (mRecycleBinEnable) {
|
||||||
SortDialogFragment.getInstance(
|
SortDialogFragment.getInstance(
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getListSort(context),
|
||||||
PreferencesUtil.getAscendingSort(context),
|
PreferencesUtil.getAscendingSort(context),
|
||||||
PreferencesUtil.getGroupsBeforeSort(context),
|
PreferencesUtil.getGroupsBeforeSort(context),
|
||||||
PreferencesUtil.getRecycleBinBottomSort(context))
|
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
SortDialogFragment.getInstance(
|
SortDialogFragment.getInstance(
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getListSort(context),
|
||||||
PreferencesUtil.getAscendingSort(context),
|
PreferencesUtil.getAscendingSort(context),
|
||||||
PreferencesUtil.getGroupsBeforeSort(context))
|
PreferencesUtil.getGroupsBeforeSort(context)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||||
@@ -399,27 +427,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
|
* Callback listener to redefine to do an action when a node is click
|
||||||
*/
|
*/
|
||||||
@@ -455,6 +462,10 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
fun onScrolled(dy: Int)
|
fun onScrolled(dy: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GroupRefreshedListener {
|
||||||
|
fun onGroupRefreshed()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = GroupFragment::class.java.name
|
private val TAG = GroupFragment::class.java.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,16 @@
|
|||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity.RESULT_OK
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
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.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||||
@@ -38,6 +40,10 @@ class ExternalFileHelper {
|
|||||||
private var activity: FragmentActivity? = null
|
private var activity: FragmentActivity? = null
|
||||||
private var fragment: Fragment? = 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) {
|
constructor(context: FragmentActivity) {
|
||||||
this.activity = context
|
this.activity = context
|
||||||
this.fragment = null
|
this.fragment = null
|
||||||
@@ -48,94 +54,81 @@ class ExternalFileHelper {
|
|||||||
this.fragment = context
|
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,
|
fun openDocument(getContent: Boolean = false,
|
||||||
typeString: String = "*/*") {
|
typeString: String = "*/*") {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
try {
|
||||||
try {
|
if (getContent) {
|
||||||
if (getContent) {
|
getContentResultLauncher?.launch(typeString)
|
||||||
openActivityWithActionGetContent(typeString)
|
} else {
|
||||||
} else {
|
openDocumentResultLauncher?.launch(arrayOf(typeString))
|
||||||
openActivityWithActionOpenDocument(typeString)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to open document", e)
|
|
||||||
showFileManagerDialogFragment()
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to open document", e)
|
||||||
showFileManagerDialogFragment()
|
showFileManagerDialogFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
fun createDocument(titleString: String) {
|
||||||
private fun openActivityWithActionOpenDocument(typeString: String) {
|
try {
|
||||||
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
createDocumentResultLauncher?.launch(titleString)
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
} catch (e: Exception) {
|
||||||
type = typeString
|
Log.e(TAG, "Unable to create document", e)
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
showFileManagerDialogFragment()
|
||||||
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(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,
|
class OpenDocument : ActivityResultContracts.OpenDocument() {
|
||||||
typeString: String = "application/octet-stream"): Int? {
|
@SuppressLint("InlinedApi")
|
||||||
val idCode = getUnusedCreateFileRequestCode()
|
override fun createIntent(context: Context, input: Array<out String>): Intent {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
return super.createIntent(context, input).apply {
|
||||||
try {
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
type = typeString
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
putExtra(Intent.EXTRA_TITLE, titleString)
|
|
||||||
}
|
}
|
||||||
if (fragment != null)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
fragment?.startActivityForResult(intent, idCode)
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intent, idCode)
|
|
||||||
return idCode
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to create document", e)
|
|
||||||
showFileManagerDialogFragment()
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
showFileManagerDialogFragment()
|
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
class GetContent : ActivityResultContracts.GetContent() {
|
||||||
* To use in onActivityResultCallback in Fragment or Activity
|
@SuppressLint("InlinedApi")
|
||||||
* @param onFileCreated Callback retrieve from data
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
* @return true if requestCode was captured, false elsewhere
|
return super.createIntent(context, input).apply {
|
||||||
*/
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
onFileCreated: (fileCreated: Uri?)->Unit) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
// Retrieve the created URI from the file manager
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) {
|
}
|
||||||
onFileCreated.invoke(data?.data)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
fileRequestCodes.remove(requestCode)
|
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 {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "OpenFileHelper"
|
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")
|
@SuppressLint("InlinedApi")
|
||||||
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
||||||
typeString: String = "application/octet-stream"): Boolean {
|
typeString: String = "application/octet-stream"): Boolean {
|
||||||
@@ -231,7 +212,7 @@ class ExternalFileHelper {
|
|||||||
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
|
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||||
externalFileHelper?.let { fileHelper ->
|
externalFileHelper?.let { fileHelper ->
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
fileHelper.openDocument()
|
fileHelper.openDocument(false)
|
||||||
}
|
}
|
||||||
setOnLongClickListener {
|
setOnLongClickListener {
|
||||||
fileHelper.openDocument(true)
|
fileHelper.openDocument(true)
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
private var mExitLock: Boolean = false
|
private var mExitLock: Boolean = false
|
||||||
|
|
||||||
protected var mDatabaseReadOnly: Boolean = true
|
protected var mDatabaseReadOnly: Boolean = true
|
||||||
|
protected var mMergeDataAllowed: Boolean = false
|
||||||
private var mAutoSaveEnable: Boolean = true
|
private var mAutoSaveEnable: Boolean = true
|
||||||
|
|
||||||
protected var mIconDrawableFactory: IconDrawableFactory? = null
|
protected var mIconDrawableFactory: IconDrawableFactory? = null
|
||||||
@@ -87,8 +88,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.mergeDatabase.observe(this) { fixDuplicateUuid ->
|
||||||
|
mDatabaseTaskProvider?.startDatabaseMerge(fixDuplicateUuid)
|
||||||
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||||
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.saveName.observe(this) {
|
mDatabaseViewModel.saveName.observe(this) {
|
||||||
@@ -100,7 +107,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.saveColor.observe(this) {
|
mDatabaseViewModel.saveColor.observe(this) {
|
||||||
@@ -180,8 +187,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
closeDatabase(database)
|
closeDatabase(database)
|
||||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||||
// Add onActivityForResult response
|
mExitLock = true
|
||||||
setResult(RESULT_EXIT_LOCK)
|
|
||||||
closeOptionsMenu()
|
closeOptionsMenu()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
@@ -198,6 +204,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseReadOnly = database.isReadOnly
|
mDatabaseReadOnly = database.isReadOnly
|
||||||
|
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||||
mIconDrawableFactory = database.iconDrawableFactory
|
mIconDrawableFactory = database.iconDrawableFactory
|
||||||
|
|
||||||
checkRegister()
|
checkRegister()
|
||||||
@@ -213,6 +220,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
|
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
// Reload the current activity
|
// Reload the current activity
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
@@ -255,8 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mDatabaseTaskProvider?.startDatabaseSave(true)
|
mDatabaseTaskProvider?.startDatabaseSave(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun mergeDatabase() {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseMerge(false)
|
||||||
|
}
|
||||||
|
|
||||||
fun reloadDatabase() {
|
fun reloadDatabase() {
|
||||||
mDatabaseTaskProvider?.startDatabaseReload(false)
|
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseReload(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createEntry(newEntry: Entry,
|
fun createEntry(newEntry: Entry,
|
||||||
@@ -353,14 +367,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
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() {
|
private fun checkRegister() {
|
||||||
// If in ave or registration mode, don't allow read only
|
// If in ave or registration mode, don't allow read only
|
||||||
if ((mSpecialMode == SpecialMode.SAVE
|
if ((mSpecialMode == SpecialMode.SAVE
|
||||||
@@ -440,8 +446,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
const val TAG = "LockingActivity"
|
const val TAG = "LockingActivity"
|
||||||
|
|
||||||
const val RESULT_EXIT_LOCK = 1450
|
|
||||||
|
|
||||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ object Stylish {
|
|||||||
*/
|
*/
|
||||||
fun load(context: Context) {
|
fun load(context: Context) {
|
||||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
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 {
|
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import android.util.Log
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED
|
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||||
@@ -89,8 +89,8 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|
||||||
|| DATABASE_APPEARANCE_PREFERENCE_CHANGED) {
|
|| DATABASE_PREFERENCE_CHANGED) {
|
||||||
DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
|
DATABASE_PREFERENCE_CHANGED = false
|
||||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||||
recreateActivity()
|
recreateActivity()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ import android.content.Context
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
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.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
abstract class StylishFragment : Fragment() {
|
abstract class StylishFragment : Fragment() {
|
||||||
|
|
||||||
@@ -42,7 +42,6 @@ abstract class StylishFragment : Fragment() {
|
|||||||
contextThemed = ContextThemeWrapper(context, themeId)
|
contextThemed = ContextThemeWrapper(context, themeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
// To fix status bar color
|
// To fix status bar color
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
@@ -58,6 +57,7 @@ abstract class StylishFragment : Fragment() {
|
|||||||
try {
|
try {
|
||||||
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
|
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
|
||||||
if (taWindowStatusLight?.getBoolean(0, false) == true) {
|
if (taWindowStatusLight?.getBoolean(0, false) == true) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||||
}
|
}
|
||||||
taWindowStatusLight?.recycle()
|
taWindowStatusLight?.recycle()
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
|
|
||||||
|
class BreadcrumbAdapter(val context: Context)
|
||||||
|
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
|
||||||
|
|
||||||
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
var iconDrawableFactory: IconDrawableFactory? = null
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
private var mNodeBreadcrumb: MutableList<Node?> = mutableListOf()
|
||||||
|
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||||
|
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||||
|
|
||||||
|
private var mShowNumberEntries = false
|
||||||
|
private var mShowUUID = false
|
||||||
|
private var mIconColor: Int = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||||
|
mShowUUID = PreferencesUtil.showUUID(context)
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||||
|
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
||||||
|
taTextColor.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun setNode(node: Node?) {
|
||||||
|
mNodeBreadcrumb.clear()
|
||||||
|
node?.let {
|
||||||
|
var currentNode = it
|
||||||
|
mNodeBreadcrumb.add(0, currentNode)
|
||||||
|
while (currentNode.containsParent()) {
|
||||||
|
currentNode.parent?.let { parent ->
|
||||||
|
currentNode = parent
|
||||||
|
mNodeBreadcrumb.add(0, currentNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return when (position) {
|
||||||
|
mNodeBreadcrumb.size - 1 -> 0
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder {
|
||||||
|
return BreadcrumbGroupViewHolder(inflater.inflate(
|
||||||
|
when (viewType) {
|
||||||
|
0 -> R.layout.item_group
|
||||||
|
else -> R.layout.item_breadcrumb
|
||||||
|
}, parent, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BreadcrumbGroupViewHolder, position: Int) {
|
||||||
|
val node = mNodeBreadcrumb[position]
|
||||||
|
|
||||||
|
holder.groupNameView.apply {
|
||||||
|
text = node?.title ?: ""
|
||||||
|
strikeOut(node?.isCurrentlyExpires ?: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemView.apply {
|
||||||
|
setOnClickListener {
|
||||||
|
node?.let {
|
||||||
|
onItemClickListener?.invoke(it, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOnLongClickListener {
|
||||||
|
node?.let {
|
||||||
|
onLongItemClickListener?.invoke(it, position)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node?.type == Type.GROUP) {
|
||||||
|
(node as Group).let { group ->
|
||||||
|
|
||||||
|
holder.groupIconView?.let { imageView ->
|
||||||
|
iconDrawableFactory?.assignDatabaseIcon(
|
||||||
|
imageView,
|
||||||
|
group.icon,
|
||||||
|
mIconColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.groupNumbersView?.apply {
|
||||||
|
if (mShowNumberEntries) {
|
||||||
|
group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
|
||||||
|
text = group.numberOfChildEntries.toString()
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.groupMetaView?.apply {
|
||||||
|
val meta = group.nodeId.toVisualString()
|
||||||
|
visibility = if (meta != null
|
||||||
|
&& !group.isVirtual
|
||||||
|
&& mShowUUID
|
||||||
|
) {
|
||||||
|
text = meta
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return mNodeBreadcrumb.size
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class BreadcrumbGroupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
var groupIconView: ImageView? = itemView.findViewById(R.id.group_icon)
|
||||||
|
var groupNumbersView: TextView? = itemView.findViewById(R.id.group_numbers)
|
||||||
|
var groupNameView: TextView = itemView.findViewById(R.id.group_name)
|
||||||
|
var groupMetaView: TextView? = itemView.findViewById(R.id.group_meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,6 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
@@ -34,6 +33,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SortedList
|
import androidx.recyclerview.widget.SortedList
|
||||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
|
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
@@ -55,9 +55,9 @@ import java.util.*
|
|||||||
* Create node list adapter with contextMenu or not
|
* Create node list adapter with contextMenu or not
|
||||||
* @param context Context to use
|
* @param context Context to use
|
||||||
*/
|
*/
|
||||||
class NodeAdapter (private val context: Context,
|
class NodesAdapter (private val context: Context,
|
||||||
private val database: Database)
|
private val database: Database)
|
||||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
: RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
|
||||||
|
|
||||||
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||||
private val mNodeSortedListCallback: NodeSortedListCallback
|
private val mNodeSortedListCallback: NodeSortedListCallback
|
||||||
@@ -79,6 +79,8 @@ class NodeAdapter (private val context: Context,
|
|||||||
private var mShowOTP: Boolean = false
|
private var mShowOTP: Boolean = false
|
||||||
private var mShowUUID: Boolean = false
|
private var mShowUUID: Boolean = false
|
||||||
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
||||||
|
private var mOldVirtualGroup = false
|
||||||
|
private var mVirtualGroup = false
|
||||||
|
|
||||||
private var mActionNodesList = LinkedList<Node>()
|
private var mActionNodesList = LinkedList<Node>()
|
||||||
private var mNodeClickCallback: NodeClickCallback? = null
|
private var mNodeClickCallback: NodeClickCallback? = null
|
||||||
@@ -87,9 +89,15 @@ class NodeAdapter (private val context: Context,
|
|||||||
@ColorInt
|
@ColorInt
|
||||||
private val mContentSelectionColor: Int
|
private val mContentSelectionColor: Int
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val mIconGroupColor: Int
|
private val mTextColorPrimary: Int
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val mIconEntryColor: Int
|
private val mTextColor: Int
|
||||||
|
@ColorInt
|
||||||
|
private val mTextColorSecondary: Int
|
||||||
|
@ColorInt
|
||||||
|
private val mColorAccentLight: Int
|
||||||
|
@ColorInt
|
||||||
|
private val mTextColorSelected: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the adapter contains or not any element
|
* Determine if the adapter contains or not any element
|
||||||
@@ -110,12 +118,24 @@ class NodeAdapter (private val context: Context,
|
|||||||
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
|
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
|
||||||
// Retrieve the color to tint the icon
|
// Retrieve the color to tint the icon
|
||||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||||
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||||
taTextColorPrimary.recycle()
|
taTextColorPrimary.recycle()
|
||||||
// In two times to fix bug compilation
|
// To get text color
|
||||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
this.mTextColor = taTextColor.getColor(0, Color.BLACK)
|
||||||
taTextColor.recycle()
|
taTextColor.recycle()
|
||||||
|
// To get text color secondary
|
||||||
|
val taTextColorSecondary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
|
||||||
|
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
|
||||||
|
taTextColorSecondary.recycle()
|
||||||
|
// To get background color for selection
|
||||||
|
val taSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
|
||||||
|
this.mColorAccentLight = taSelectionColor.getColor(0, Color.GRAY)
|
||||||
|
taSelectionColor.recycle()
|
||||||
|
// To get text color for selection
|
||||||
|
val taSelectionTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor))
|
||||||
|
this.mTextColorSelected = taSelectionTextColor.getColor(0, Color.WHITE)
|
||||||
|
taSelectionTextColor.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignPreferences() {
|
private fun assignPreferences() {
|
||||||
@@ -145,6 +165,8 @@ class NodeAdapter (private val context: Context,
|
|||||||
* Rebuild the list by clear and build children from the group
|
* Rebuild the list by clear and build children from the group
|
||||||
*/
|
*/
|
||||||
fun rebuildList(group: Group) {
|
fun rebuildList(group: Group) {
|
||||||
|
mOldVirtualGroup = mVirtualGroup
|
||||||
|
mVirtualGroup = group.isVirtual
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
||||||
}
|
}
|
||||||
@@ -155,14 +177,19 @@ class NodeAdapter (private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||||
|
if (mOldVirtualGroup != mVirtualGroup)
|
||||||
|
return false
|
||||||
var typeContentTheSame = true
|
var typeContentTheSame = true
|
||||||
if (oldItem is Entry && newItem is Entry) {
|
if (oldItem is Entry && newItem is Entry) {
|
||||||
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
|
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
|
||||||
&& oldItem.username == newItem.username
|
&& oldItem.username == newItem.username
|
||||||
|
&& oldItem.backgroundColor == newItem.backgroundColor
|
||||||
|
&& oldItem.foregroundColor == newItem.foregroundColor
|
||||||
&& oldItem.getOtpElement() == newItem.getOtpElement()
|
&& oldItem.getOtpElement() == newItem.getOtpElement()
|
||||||
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
||||||
} else if (oldItem is Group && newItem is Group) {
|
} else if (oldItem is Group && newItem is Group) {
|
||||||
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
||||||
|
&& oldItem.notes == newItem.notes
|
||||||
}
|
}
|
||||||
return typeContentTheSame
|
return typeContentTheSame
|
||||||
&& oldItem.nodeId == newItem.nodeId
|
&& oldItem.nodeId == newItem.nodeId
|
||||||
@@ -327,8 +354,8 @@ class NodeAdapter (private val context: Context,
|
|||||||
val iconColor = if (holder.container.isSelected)
|
val iconColor = if (holder.container.isSelected)
|
||||||
mContentSelectionColor
|
mContentSelectionColor
|
||||||
else when (subNode.type) {
|
else when (subNode.type) {
|
||||||
Type.GROUP -> mIconGroupColor
|
Type.GROUP -> mTextColorPrimary
|
||||||
Type.ENTRY -> mIconEntryColor
|
Type.ENTRY -> mTextColor
|
||||||
}
|
}
|
||||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||||
holder.icon.apply {
|
holder.icon.apply {
|
||||||
@@ -348,14 +375,24 @@ class NodeAdapter (private val context: Context,
|
|||||||
}
|
}
|
||||||
// Add meta text to show UUID
|
// Add meta text to show UUID
|
||||||
holder.meta.apply {
|
holder.meta.apply {
|
||||||
if (mShowUUID) {
|
val nodeId = subNode.nodeId?.toVisualString()
|
||||||
text = subNode.nodeId.toString()
|
if (mShowUUID && nodeId != null) {
|
||||||
|
text = nodeId
|
||||||
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Add path to virtual group
|
||||||
|
if (mVirtualGroup) {
|
||||||
|
holder.path?.apply {
|
||||||
|
text = subNode.getPathString()
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.path?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
// Specific elements for entry
|
// Specific elements for entry
|
||||||
if (subNode.type == Type.ENTRY) {
|
if (subNode.type == Type.ENTRY) {
|
||||||
@@ -398,6 +435,50 @@ class NodeAdapter (private val context: Context,
|
|||||||
holder.attachmentIcon?.visibility =
|
holder.attachmentIcon?.visibility =
|
||||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
// Assign colors
|
||||||
|
val backgroundColor = entry.backgroundColor
|
||||||
|
if (!holder.container.isSelected) {
|
||||||
|
if (backgroundColor != null) {
|
||||||
|
holder.container.setBackgroundColor(backgroundColor)
|
||||||
|
} else {
|
||||||
|
holder.container.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.container.setBackgroundColor(mColorAccentLight)
|
||||||
|
}
|
||||||
|
val foregroundColor = entry.foregroundColor
|
||||||
|
if (!holder.container.isSelected) {
|
||||||
|
if (foregroundColor != null) {
|
||||||
|
holder.text.setTextColor(foregroundColor)
|
||||||
|
holder.subText?.setTextColor(foregroundColor)
|
||||||
|
holder.otpToken?.setTextColor(foregroundColor)
|
||||||
|
holder.otpProgress?.setIndicatorColor(foregroundColor)
|
||||||
|
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
||||||
|
holder.meta.setTextColor(foregroundColor)
|
||||||
|
holder.icon.apply {
|
||||||
|
database.iconDrawableFactory.assignDatabaseIcon(
|
||||||
|
this,
|
||||||
|
subNode.icon,
|
||||||
|
foregroundColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.text.setTextColor(mTextColor)
|
||||||
|
holder.subText?.setTextColor(mTextColorSecondary)
|
||||||
|
holder.otpToken?.setTextColor(mTextColorSecondary)
|
||||||
|
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
||||||
|
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
||||||
|
holder.meta.setTextColor(mTextColor)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.text.setTextColor(mTextColorSelected)
|
||||||
|
holder.subText?.setTextColor(mTextColorSelected)
|
||||||
|
holder.otpToken?.setTextColor(mTextColorSelected)
|
||||||
|
holder.otpProgress?.setIndicatorColor(mTextColorSelected)
|
||||||
|
holder.attachmentIcon?.setColorFilter(mTextColorSelected)
|
||||||
|
holder.meta.setTextColor(mTextColorSelected)
|
||||||
|
}
|
||||||
|
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,15 +511,16 @@ class NodeAdapter (private val context: Context,
|
|||||||
OtpType.HOTP -> {
|
OtpType.HOTP -> {
|
||||||
holder?.otpProgress?.apply {
|
holder?.otpProgress?.apply {
|
||||||
max = 100
|
max = 100
|
||||||
progress = 100
|
setProgressCompat(100, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OtpType.TOTP -> {
|
OtpType.TOTP -> {
|
||||||
holder?.otpProgress?.apply {
|
holder?.otpProgress?.apply {
|
||||||
max = otpElement.period
|
max = otpElement.period
|
||||||
progress = otpElement.secondsRemaining
|
setProgressCompat(otpElement.secondsRemaining, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
null -> {}
|
||||||
}
|
}
|
||||||
holder?.otpToken?.apply {
|
holder?.otpToken?.apply {
|
||||||
text = otpElement?.token
|
text = otpElement?.token
|
||||||
@@ -497,8 +579,9 @@ class NodeAdapter (private val context: Context,
|
|||||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
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 meta: TextView = itemView.findViewById(R.id.node_meta)
|
||||||
|
var path: TextView? = itemView.findViewById(R.id.node_path)
|
||||||
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
|
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
|
||||||
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)
|
var otpProgress: CircularProgressIndicator? = itemView.findViewById(R.id.node_otp_progress)
|
||||||
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
|
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
|
||||||
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||||
@@ -506,6 +589,6 @@ class NodeAdapter (private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = NodeAdapter::class.java.name
|
private val TAG = NodesAdapter::class.java.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,27 +21,29 @@ package com.kunzisoft.keepass.adapters
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import android.database.MatrixCursor
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.provider.BaseColumns
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.cursoradapter.widget.CursorAdapter
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
|
|
||||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class SearchEntryCursorAdapter(private val context: Context,
|
class SearchEntryCursorAdapter(private val context: Context,
|
||||||
private val database: Database)
|
private val database: Database)
|
||||||
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
: CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||||
|
|
||||||
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
||||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
@@ -70,6 +72,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
||||||
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
||||||
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext)
|
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext)
|
||||||
|
viewHolder.textViewPath = view.findViewById(R.id.entry_path)
|
||||||
view.tag = viewHolder
|
view.tag = viewHolder
|
||||||
|
|
||||||
return view
|
return view
|
||||||
@@ -101,32 +104,16 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
visibility = if (text.isEmpty()) View.GONE else View.VISIBLE
|
visibility = if (text.isEmpty()) View.GONE else View.VISIBLE
|
||||||
strikeOut(currentEntry.isCurrentlyExpires)
|
strikeOut(currentEntry.isCurrentlyExpires)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewHolder.textViewPath?.apply {
|
||||||
|
text = currentEntry.getPathString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEntryFrom(cursor: Cursor): Entry? {
|
private fun getEntryFrom(cursor: Cursor): Entry? {
|
||||||
return database.createEntry()?.apply {
|
val entryCursor = cursor as EntryCursor
|
||||||
entryKDB?.let { entryKDB ->
|
return database.getEntryById(entryCursor.getNodeId())
|
||||||
(cursor as EntryCursorKDB).populateEntry(entryKDB,
|
|
||||||
{ standardIconId ->
|
|
||||||
database.getStandardIcon(standardIconId)
|
|
||||||
},
|
|
||||||
{ customIconId ->
|
|
||||||
database.getCustomIcon(customIconId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
entryKDBX?.let { entryKDBX ->
|
|
||||||
(cursor as EntryCursorKDBX).populateEntry(entryKDBX,
|
|
||||||
{ standardIconId ->
|
|
||||||
database.getStandardIcon(standardIconId)
|
|
||||||
},
|
|
||||||
{ customIconId ->
|
|
||||||
database.getCustomIcon(customIconId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
||||||
@@ -134,14 +121,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun searchEntries(context: Context, query: String): Cursor? {
|
private fun searchEntries(context: Context, query: String): Cursor? {
|
||||||
var cursorKDB: EntryCursorKDB? = null
|
val cursor = EntryCursor()
|
||||||
var cursorKDBX: EntryCursorKDBX? = null
|
|
||||||
|
|
||||||
if (database.type == DatabaseKDB.TYPE)
|
|
||||||
cursorKDB = EntryCursorKDB()
|
|
||||||
if (database.type == DatabaseKDBX.TYPE)
|
|
||||||
cursorKDBX = EntryCursorKDBX()
|
|
||||||
|
|
||||||
val searchGroup = database.createVirtualGroupFromSearch(query,
|
val searchGroup = database.createVirtualGroupFromSearch(query,
|
||||||
mOmitBackup,
|
mOmitBackup,
|
||||||
SearchHelper.MAX_SEARCH_ENTRY)
|
SearchHelper.MAX_SEARCH_ENTRY)
|
||||||
@@ -149,17 +129,11 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
// Search in hide entries but not meta-stream
|
// Search in hide entries but not meta-stream
|
||||||
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
|
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
|
||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
entry.entryKDB?.let {
|
cursor.addEntry(entry)
|
||||||
cursorKDB?.addEntry(it)
|
|
||||||
}
|
|
||||||
entry.entryKDBX?.let {
|
|
||||||
cursorKDBX?.addEntry(it)
|
|
||||||
}
|
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return cursor
|
||||||
return cursorKDB ?: cursorKDBX
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryFromPosition(position: Int): Entry? {
|
fun getEntryFromPosition(position: Int): Entry? {
|
||||||
@@ -176,5 +150,37 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
var imageViewIcon: ImageView? = null
|
var imageViewIcon: ImageView? = null
|
||||||
var textViewTitle: TextView? = null
|
var textViewTitle: TextView? = null
|
||||||
var textViewSubTitle: TextView? = null
|
var textViewSubTitle: TextView? = null
|
||||||
|
var textViewPath: TextView? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EntryCursor : MatrixCursor(arrayOf(
|
||||||
|
ID,
|
||||||
|
COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS,
|
||||||
|
COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS
|
||||||
|
)) {
|
||||||
|
|
||||||
|
private var entryId: Long = 0
|
||||||
|
|
||||||
|
fun addEntry(entry: Entry) {
|
||||||
|
addRow(arrayOf(
|
||||||
|
entryId,
|
||||||
|
entry.nodeId.id.mostSignificantBits,
|
||||||
|
entry.nodeId.id.leastSignificantBits
|
||||||
|
))
|
||||||
|
entryId++
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNodeId(): NodeId<UUID> {
|
||||||
|
return NodeIdUUID(
|
||||||
|
UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
|
||||||
|
getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ID = BaseColumns._ID
|
||||||
|
const val COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS = "UUID_most_significant_bits"
|
||||||
|
const val COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS = "UUID_least_significant_bits"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,23 +9,23 @@ import android.widget.BaseAdapter
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
|
||||||
|
|
||||||
class TemplatesSelectorAdapter(private val context: Context,
|
class TemplatesSelectorAdapter(
|
||||||
private val iconDrawableFactory: IconDrawableFactory?,
|
context: Context,
|
||||||
private var templates: List<Template>): BaseAdapter() {
|
private var templates: List<Template>): BaseAdapter() {
|
||||||
|
|
||||||
|
var iconDrawableFactory: IconDrawableFactory? = null
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
private var mIconColor = Color.BLACK
|
private var mTextColor = Color.BLACK
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
mIconColor = taIconColor.getColor(0, Color.BLACK)
|
mTextColor = taTextColor.getColor(0, Color.BLACK)
|
||||||
taIconColor.recycle()
|
taTextColor.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
@@ -36,6 +36,7 @@ class TemplatesSelectorAdapter(private val context: Context,
|
|||||||
if (templateView == null) {
|
if (templateView == null) {
|
||||||
holder = TemplateSelectorViewHolder()
|
holder = TemplateSelectorViewHolder()
|
||||||
templateView = inflater.inflate(R.layout.item_template, parent, false)
|
templateView = inflater.inflate(R.layout.item_template, parent, false)
|
||||||
|
holder.background = templateView?.findViewById(R.id.template_background)
|
||||||
holder.icon = templateView?.findViewById(R.id.template_image)
|
holder.icon = templateView?.findViewById(R.id.template_image)
|
||||||
holder.name = templateView?.findViewById(R.id.template_name)
|
holder.name = templateView?.findViewById(R.id.template_name)
|
||||||
templateView?.tag = holder
|
templateView?.tag = holder
|
||||||
@@ -43,10 +44,15 @@ class TemplatesSelectorAdapter(private val context: Context,
|
|||||||
holder = templateView.tag as TemplateSelectorViewHolder
|
holder = templateView.tag as TemplateSelectorViewHolder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.background?.setBackgroundColor(template.backgroundColor ?: Color.TRANSPARENT)
|
||||||
|
val textColor = template.foregroundColor ?: mTextColor
|
||||||
holder.icon?.let { icon ->
|
holder.icon?.let { icon ->
|
||||||
iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, mIconColor)
|
iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, textColor)
|
||||||
|
}
|
||||||
|
holder.name?.apply {
|
||||||
|
setTextColor(textColor)
|
||||||
|
text = TemplateField.getLocalizedName(context, template.title)
|
||||||
}
|
}
|
||||||
holder.name?.text = TemplateField.getLocalizedName(context, template.title)
|
|
||||||
|
|
||||||
return templateView!!
|
return templateView!!
|
||||||
}
|
}
|
||||||
@@ -64,6 +70,7 @@ class TemplatesSelectorAdapter(private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
inner class TemplateSelectorViewHolder {
|
inner class TemplateSelectorViewHolder {
|
||||||
|
var background: View? = null
|
||||||
var icon: ImageView? = null
|
var icon: ImageView? = null
|
||||||
var name: TextView? = null
|
var name: TextView? = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ import android.app.assist.AssistStructure
|
|||||||
import android.view.inputmethod.InlineSuggestionsRequest
|
import android.view.inputmethod.InlineSuggestionsRequest
|
||||||
|
|
||||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
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.app.assist.AssistStructure
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentSender
|
|
||||||
import android.graphics.BlendMode
|
import android.graphics.BlendMode
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -35,11 +34,13 @@ import android.service.autofill.InlinePresentation
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.autofill.AutofillManager
|
import android.view.autofill.AutofillManager
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
import android.view.inputmethod.InlineSuggestionsRequest
|
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import android.widget.inline.InlinePresentationSpec
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import androidx.core.content.ContextCompat
|
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.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
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.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
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.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import kotlin.collections.ArrayList
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
object AutofillHelper {
|
object AutofillHelper {
|
||||||
|
|
||||||
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
|
||||||
|
|
||||||
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
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? {
|
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
||||||
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||||
@@ -112,7 +111,7 @@ object AutofillHelper {
|
|||||||
database: Database,
|
database: Database,
|
||||||
entryInfo: EntryInfo,
|
entryInfo: EntryInfo,
|
||||||
struct: StructureParser.Result,
|
struct: StructureParser.Result,
|
||||||
inlinePresentation: InlinePresentation?): Dataset? {
|
additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
|
||||||
val title = makeEntryTitle(entryInfo)
|
val title = makeEntryTitle(entryInfo)
|
||||||
val views = newRemoteViews(context, database, title, entryInfo.icon)
|
val views = newRemoteViews(context, database, title, entryInfo.icon)
|
||||||
val builder = Dataset.Builder(views)
|
val builder = Dataset.Builder(views)
|
||||||
@@ -201,11 +200,7 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
additionalBuild?.invoke(builder)
|
||||||
inlinePresentation?.let {
|
|
||||||
builder.setInlinePresentation(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
builder.build()
|
builder.build()
|
||||||
@@ -236,40 +231,51 @@ object AutofillHelper {
|
|||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(context: Context,
|
||||||
database: Database,
|
database: Database,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest,
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||||
positionItem: Int,
|
positionItem: Int,
|
||||||
entryInfo: EntryInfo): InlinePresentation? {
|
entryInfo: EntryInfo): InlinePresentation? {
|
||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
|
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||||
|
|
||||||
if (positionItem <= maxSuggestion - 1
|
if (positionItem <= maxSuggestion - 1
|
||||||
&& inlinePresentationSpecs.size > positionItem) {
|
&& inlinePresentationSpecs.size > positionItem
|
||||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
) {
|
||||||
|
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||||
|
|
||||||
// Make sure that the IME spec claims support for v1 UI template.
|
// Make sure that the IME spec claims support for v1 UI template.
|
||||||
val imeStyle = inlinePresentationSpec.style
|
val imeStyle = inlinePresentationSpec.style
|
||||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
// Build the content for IME UI
|
// Build the content for IME UI
|
||||||
val pendingIntent = PendingIntent.getActivity(context,
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
0,
|
0,
|
||||||
Intent(context, AutofillSettingsActivity::class.java),
|
Intent(context, AutofillSettingsActivity::class.java),
|
||||||
0)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
return InlinePresentation(
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return InlinePresentation(
|
||||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||||
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
||||||
setTitle(entryInfo.title)
|
setTitle(entryInfo.title)
|
||||||
setSubtitle(entryInfo.username)
|
setSubtitle(entryInfo.username)
|
||||||
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
setStartIcon(
|
||||||
setTintBlendMode(BlendMode.DST)
|
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||||
})
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
})
|
||||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
||||||
setEndIcon(icon.apply {
|
setEndIcon(icon.apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}.build().slice, inlinePresentationSpec, false)
|
}.build().slice, inlinePresentationSpec, false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -299,7 +305,7 @@ object AutofillHelper {
|
|||||||
database: Database,
|
database: Database,
|
||||||
entriesInfo: List<EntryInfo>,
|
entriesInfo: List<EntryInfo>,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
// Add Header
|
// Add Header
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
@@ -320,7 +326,7 @@ object AutofillHelper {
|
|||||||
// Add inline suggestion for new IME and dataset
|
// Add inline suggestion for new IME and dataset
|
||||||
var numberInlineSuggestions = 0
|
var numberInlineSuggestions = 0
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
inlineSuggestionsRequest?.let {
|
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||||
@@ -332,14 +338,19 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entriesInfo.forEachIndexed { _, entry ->
|
entriesInfo.forEachIndexed { _, entry ->
|
||||||
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (numberInlineSuggestions > 0
|
||||||
inlineSuggestionsRequest?.let {
|
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry)
|
&& compatInlineSuggestionsRequest != null) {
|
||||||
}
|
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
|
||||||
|
buildInlinePresentationForEntry(context, database,
|
||||||
|
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
|
||||||
|
)?.let { inlinePresentation ->
|
||||||
|
builder.setInlinePresentation(inlinePresentation)
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
null
|
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
|
||||||
}
|
}
|
||||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||||
@@ -351,14 +362,14 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
||||||
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||||
searchInfo, inlineSuggestionsRequest)
|
searchInfo, compatInlineSuggestionsRequest)
|
||||||
|
|
||||||
parseResult.allAutofillIds().let { autofillIds ->
|
parseResult.allAutofillIds().let { autofillIds ->
|
||||||
autofillIds.forEach { id ->
|
autofillIds.forEach { id ->
|
||||||
val builder = Dataset.Builder(manualSelectionView)
|
val builder = Dataset.Builder(manualSelectionView)
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
inlineSuggestionsRequest?.let {
|
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||||
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
||||||
inlinePresentation?.let {
|
inlinePresentation?.let {
|
||||||
@@ -403,11 +414,11 @@ object AutofillHelper {
|
|||||||
StructureParser(structure).parse()?.let { result ->
|
StructureParser(structure).parse()?.let { result ->
|
||||||
// New Response
|
// New Response
|
||||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<CompatInlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||||
if (inlineSuggestionsRequest != null) {
|
if (compatInlineSuggestionsRequest != null) {
|
||||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
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 {
|
} else {
|
||||||
buildResponse(activity, database, entriesInfo, result, null)
|
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
|
* Utility method to start an activity with an Autofill for result
|
||||||
*/
|
*/
|
||||||
fun startActivityForAutofillResult(activity: Activity,
|
fun startActivityForAutofillResult(activity: AppCompatActivity,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
||||||
autofillComponent.inlineSuggestionsRequest?.let {
|
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||||
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
||||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
activityResultLauncher?.launch(intent)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& autofillInlineSuggestionsEnabled) {
|
&& autofillInlineSuggestionsEnabled) {
|
||||||
request.inlineSuggestionsRequest
|
CompatInlineSuggestionsRequest(request)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
private fun launchSelection(database: Database?,
|
private fun launchSelection(database: Database?,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
database,
|
database,
|
||||||
@@ -155,7 +155,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||||
database: Database?,
|
database: Database?,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
parseResult.allAutofillIds().let { autofillIds ->
|
parseResult.allAutofillIds().let { autofillIds ->
|
||||||
if (autofillIds.isNotEmpty()) {
|
if (autofillIds.isNotEmpty()) {
|
||||||
@@ -249,7 +249,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& autofillInlineSuggestionsEnabled) {
|
&& autofillInlineSuggestionsEnabled) {
|
||||||
var inlinePresentation: InlinePresentation? = null
|
var inlinePresentation: InlinePresentation? = null
|
||||||
inlineSuggestionsRequest?.let {
|
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||||
&& inlinePresentationSpecs.size > 0) {
|
&& inlinePresentationSpecs.size > 0) {
|
||||||
@@ -262,9 +262,13 @@ class KeeAutofillService : AutofillService() {
|
|||||||
inlinePresentation = InlinePresentation(
|
inlinePresentation = InlinePresentation(
|
||||||
InlineSuggestionUi.newContentBuilder(
|
InlineSuggestionUi.newContentBuilder(
|
||||||
PendingIntent.getActivity(this,
|
PendingIntent.getActivity(this,
|
||||||
0,
|
0,
|
||||||
Intent(this, AutofillSettingsActivity::class.java),
|
Intent(this, AutofillSettingsActivity::class.java),
|
||||||
0)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
})
|
||||||
).apply {
|
).apply {
|
||||||
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
||||||
setTitle(getString(R.string.autofill_sign_in_prompt))
|
setTitle(getString(R.string.autofill_sign_in_prompt))
|
||||||
@@ -277,8 +281,9 @@ class KeeAutofillService : AutofillService() {
|
|||||||
}
|
}
|
||||||
// Build response
|
// Build response
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
||||||
|
} else {
|
||||||
|
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||||
}
|
}
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
|
||||||
callback.onSuccess(responseBuilder.build())
|
callback.onSuccess(responseBuilder.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,12 +272,12 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
|
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
|
||||||
val autofillId = node.autofillId
|
val autofillId = node.autofillId
|
||||||
val nodHtml = node.htmlInfo
|
val nodHtml = node.htmlInfo
|
||||||
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
|
when (nodHtml?.tag?.lowercase(Locale.ENGLISH)) {
|
||||||
"input" -> {
|
"input" -> {
|
||||||
nodHtml.attributes?.forEach { pairAttribute ->
|
nodHtml.attributes?.forEach { pairAttribute ->
|
||||||
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
|
when (pairAttribute.first.lowercase(Locale.ENGLISH)) {
|
||||||
"type" -> {
|
"type" -> {
|
||||||
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
|
||||||
"tel", "email" -> {
|
"tel", "email" -> {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
result?.usernameValue = node.autofillValue
|
result?.usernameValue = node.autofillValue
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.biometric
|
package com.kunzisoft.keepass.biometric
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -27,9 +28,11 @@ import android.os.Bundle
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView
|
import com.getkeepsafe.taptargetview.TapTargetView
|
||||||
import com.kunzisoft.keepass.R
|
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.education.PasswordActivityEducation
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
|
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -59,9 +63,12 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
/**
|
/**
|
||||||
* Manage setting to auto open biometric prompt
|
* Manage setting to auto open biometric prompt
|
||||||
*/
|
*/
|
||||||
private var mAutoOpenPrompt: Boolean = false
|
private var mAutoOpenPrompt: Boolean
|
||||||
get() {
|
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)
|
// 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 var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
||||||
|
|
||||||
|
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
|
||||||
|
|
||||||
// Only to fix multiple fingerprint menu #332
|
// Only to fix multiple fingerprint menu #332
|
||||||
private var mAllowAdvancedUnlockMenu = false
|
private var mAllowAdvancedUnlockMenu = false
|
||||||
private var mAddBiometricMenuInProgress = false
|
private var mAddBiometricMenuInProgress = false
|
||||||
@@ -79,6 +88,15 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
// Only keep connection when we request a device credential activity
|
// Only keep connection when we request a device credential activity
|
||||||
private var keepConnection = false
|
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) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
|
|
||||||
@@ -97,10 +115,21 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
retainInstance = true
|
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
|
||||||
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
|
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? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
@@ -114,17 +143,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
return rootView
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
context?.let {
|
context?.let {
|
||||||
@@ -154,32 +172,38 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) {
|
private fun onDatabaseLoaded(databaseUri: Uri?) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// To get device credential unlock result, only if same database uri
|
// To get device credential unlock result, only if same database uri
|
||||||
if (databaseUri != null
|
if (databaseUri != null
|
||||||
&& mAdvancedUnlockEnabled) {
|
&& mAdvancedUnlockEnabled) {
|
||||||
activityResult?.let {
|
val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded
|
||||||
|
deviceCredentialAuthSucceeded?.let {
|
||||||
if (databaseUri == databaseFileUri) {
|
if (databaseUri == databaseFileUri) {
|
||||||
advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode)
|
if (deviceCredentialAuthSucceeded == true) {
|
||||||
|
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||||
|
} else {
|
||||||
|
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
disconnect()
|
disconnect()
|
||||||
}
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
this.mAutoOpenPrompt = autoOpenPrompt
|
if (databaseUri != databaseFileUri) {
|
||||||
connect(databaseUri)
|
connect(databaseUri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
disconnect()
|
disconnect()
|
||||||
}
|
}
|
||||||
activityResult = null
|
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check unlock availability and change the current mode depending of device's state
|
* Check unlock availability and change the current mode depending of device's state
|
||||||
*/
|
*/
|
||||||
fun checkUnlockAvailability() {
|
private fun checkUnlockAvailability() {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
allowOpenBiometricPrompt = true
|
allowOpenBiometricPrompt = true
|
||||||
@@ -317,7 +341,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
if (cryptoPrompt.isDeviceCredentialOperation)
|
if (cryptoPrompt.isDeviceCredentialOperation)
|
||||||
keepConnection = true
|
keepConnection = true
|
||||||
try {
|
try {
|
||||||
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt)
|
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
|
||||||
|
mDeviceCredentialResultLauncher)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to open advanced unlock prompt", e)
|
Log.e(TAG, "Unable to open advanced unlock prompt", e)
|
||||||
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
||||||
@@ -369,8 +394,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
private fun initAdvancedUnlockMode() {
|
||||||
fun initAdvancedUnlockMode() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mAllowAdvancedUnlockMenu = false
|
mAllowAdvancedUnlockMenu = false
|
||||||
try {
|
try {
|
||||||
@@ -444,6 +468,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
fun deleteEncryptedDatabaseKey() {
|
fun deleteEncryptedDatabaseKey() {
|
||||||
|
mAllowAdvancedUnlockMenu = false
|
||||||
advancedUnlockManager?.closeBiometricPrompt()
|
advancedUnlockManager?.closeBiometricPrompt()
|
||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
cipherDatabaseAction.deleteByDatabaseUri(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)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
override fun onInvalidKeyException(e: Exception) {
|
override fun onInvalidKeyException(e: Exception) {
|
||||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||||
|
|||||||
@@ -19,15 +19,18 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.biometric
|
package com.kunzisoft.keepass.biometric
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.KeyguardManager
|
import android.app.KeyguardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricManager.Authenticators.*
|
import androidx.biometric.BiometricManager.Authenticators.*
|
||||||
@@ -35,6 +38,7 @@ import androidx.biometric.BiometricPrompt
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.UnrecoverableKeyException
|
import java.security.UnrecoverableKeyException
|
||||||
@@ -136,18 +140,24 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
// and the constrains (purposes) in the constructor of the Builder
|
// and the constrains (purposes) in the constructor of the Builder
|
||||||
keyGenerator?.init(
|
keyGenerator?.init(
|
||||||
KeyGenParameterSpec.Builder(
|
KeyGenParameterSpec.Builder(
|
||||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||||
|
.apply {
|
||||||
// Require the user to authenticate with a fingerprint to authorize every use
|
// 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
|
// of the key, don't use it for device credential because it's the user authentication
|
||||||
.apply {
|
if (biometricUnlockEnable) {
|
||||||
if (biometricUnlockEnable) {
|
setUserAuthenticationRequired(true)
|
||||||
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()
|
keyGenerator?.generateKey()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -164,8 +174,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initEncryptData(actionIfCypherInit
|
fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
|
||||||
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
initEncryptData(actionIfCypherInit, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||||
|
firstLaunch: Boolean) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -185,10 +199,15 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||||
advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
|
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
||||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
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) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", e)
|
Log.e(TAG, "Unable to initialize encrypt data", e)
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
@@ -214,8 +233,14 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
fun initDecryptData(ivSpecValue: String,
|
||||||
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||||
|
initDecryptData(ivSpecValue, actionIfCypherInit, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initDecryptData(ivSpecValue: String,
|
||||||
|
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||||
|
firstLaunch: Boolean = true) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -239,10 +264,20 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", 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) {
|
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
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) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", e)
|
Log.e(TAG, "Unable to initialize decrypt data", e)
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
@@ -278,9 +313,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
|
||||||
@Synchronized
|
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
|
||||||
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
|
) {
|
||||||
// Init advanced unlock prompt
|
// Init advanced unlock prompt
|
||||||
if (biometricPrompt == null) {
|
if (biometricPrompt == null) {
|
||||||
biometricPrompt = BiometricPrompt(retrieveContext(),
|
biometricPrompt = BiometricPrompt(retrieveContext(),
|
||||||
@@ -311,20 +346,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
else if (cryptoPrompt.isDeviceCredentialOperation) {
|
else if (cryptoPrompt.isDeviceCredentialOperation) {
|
||||||
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
|
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
|
||||||
retrieveContext().startActivityForResult(
|
@Suppress("DEPRECATION")
|
||||||
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription),
|
deviceCredentialResultLauncher.launch(
|
||||||
REQUEST_DEVICE_CREDENTIAL)
|
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun onActivityResult(requestCode: Int, resultCode: Int) {
|
|
||||||
if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
advancedUnlockCallback?.onAuthenticationSucceeded()
|
|
||||||
} else {
|
|
||||||
advancedUnlockCallback?.onAuthenticationFailed()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,6 +358,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AdvancedUnlockErrorCallback {
|
interface AdvancedUnlockErrorCallback {
|
||||||
|
fun onUnrecoverableKeyException(e: Exception)
|
||||||
fun onInvalidKeyException(e: Exception)
|
fun onInvalidKeyException(e: Exception)
|
||||||
fun onGenericException(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_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||||
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||||
|
|
||||||
private const val REQUEST_DEVICE_CREDENTIAL = 556
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
fun canAuthenticate(context: Context): Int {
|
fun canAuthenticate(context: Context): Int {
|
||||||
return try {
|
return try {
|
||||||
@@ -449,6 +473,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
|
|
||||||
override fun handleDecryptedResult(decryptedValue: String) {}
|
override fun handleDecryptedResult(decryptedValue: String) {}
|
||||||
|
|
||||||
|
override fun onUnrecoverableKeyException(e: Exception) {
|
||||||
|
advancedCallback.onUnrecoverableKeyException(e)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onInvalidKeyException(e: Exception) {
|
override fun onInvalidKeyException(e: Exception) {
|
||||||
advancedCallback.onInvalidKeyException(e)
|
advancedCallback.onInvalidKeyException(e)
|
||||||
}
|
}
|
||||||
@@ -460,6 +488,33 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
deleteKeystoreKey()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||||
|
|
||||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
||||||
database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream)
|
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
erase(mBackupKey)
|
erase(mBackupKey)
|
||||||
setError(e)
|
setError(e)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.os.Bundle
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -53,6 +54,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MERGE_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||||
@@ -354,6 +356,13 @@ class DatabaseTaskProvider {
|
|||||||
, ACTION_DATABASE_LOAD_TASK)
|
, ACTION_DATABASE_LOAD_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDatabaseMerge(fixDuplicateUuid: Boolean) {
|
||||||
|
start(Bundle().apply {
|
||||||
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
|
}
|
||||||
|
, ACTION_DATABASE_MERGE_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
@@ -361,6 +370,19 @@ class DatabaseTaskProvider {
|
|||||||
, ACTION_DATABASE_RELOAD_TASK)
|
, ACTION_DATABASE_RELOAD_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
||||||
|
if (conditionToAsk) {
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setMessage(R.string.warning_database_info_reloaded)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
approved.invoke()
|
||||||
|
}.create().show()
|
||||||
|
} else {
|
||||||
|
approved.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
fun startDatabaseAssignPassword(databaseUri: Uri,
|
||||||
mainCredential: MainCredential) {
|
mainCredential: MainCredential) {
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
{ memoryWanted ->
|
{ memoryWanted ->
|
||||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
},
|
},
|
||||||
LoadedKey.generateNewCipherKey(),
|
|
||||||
mFixDuplicateUUID,
|
mFixDuplicateUUID,
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.action
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
class MergeDatabaseRunnable(private val context: Context,
|
||||||
|
private val mDatabase: Database,
|
||||||
|
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
|
: ActionRunnable() {
|
||||||
|
|
||||||
|
override fun onStartRun() {
|
||||||
|
mDatabase.wasReloaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionRun() {
|
||||||
|
try {
|
||||||
|
mDatabase.mergeData(context.contentResolver,
|
||||||
|
{ memoryWanted ->
|
||||||
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
|
},
|
||||||
|
progressTaskUpdater)
|
||||||
|
} catch (e: LoadDatabaseException) {
|
||||||
|
setError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
// Register the current time to init the lock timer
|
||||||
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinishRun() {
|
||||||
|
mLoadDatabaseResult?.invoke(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.database.action
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -35,23 +34,18 @@ class ReloadDatabaseRunnable(private val context: Context,
|
|||||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
: ActionRunnable() {
|
: ActionRunnable() {
|
||||||
|
|
||||||
private var tempCipherKey: LoadedKey? = null
|
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
tempCipherKey = mDatabase.binaryCache.loadedCipherKey
|
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.clear(UriUtil.getBinaryDir(context))
|
mDatabase.clearIndexesAndBinaries(UriUtil.getBinaryDir(context))
|
||||||
mDatabase.wasReloaded = true
|
mDatabase.wasReloaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
try {
|
try {
|
||||||
mDatabase.reloadData(context.contentResolver,
|
mDatabase.reloadData(context.contentResolver,
|
||||||
UriUtil.getBinaryDir(context),
|
|
||||||
{ memoryWanted ->
|
{ memoryWanted ->
|
||||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
},
|
},
|
||||||
tempCipherKey ?: LoadedKey.generateNewCipherKey(),
|
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
setError(e)
|
setError(e)
|
||||||
@@ -61,7 +55,6 @@ class ReloadDatabaseRunnable(private val context: Context,
|
|||||||
// Register the current time to init the lock timer
|
// Register the current time to init the lock timer
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
} else {
|
} else {
|
||||||
tempCipherKey = null
|
|
||||||
mDatabase.clearAndClose(context)
|
mDatabase.clearAndClose(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ open class SaveDatabaseRunnable(protected var context: Context,
|
|||||||
override fun onStartRun() {}
|
override fun onStartRun() {}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
|
database.checkVersion()
|
||||||
if (saveDatabase && result.isSuccess) {
|
if (saveDatabase && result.isSuccess) {
|
||||||
try {
|
try {
|
||||||
database.saveData(context.contentResolver)
|
database.saveData(context.contentResolver)
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ abstract class ActionNodeDatabaseRunnable(
|
|||||||
abstract fun nodeAction()
|
abstract fun nodeAction()
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
nodeAction()
|
try {
|
||||||
|
nodeAction()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setError(e)
|
||||||
|
}
|
||||||
super.onStartRun()
|
super.onStartRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class DeleteNodesRunnable(context: Context,
|
|||||||
|
|
||||||
foreachNode@ for(nodeToDelete in mNodesToDelete) {
|
foreachNode@ for(nodeToDelete in mNodesToDelete) {
|
||||||
mOldParent = nodeToDelete.parent
|
mOldParent = nodeToDelete.parent
|
||||||
mOldParent?.touch(modified = false, touchParents = true)
|
nodeToDelete.touch(modified = true, touchParents = true)
|
||||||
|
|
||||||
when (nodeToDelete.type) {
|
when (nodeToDelete.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
@@ -50,9 +50,9 @@ class DeleteNodesRunnable(context: Context,
|
|||||||
// Remove Node from parent
|
// Remove Node from parent
|
||||||
mCanRecycle = database.canRecycle(groupToDelete)
|
mCanRecycle = database.canRecycle(groupToDelete)
|
||||||
if (mCanRecycle) {
|
if (mCanRecycle) {
|
||||||
groupToDelete.touch(modified = false, touchParents = true)
|
|
||||||
database.recycle(groupToDelete, context.resources)
|
database.recycle(groupToDelete, context.resources)
|
||||||
groupToDelete.setPreviousParentGroup(mOldParent)
|
groupToDelete.setPreviousParentGroup(mOldParent)
|
||||||
|
groupToDelete.touch(modified = true, touchParents = true)
|
||||||
} else {
|
} else {
|
||||||
database.deleteGroup(groupToDelete)
|
database.deleteGroup(groupToDelete)
|
||||||
}
|
}
|
||||||
@@ -64,9 +64,9 @@ class DeleteNodesRunnable(context: Context,
|
|||||||
// Remove Node from parent
|
// Remove Node from parent
|
||||||
mCanRecycle = database.canRecycle(entryToDelete)
|
mCanRecycle = database.canRecycle(entryToDelete)
|
||||||
if (mCanRecycle) {
|
if (mCanRecycle) {
|
||||||
entryToDelete.touch(modified = false, touchParents = true)
|
|
||||||
database.recycle(entryToDelete, context.resources)
|
database.recycle(entryToDelete, context.resources)
|
||||||
entryToDelete.setPreviousParentGroup(mOldParent)
|
entryToDelete.setPreviousParentGroup(mOldParent)
|
||||||
|
entryToDelete.touch(modified = true, touchParents = true)
|
||||||
} else {
|
} else {
|
||||||
database.deleteEntry(entryToDelete)
|
database.deleteEntry(entryToDelete)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class MoveNodesRunnable constructor(
|
|||||||
foreachNode@ for(nodeToMove in mNodesToMove) {
|
foreachNode@ for(nodeToMove in mNodesToMove) {
|
||||||
// Move node in new parent
|
// Move node in new parent
|
||||||
mOldParent = nodeToMove.parent
|
mOldParent = nodeToMove.parent
|
||||||
|
nodeToMove.touch(modified = true, touchParents = true)
|
||||||
|
|
||||||
when (nodeToMove.type) {
|
when (nodeToMove.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
@@ -52,9 +53,9 @@ class MoveNodesRunnable constructor(
|
|||||||
// and if not in the current group
|
// and if not in the current group
|
||||||
&& groupToMove != mNewParent
|
&& groupToMove != mNewParent
|
||||||
&& !mNewParent.isContainedIn(groupToMove)) {
|
&& !mNewParent.isContainedIn(groupToMove)) {
|
||||||
groupToMove.touch(modified = true, touchParents = true)
|
|
||||||
database.moveGroupTo(groupToMove, mNewParent)
|
database.moveGroupTo(groupToMove, mNewParent)
|
||||||
groupToMove.setPreviousParentGroup(mOldParent)
|
groupToMove.setPreviousParentGroup(mOldParent)
|
||||||
|
groupToMove.touch(modified = true, touchParents = true)
|
||||||
} else {
|
} else {
|
||||||
// Only finish thread
|
// Only finish thread
|
||||||
setError(MoveGroupDatabaseException())
|
setError(MoveGroupDatabaseException())
|
||||||
@@ -67,9 +68,9 @@ class MoveNodesRunnable constructor(
|
|||||||
if (mOldParent != mNewParent
|
if (mOldParent != mNewParent
|
||||||
// and root can contains entry
|
// and root can contains entry
|
||||||
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
|
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
|
||||||
entryToMove.touch(modified = true, touchParents = true)
|
|
||||||
database.moveEntryTo(entryToMove, mNewParent)
|
database.moveEntryTo(entryToMove, mNewParent)
|
||||||
entryToMove.setPreviousParentGroup(mOldParent)
|
entryToMove.setPreviousParentGroup(mOldParent)
|
||||||
|
entryToMove.touch(modified = true, touchParents = true)
|
||||||
} else {
|
} else {
|
||||||
// Only finish thread
|
// Only finish thread
|
||||||
setError(MoveEntryDatabaseException())
|
setError(MoveEntryDatabaseException())
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ class UpdateGroupRunnable constructor(
|
|||||||
// Update group with new values
|
// Update group with new values
|
||||||
mNewGroup.touch(modified = true, touchParents = true)
|
mNewGroup.touch(modified = true, touchParents = true)
|
||||||
|
|
||||||
|
if (database.rootGroup == mOldGroup) {
|
||||||
|
database.rootGroup = mNewGroup
|
||||||
|
}
|
||||||
// Only change data in index
|
// Only change data in index
|
||||||
database.updateGroup(mNewGroup)
|
database.updateGroup(mNewGroup)
|
||||||
}
|
}
|
||||||
@@ -50,6 +53,9 @@ class UpdateGroupRunnable constructor(
|
|||||||
override fun nodeFinish(): ActionNodesValues {
|
override fun nodeFinish(): ActionNodesValues {
|
||||||
if (!result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
// If we fail to save, back out changes to global structure
|
// If we fail to save, back out changes to global structure
|
||||||
|
if (database.rootGroup == mNewGroup) {
|
||||||
|
database.rootGroup = mOldGroup
|
||||||
|
}
|
||||||
database.updateGroup(mOldGroup)
|
database.updateGroup(mOldGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.database.cursor
|
|
||||||
|
|
||||||
import android.database.MatrixCursor
|
|
||||||
import android.provider.BaseColumns
|
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>> : MatrixCursor(arrayOf(
|
|
||||||
_ID,
|
|
||||||
COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS,
|
|
||||||
COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS,
|
|
||||||
COLUMN_INDEX_TITLE,
|
|
||||||
COLUMN_INDEX_ICON_STANDARD,
|
|
||||||
COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS,
|
|
||||||
COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS,
|
|
||||||
COLUMN_INDEX_USERNAME,
|
|
||||||
COLUMN_INDEX_PASSWORD,
|
|
||||||
COLUMN_INDEX_URL,
|
|
||||||
COLUMN_INDEX_NOTES,
|
|
||||||
COLUMN_INDEX_EXPIRY_TIME,
|
|
||||||
COLUMN_INDEX_EXPIRES
|
|
||||||
)) {
|
|
||||||
|
|
||||||
protected var entryId: Long = 0
|
|
||||||
|
|
||||||
abstract fun addEntry(entry: PwEntryV)
|
|
||||||
|
|
||||||
abstract fun getPwNodeId(): NodeId<EntryId>
|
|
||||||
|
|
||||||
open fun populateEntry(pwEntry: PwEntryV,
|
|
||||||
retrieveStandardIcon: (Int) -> IconImageStandard,
|
|
||||||
retrieveCustomIcon: (UUID) -> IconImageCustom) {
|
|
||||||
pwEntry.nodeId = getPwNodeId()
|
|
||||||
pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE))
|
|
||||||
|
|
||||||
val iconStandard = retrieveStandardIcon.invoke(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD)))
|
|
||||||
val iconCustom = retrieveCustomIcon.invoke(UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
|
|
||||||
getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
|
|
||||||
pwEntry.icon = IconImage(iconStandard, iconCustom)
|
|
||||||
|
|
||||||
pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME))
|
|
||||||
pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD))
|
|
||||||
pwEntry.url = getString(getColumnIndex(COLUMN_INDEX_URL))
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val _ID = BaseColumns._ID
|
|
||||||
const val COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS = "UUID_most_significant_bits"
|
|
||||||
const val COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS = "UUID_least_significant_bits"
|
|
||||||
const val COLUMN_INDEX_TITLE = "title"
|
|
||||||
const val COLUMN_INDEX_ICON_STANDARD = "icon_standard"
|
|
||||||
const val COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS = "icon_custom_UUID_most_significant_bits"
|
|
||||||
const val COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS = "icon_custom_UUID_least_significant_bits"
|
|
||||||
const val COLUMN_INDEX_USERNAME = "username"
|
|
||||||
const val COLUMN_INDEX_PASSWORD = "password"
|
|
||||||
const val COLUMN_INDEX_URL = "URL"
|
|
||||||
const val COLUMN_INDEX_NOTES = "notes"
|
|
||||||
const val COLUMN_INDEX_EXPIRY_TIME = "expiry_time"
|
|
||||||
const val COLUMN_INDEX_EXPIRES = "expires"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.database.cursor
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
|
||||||
|
|
||||||
class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
|
|
||||||
|
|
||||||
override fun addEntry(entry: EntryKDB) {
|
|
||||||
addRow(arrayOf(
|
|
||||||
entryId,
|
|
||||||
entry.id.mostSignificantBits,
|
|
||||||
entry.id.leastSignificantBits,
|
|
||||||
entry.title,
|
|
||||||
entry.icon.standard.id,
|
|
||||||
entry.icon.custom.uuid.mostSignificantBits,
|
|
||||||
entry.icon.custom.uuid.leastSignificantBits,
|
|
||||||
entry.username,
|
|
||||||
entry.password,
|
|
||||||
entry.url,
|
|
||||||
entry.notes,
|
|
||||||
entry.expiryTime,
|
|
||||||
entry.expires
|
|
||||||
))
|
|
||||||
entryId++
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.database.cursor
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
|
||||||
|
|
||||||
private val extraFieldCursor: ExtraFieldCursor = ExtraFieldCursor()
|
|
||||||
|
|
||||||
override fun addEntry(entry: EntryKDBX) {
|
|
||||||
addRow(arrayOf(
|
|
||||||
entryId,
|
|
||||||
entry.id.mostSignificantBits,
|
|
||||||
entry.id.leastSignificantBits,
|
|
||||||
entry.title,
|
|
||||||
entry.icon.standard.id,
|
|
||||||
entry.icon.custom.uuid.mostSignificantBits,
|
|
||||||
entry.icon.custom.uuid.leastSignificantBits,
|
|
||||||
entry.username,
|
|
||||||
entry.password,
|
|
||||||
entry.url,
|
|
||||||
entry.notes,
|
|
||||||
entry.expiryTime,
|
|
||||||
entry.expires
|
|
||||||
))
|
|
||||||
|
|
||||||
entry.doForEachDecodedCustomField { field ->
|
|
||||||
extraFieldCursor.addExtraField(entryId, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
entryId++
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun populateEntry(pwEntry: EntryKDBX,
|
|
||||||
retrieveStandardIcon: (Int) -> IconImageStandard,
|
|
||||||
retrieveCustomIcon: (UUID) -> IconImageCustom) {
|
|
||||||
super.populateEntry(pwEntry, retrieveStandardIcon, retrieveCustomIcon)
|
|
||||||
|
|
||||||
// Retrieve extra fields
|
|
||||||
if (extraFieldCursor.moveToFirst()) {
|
|
||||||
while (!extraFieldCursor.isAfterLast) {
|
|
||||||
// Add a new extra field only if entryId is the one we want
|
|
||||||
if (extraFieldCursor.getLong(extraFieldCursor
|
|
||||||
.getColumnIndex(ExtraFieldCursor.FOREIGN_KEY_ENTRY_ID))
|
|
||||||
== getLong(getColumnIndex(_ID))) {
|
|
||||||
extraFieldCursor.populateExtraFieldInEntry(pwEntry)
|
|
||||||
}
|
|
||||||
extraFieldCursor.moveToNext()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.database.cursor
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
abstract class EntryCursorUUID<EntryV: EntryVersioned<*, UUID, *, *>>: EntryCursor<UUID, EntryV>() {
|
|
||||||
|
|
||||||
override fun getPwNodeId(): NodeId<UUID> {
|
|
||||||
return NodeIdUUID(
|
|
||||||
UUID(getLong(getColumnIndex(COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
|
|
||||||
getLong(getColumnIndex(COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.database.cursor
|
|
||||||
|
|
||||||
import android.database.MatrixCursor
|
|
||||||
import android.provider.BaseColumns
|
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
|
||||||
|
|
||||||
class ExtraFieldCursor : MatrixCursor(arrayOf(
|
|
||||||
_ID,
|
|
||||||
FOREIGN_KEY_ENTRY_ID,
|
|
||||||
COLUMN_LABEL,
|
|
||||||
COLUMN_PROTECTION,
|
|
||||||
COLUMN_VALUE
|
|
||||||
)) {
|
|
||||||
|
|
||||||
private var fieldId: Long = 0
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun addExtraField(entryId: Long, field: Field) {
|
|
||||||
addRow(arrayOf(fieldId,
|
|
||||||
entryId,
|
|
||||||
field.name,
|
|
||||||
if (field.protectedValue.isProtected) 1 else 0,
|
|
||||||
field.protectedValue.toString()))
|
|
||||||
fieldId++
|
|
||||||
}
|
|
||||||
|
|
||||||
fun populateExtraFieldInEntry(pwEntry: EntryKDBX) {
|
|
||||||
pwEntry.putField(getString(getColumnIndex(COLUMN_LABEL)),
|
|
||||||
ProtectedString(getInt(getColumnIndex(COLUMN_PROTECTION)) > 0,
|
|
||||||
getString(getColumnIndex(COLUMN_VALUE))))
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val _ID = BaseColumns._ID
|
|
||||||
const val FOREIGN_KEY_ENTRY_ID = "entry_id"
|
|
||||||
const val COLUMN_LABEL = "label"
|
|
||||||
const val COLUMN_PROTECTION = "protection"
|
|
||||||
const val COLUMN_VALUE = "value"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,17 +22,16 @@ package com.kunzisoft.keepass.database.element
|
|||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
@@ -52,6 +51,7 @@ import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
|||||||
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
|
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
|
||||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
||||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
||||||
|
import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
@@ -94,6 +94,8 @@ class Database {
|
|||||||
*/
|
*/
|
||||||
var wasReloaded = false
|
var wasReloaded = false
|
||||||
|
|
||||||
|
var dataModifiedSinceLastLoading = false
|
||||||
|
|
||||||
var loadTimestamp: Long? = null
|
var loadTimestamp: Long? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ class Database {
|
|||||||
|
|
||||||
private val iconsManager: IconsManager
|
private val iconsManager: IconsManager
|
||||||
get() {
|
get() {
|
||||||
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
|
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
|
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
|
||||||
@@ -130,7 +132,7 @@ class Database {
|
|||||||
return iconsManager.doForEachCustomIcon(action)
|
return iconsManager.doForEachCustomIcon(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCustomIcon(iconId: UUID): IconImageCustom {
|
fun getCustomIcon(iconId: UUID): IconImageCustom? {
|
||||||
return iconsManager.getIcon(iconId)
|
return iconsManager.getIcon(iconId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,11 +146,12 @@ class Database {
|
|||||||
|
|
||||||
fun removeCustomIcon(customIcon: IconImageCustom) {
|
fun removeCustomIcon(customIcon: IconImageCustom) {
|
||||||
iconDrawableFactory.clearFromCache(customIcon)
|
iconDrawableFactory.clearFromCache(customIcon)
|
||||||
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
|
iconsManager.removeCustomIcon(customIcon.uuid, binaryCache)
|
||||||
|
mDatabaseKDBX?.addDeletedObject(customIcon.uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCustomIcon(customIcon: IconImageCustom) {
|
fun updateCustomIcon(customIcon: IconImageCustom) {
|
||||||
iconsManager.getIcon(customIcon.uuid).updateWith(customIcon)
|
iconsManager.getIcon(customIcon.uuid)?.updateWith(customIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTemplates(templateCreation: Boolean): List<Template> {
|
fun getTemplates(templateCreation: Boolean): List<Template> {
|
||||||
@@ -212,6 +215,7 @@ class Database {
|
|||||||
set(name) {
|
set(name) {
|
||||||
mDatabaseKDBX?.name = name
|
mDatabaseKDBX?.name = name
|
||||||
mDatabaseKDBX?.nameChanged = DateInstant()
|
mDatabaseKDBX?.nameChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowDescription: Boolean
|
val allowDescription: Boolean
|
||||||
@@ -224,33 +228,39 @@ class Database {
|
|||||||
set(description) {
|
set(description) {
|
||||||
mDatabaseKDBX?.description = description
|
mDatabaseKDBX?.description = description
|
||||||
mDatabaseKDBX?.descriptionChanged = DateInstant()
|
mDatabaseKDBX?.descriptionChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowDefaultUsername: Boolean
|
|
||||||
get() = mDatabaseKDBX != null
|
|
||||||
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
|
|
||||||
|
|
||||||
var defaultUsername: String
|
var defaultUsername: String
|
||||||
get() {
|
get() {
|
||||||
return mDatabaseKDBX?.defaultUserName ?: "" // TODO mDatabaseKDB default username
|
return mDatabaseKDB?.defaultUserName ?: mDatabaseKDBX?.defaultUserName ?: ""
|
||||||
}
|
}
|
||||||
set(username) {
|
set(username) {
|
||||||
|
mDatabaseKDB?.defaultUserName = username
|
||||||
mDatabaseKDBX?.defaultUserName = username
|
mDatabaseKDBX?.defaultUserName = username
|
||||||
mDatabaseKDBX?.defaultUserNameChanged = DateInstant()
|
mDatabaseKDBX?.defaultUserNameChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowCustomColor: Boolean
|
var customColor: Int?
|
||||||
get() = mDatabaseKDBX != null
|
|
||||||
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
|
|
||||||
|
|
||||||
// with format "#000000"
|
|
||||||
var customColor: String
|
|
||||||
get() {
|
get() {
|
||||||
return mDatabaseKDBX?.color ?: "" // TODO mDatabaseKDB color
|
var colorInt: Int? = null
|
||||||
|
mDatabaseKDBX?.color?.let {
|
||||||
|
try {
|
||||||
|
colorInt = Color.parseColor(it)
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
}
|
||||||
|
return mDatabaseKDB?.color ?: colorInt
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
// TODO Check color string
|
mDatabaseKDB?.color = value
|
||||||
mDatabaseKDBX?.color = value
|
mDatabaseKDBX?.color = if (value == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
ChromaUtil.getFormattedColorString(value, false)
|
||||||
|
}
|
||||||
|
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowOTP: Boolean
|
val allowOTP: Boolean
|
||||||
@@ -259,6 +269,12 @@ class Database {
|
|||||||
val version: String
|
val version: String
|
||||||
get() = mDatabaseKDB?.version ?: mDatabaseKDBX?.version ?: "-"
|
get() = mDatabaseKDB?.version ?: mDatabaseKDBX?.version ?: "-"
|
||||||
|
|
||||||
|
fun checkVersion() {
|
||||||
|
mDatabaseKDBX?.getMinKdbxVersion()?.let {
|
||||||
|
mDatabaseKDBX?.kdbxVersion = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val type: Class<*>?
|
val type: Class<*>?
|
||||||
get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass
|
get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass
|
||||||
|
|
||||||
@@ -274,6 +290,8 @@ class Database {
|
|||||||
value?.let {
|
value?.let {
|
||||||
mDatabaseKDBX?.compressionAlgorithm = it
|
mDatabaseKDBX?.compressionAlgorithm = it
|
||||||
}
|
}
|
||||||
|
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun compressionForNewEntry(): Boolean {
|
fun compressionForNewEntry(): Boolean {
|
||||||
@@ -290,6 +308,7 @@ class Database {
|
|||||||
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||||
newCompression: CompressionAlgorithm) {
|
newCompression: CompressionAlgorithm) {
|
||||||
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowNoMasterKey: Boolean
|
val allowNoMasterKey: Boolean
|
||||||
@@ -309,8 +328,6 @@ class Database {
|
|||||||
set(algorithm) {
|
set(algorithm) {
|
||||||
algorithm?.let {
|
algorithm?.let {
|
||||||
mDatabaseKDBX?.encryptionAlgorithm = algorithm
|
mDatabaseKDBX?.encryptionAlgorithm = algorithm
|
||||||
mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine)
|
|
||||||
mDatabaseKDBX?.cipherUuid = algorithm.uuid
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,13 +340,10 @@ class Database {
|
|||||||
var kdfEngine: KdfEngine?
|
var kdfEngine: KdfEngine?
|
||||||
get() = mDatabaseKDB?.kdfEngine ?: mDatabaseKDBX?.kdfEngine
|
get() = mDatabaseKDB?.kdfEngine ?: mDatabaseKDBX?.kdfEngine
|
||||||
set(kdfEngine) {
|
set(kdfEngine) {
|
||||||
kdfEngine?.let {
|
mDatabaseKDB?.kdfEngine = kdfEngine
|
||||||
if (mDatabaseKDBX?.kdfParameters?.uuid != kdfEngine.defaultParameters.uuid)
|
mDatabaseKDBX?.kdfEngine = kdfEngine
|
||||||
mDatabaseKDBX?.kdfParameters = kdfEngine.defaultParameters
|
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||||
numberKeyEncryptionRounds = kdfEngine.defaultKeyRounds
|
dataModifiedSinceLastLoading = true
|
||||||
memoryUsage = kdfEngine.defaultMemoryUsage
|
|
||||||
parallelism = kdfEngine.defaultParallelism
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKeyDerivationName(): String {
|
fun getKeyDerivationName(): String {
|
||||||
@@ -341,6 +355,8 @@ class Database {
|
|||||||
set(numberRounds) {
|
set(numberRounds) {
|
||||||
mDatabaseKDB?.numberKeyEncryptionRounds = numberRounds
|
mDatabaseKDB?.numberKeyEncryptionRounds = numberRounds
|
||||||
mDatabaseKDBX?.numberKeyEncryptionRounds = numberRounds
|
mDatabaseKDBX?.numberKeyEncryptionRounds = numberRounds
|
||||||
|
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryUsage: Long
|
var memoryUsage: Long
|
||||||
@@ -349,12 +365,16 @@ class Database {
|
|||||||
}
|
}
|
||||||
set(memory) {
|
set(memory) {
|
||||||
mDatabaseKDBX?.memoryUsage = memory
|
mDatabaseKDBX?.memoryUsage = memory
|
||||||
|
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var parallelism: Long
|
var parallelism: Long
|
||||||
get() = mDatabaseKDBX?.parallelism ?: KdfEngine.UNKNOWN_VALUE
|
get() = mDatabaseKDBX?.parallelism ?: KdfEngine.UNKNOWN_VALUE
|
||||||
set(parallelism) {
|
set(parallelism) {
|
||||||
mDatabaseKDBX?.parallelism = parallelism
|
mDatabaseKDBX?.parallelism = parallelism
|
||||||
|
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var masterKey: ByteArray
|
var masterKey: ByteArray
|
||||||
@@ -362,9 +382,11 @@ class Database {
|
|||||||
set(masterKey) {
|
set(masterKey) {
|
||||||
mDatabaseKDB?.masterKey = masterKey
|
mDatabaseKDB?.masterKey = masterKey
|
||||||
mDatabaseKDBX?.masterKey = masterKey
|
mDatabaseKDBX?.masterKey = masterKey
|
||||||
|
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val rootGroup: Group?
|
var rootGroup: Group?
|
||||||
get() {
|
get() {
|
||||||
mDatabaseKDB?.rootGroup?.let {
|
mDatabaseKDB?.rootGroup?.let {
|
||||||
return Group(it)
|
return Group(it)
|
||||||
@@ -374,6 +396,25 @@ class Database {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
set(value) {
|
||||||
|
value?.groupKDB?.let { rootKDB ->
|
||||||
|
mDatabaseKDB?.rootGroup = rootKDB
|
||||||
|
}
|
||||||
|
value?.groupKDBX?.let { rootKDBX ->
|
||||||
|
mDatabaseKDBX?.rootGroup = rootKDBX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val rootGroupIsVirtual: Boolean
|
||||||
|
get() {
|
||||||
|
mDatabaseKDB?.let {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
mDatabaseKDBX?.let {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do not modify groups here, used for read only
|
* Do not modify groups here, used for read only
|
||||||
@@ -393,6 +434,8 @@ class Database {
|
|||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
mDatabaseKDBX?.historyMaxItems = value
|
mDatabaseKDBX?.historyMaxItems = value
|
||||||
|
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var historyMaxSize: Long
|
var historyMaxSize: Long
|
||||||
@@ -401,6 +444,8 @@ class Database {
|
|||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
mDatabaseKDBX?.historyMaxSize = value
|
mDatabaseKDBX?.historyMaxSize = value
|
||||||
|
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -421,15 +466,17 @@ class Database {
|
|||||||
} else {
|
} else {
|
||||||
mDatabaseKDBX?.removeRecycleBin()
|
mDatabaseKDBX?.removeRecycleBin()
|
||||||
}
|
}
|
||||||
|
mDatabaseKDBX?.recycleBinChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val recycleBin: Group?
|
val recycleBin: Group?
|
||||||
get() {
|
get() {
|
||||||
mDatabaseKDB?.backupGroup?.let {
|
mDatabaseKDB?.backupGroup?.let {
|
||||||
return Group(it)
|
return getGroupById(it.nodeId) ?: Group(it)
|
||||||
}
|
}
|
||||||
mDatabaseKDBX?.recycleBin?.let {
|
mDatabaseKDBX?.recycleBin?.let {
|
||||||
return Group(it)
|
return getGroupById(it.nodeId) ?: Group(it)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -439,8 +486,10 @@ class Database {
|
|||||||
if (group != null) {
|
if (group != null) {
|
||||||
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
|
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
|
||||||
} else {
|
} else {
|
||||||
mDatabaseKDBX?.removeTemplatesGroup()
|
mDatabaseKDBX?.removeRecycleBin()
|
||||||
}
|
}
|
||||||
|
mDatabaseKDBX?.recycleBinChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -456,6 +505,8 @@ class Database {
|
|||||||
|
|
||||||
fun enableTemplates(enable: Boolean, templatesGroupName: String) {
|
fun enableTemplates(enable: Boolean, templatesGroupName: String) {
|
||||||
mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName)
|
mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName)
|
||||||
|
mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val templatesGroup: Group?
|
val templatesGroup: Group?
|
||||||
@@ -471,8 +522,10 @@ class Database {
|
|||||||
if (group != null) {
|
if (group != null) {
|
||||||
mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id
|
mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id
|
||||||
} else {
|
} else {
|
||||||
mDatabaseKDBX?.entryTemplatesGroup
|
mDatabaseKDBX?.removeTemplatesGroup()
|
||||||
}
|
}
|
||||||
|
mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val groupNamesNotAllowed: List<String>
|
val groupNamesNotAllowed: List<String>
|
||||||
@@ -499,6 +552,7 @@ class Database {
|
|||||||
this.fileUri = databaseUri
|
this.fileUri = databaseUri
|
||||||
// Set Database state
|
// Set Database state
|
||||||
this.loaded = true
|
this.loaded = true
|
||||||
|
this.dataModifiedSinceLastLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
@@ -555,7 +609,6 @@ class Database {
|
|||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
cacheDirectory: File,
|
cacheDirectory: File,
|
||||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||||
tempCipherKey: LoadedKey,
|
|
||||||
fixDuplicateUUID: Boolean,
|
fixDuplicateUUID: Boolean,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
@@ -576,73 +629,156 @@ class Database {
|
|||||||
// Read database stream for the first time
|
// Read database stream for the first time
|
||||||
readDatabaseStream(contentResolver, uri,
|
readDatabaseStream(contentResolver, uri,
|
||||||
{ databaseInputStream ->
|
{ databaseInputStream ->
|
||||||
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
|
val databaseKDB = DatabaseKDB().apply {
|
||||||
.openDatabase(databaseInputStream,
|
binaryCache.cacheDirectory = cacheDirectory
|
||||||
mainCredential.masterPassword,
|
changeDuplicateId = fixDuplicateUUID
|
||||||
keyFileInputStream,
|
}
|
||||||
tempCipherKey,
|
DatabaseInputKDB(databaseKDB)
|
||||||
progressTaskUpdater,
|
.openDatabase(databaseInputStream,
|
||||||
fixDuplicateUUID)
|
mainCredential.masterPassword,
|
||||||
|
keyFileInputStream,
|
||||||
|
progressTaskUpdater)
|
||||||
|
databaseKDB
|
||||||
},
|
},
|
||||||
{ databaseInputStream ->
|
{ databaseInputStream ->
|
||||||
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
|
val databaseKDBX = DatabaseKDBX().apply {
|
||||||
.openDatabase(databaseInputStream,
|
binaryCache.cacheDirectory = cacheDirectory
|
||||||
mainCredential.masterPassword,
|
changeDuplicateId = fixDuplicateUUID
|
||||||
keyFileInputStream,
|
}
|
||||||
tempCipherKey,
|
DatabaseInputKDBX(databaseKDBX).apply {
|
||||||
progressTaskUpdater,
|
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||||
fixDuplicateUUID)
|
openDatabase(databaseInputStream,
|
||||||
|
mainCredential.masterPassword,
|
||||||
|
keyFileInputStream,
|
||||||
|
progressTaskUpdater)
|
||||||
|
}
|
||||||
|
databaseKDBX
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
Log.e(TAG, "Unable to load keyfile", e)
|
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||||
throw FileNotFoundDatabaseException()
|
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw LoadDatabaseException(e)
|
throw LoadDatabaseException(e)
|
||||||
} finally {
|
} finally {
|
||||||
keyFileInputStream?.close()
|
keyFileInputStream?.close()
|
||||||
|
dataModifiedSinceLastLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isMergeDataAllowed(): Boolean {
|
||||||
|
return mDatabaseKDBX != null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(LoadDatabaseException::class)
|
||||||
|
fun mergeData(contentResolver: ContentResolver,
|
||||||
|
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||||
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
|
mDatabaseKDB?.let {
|
||||||
|
throw IODatabaseException("Unable to merge from a database V1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// New database instance to get new changes
|
||||||
|
val databaseToMerge = Database()
|
||||||
|
databaseToMerge.fileUri = this.fileUri
|
||||||
|
|
||||||
|
try {
|
||||||
|
databaseToMerge.fileUri?.let { databaseUri ->
|
||||||
|
|
||||||
|
val databaseKDB = DatabaseKDB()
|
||||||
|
val databaseKDBX = DatabaseKDBX()
|
||||||
|
|
||||||
|
databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
|
||||||
|
{ databaseInputStream ->
|
||||||
|
DatabaseInputKDB(databaseKDB)
|
||||||
|
.openDatabase(databaseInputStream,
|
||||||
|
masterKey,
|
||||||
|
progressTaskUpdater)
|
||||||
|
databaseKDB
|
||||||
|
},
|
||||||
|
{ databaseInputStream ->
|
||||||
|
DatabaseInputKDBX(databaseKDBX).apply {
|
||||||
|
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||||
|
openDatabase(databaseInputStream,
|
||||||
|
masterKey,
|
||||||
|
progressTaskUpdater)
|
||||||
|
}
|
||||||
|
databaseKDBX
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
mDatabaseKDBX?.let { currentDatabaseKDBX ->
|
||||||
|
val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply {
|
||||||
|
this.isRAMSufficient = isRAMSufficient
|
||||||
|
}
|
||||||
|
databaseToMerge.mDatabaseKDB?.let { databaseKDBToMerge ->
|
||||||
|
databaseMerger.merge(databaseKDBToMerge)
|
||||||
|
}
|
||||||
|
databaseToMerge.mDatabaseKDBX?.let { databaseKDBXToMerge ->
|
||||||
|
databaseMerger.merge(databaseKDBXToMerge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
throw IODatabaseException("Database URI is null, database cannot be reloaded")
|
||||||
|
}
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||||
|
} catch (e: LoadDatabaseException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw LoadDatabaseException(e)
|
||||||
|
} finally {
|
||||||
|
databaseToMerge.clearAndClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
fun reloadData(contentResolver: ContentResolver,
|
fun reloadData(contentResolver: ContentResolver,
|
||||||
cacheDirectory: File,
|
|
||||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||||
tempCipherKey: LoadedKey,
|
|
||||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
// Retrieve the stream from the old database URI
|
// Retrieve the stream from the old database URI
|
||||||
try {
|
try {
|
||||||
fileUri?.let { oldDatabaseUri ->
|
fileUri?.let { oldDatabaseUri ->
|
||||||
readDatabaseStream(contentResolver, oldDatabaseUri,
|
readDatabaseStream(contentResolver, oldDatabaseUri,
|
||||||
{ databaseInputStream ->
|
{ databaseInputStream ->
|
||||||
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
|
val databaseKDB = DatabaseKDB()
|
||||||
.openDatabase(databaseInputStream,
|
mDatabaseKDB?.let {
|
||||||
masterKey,
|
databaseKDB.binaryCache = it.binaryCache
|
||||||
tempCipherKey,
|
|
||||||
progressTaskUpdater)
|
|
||||||
},
|
|
||||||
{ databaseInputStream ->
|
|
||||||
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
|
|
||||||
.openDatabase(databaseInputStream,
|
|
||||||
masterKey,
|
|
||||||
tempCipherKey,
|
|
||||||
progressTaskUpdater)
|
|
||||||
}
|
}
|
||||||
|
DatabaseInputKDB(databaseKDB)
|
||||||
|
.openDatabase(databaseInputStream,
|
||||||
|
masterKey,
|
||||||
|
progressTaskUpdater)
|
||||||
|
databaseKDB
|
||||||
|
},
|
||||||
|
{ databaseInputStream ->
|
||||||
|
val databaseKDBX = DatabaseKDBX()
|
||||||
|
mDatabaseKDBX?.let {
|
||||||
|
databaseKDBX.binaryCache = it.binaryCache
|
||||||
|
}
|
||||||
|
DatabaseInputKDBX(databaseKDBX).apply {
|
||||||
|
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||||
|
openDatabase(databaseInputStream,
|
||||||
|
masterKey,
|
||||||
|
progressTaskUpdater)
|
||||||
|
}
|
||||||
|
databaseKDBX
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
Log.e(TAG, "Database URI is null, database cannot be reloaded")
|
throw IODatabaseException("Database URI is null, database cannot be reloaded")
|
||||||
throw IODatabaseException()
|
|
||||||
}
|
}
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
Log.e(TAG, "Unable to load keyfile", e)
|
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||||
throw FileNotFoundDatabaseException()
|
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw LoadDatabaseException(e)
|
throw LoadDatabaseException(e)
|
||||||
|
} finally {
|
||||||
|
dataModifiedSinceLastLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,7 +818,7 @@ class Database {
|
|||||||
|
|
||||||
val attachmentPool: AttachmentPool
|
val attachmentPool: AttachmentPool
|
||||||
get() {
|
get() {
|
||||||
return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool(binaryCache)
|
return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool()
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowMultipleAttachments: Boolean
|
val allowMultipleAttachments: Boolean
|
||||||
@@ -695,8 +831,8 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewBinaryAttachment(): BinaryData? {
|
fun buildNewBinaryAttachment(): BinaryData? {
|
||||||
return mDatabaseKDB?.buildNewAttachment()
|
return mDatabaseKDB?.buildNewBinaryAttachment()
|
||||||
?: mDatabaseKDBX?.buildNewAttachment( false,
|
?: mDatabaseKDBX?.buildNewBinaryAttachment( false,
|
||||||
compressionForNewEntry(),
|
compressionForNewEntry(),
|
||||||
false)
|
false)
|
||||||
}
|
}
|
||||||
@@ -710,6 +846,7 @@ class Database {
|
|||||||
fun removeUnlinkedAttachments() {
|
fun removeUnlinkedAttachments() {
|
||||||
// No check in database KDB because unique attachment by entry
|
// No check in database KDB because unique attachment by entry
|
||||||
mDatabaseKDBX?.removeUnlinkedAttachments(true)
|
mDatabaseKDBX?.removeUnlinkedAttachments(true)
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
@@ -770,16 +907,25 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.fileUri = uri
|
this.fileUri = uri
|
||||||
|
this.dataModifiedSinceLastLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear(filesDirectory: File? = null) {
|
fun clearIndexesAndBinaries(filesDirectory: File? = null) {
|
||||||
binaryCache.clear()
|
this.mDatabaseKDB?.clearIndexes()
|
||||||
iconsManager.clearCache()
|
this.mDatabaseKDBX?.clearIndexes()
|
||||||
|
|
||||||
|
this.mDatabaseKDB?.clearIconsCache()
|
||||||
|
this.mDatabaseKDBX?.clearIconsCache()
|
||||||
|
|
||||||
|
this.mDatabaseKDB?.clearAttachmentsCache()
|
||||||
|
this.mDatabaseKDBX?.clearAttachmentsCache()
|
||||||
|
|
||||||
|
this.mDatabaseKDB?.clearBinaries()
|
||||||
|
this.mDatabaseKDBX?.clearBinaries()
|
||||||
|
|
||||||
iconDrawableFactory.clearCache()
|
iconDrawableFactory.clearCache()
|
||||||
// Delete the cache of the database if present
|
|
||||||
mDatabaseKDB?.clearCache()
|
// delete all the files in the temp dir if allowed
|
||||||
mDatabaseKDBX?.clearCache()
|
|
||||||
// In all cases, delete all the files in the temp dir
|
|
||||||
try {
|
try {
|
||||||
filesDirectory?.let { directory ->
|
filesDirectory?.let { directory ->
|
||||||
cleanDirectory(directory)
|
cleanDirectory(directory)
|
||||||
@@ -790,7 +936,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clearAndClose(context: Context? = null) {
|
fun clearAndClose(context: Context? = null) {
|
||||||
clear(context?.let { UriUtil.getBinaryDir(context) })
|
clearIndexesAndBinaries(context?.let { UriUtil.getBinaryDir(context) })
|
||||||
this.mDatabaseKDB = null
|
this.mDatabaseKDB = null
|
||||||
this.mDatabaseKDBX = null
|
this.mDatabaseKDBX = null
|
||||||
this.fileUri = null
|
this.fileUri = null
|
||||||
@@ -817,9 +963,10 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
|
fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||||
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
|
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
|
||||||
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
|
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
|
||||||
|
mDatabaseKDBX?.keyLastChanged = DateInstant()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rootCanContainsEntry(): Boolean {
|
fun rootCanContainsEntry(): Boolean {
|
||||||
@@ -827,6 +974,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createEntry(): Entry? {
|
fun createEntry(): Entry? {
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
mDatabaseKDB?.let { database ->
|
mDatabaseKDB?.let { database ->
|
||||||
return Entry(database.createEntry()).apply {
|
return Entry(database.createEntry()).apply {
|
||||||
nodeId = database.newEntryId()
|
nodeId = database.newEntryId()
|
||||||
@@ -842,6 +990,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createGroup(): Group? {
|
fun createGroup(): Group? {
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
mDatabaseKDB?.let { database ->
|
mDatabaseKDB?.let { database ->
|
||||||
return Group(database.createGroup()).apply {
|
return Group(database.createGroup()).apply {
|
||||||
setNodeId(database.newGroupId())
|
setNodeId(database.newGroupId())
|
||||||
@@ -879,6 +1028,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addEntryTo(entry: Entry, parent: Group) {
|
fun addEntryTo(entry: Entry, parent: Group) {
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
entry.entryKDB?.let { entryKDB ->
|
entry.entryKDB?.let { entryKDB ->
|
||||||
mDatabaseKDB?.addEntryTo(entryKDB, parent.groupKDB)
|
mDatabaseKDB?.addEntryTo(entryKDB, parent.groupKDB)
|
||||||
}
|
}
|
||||||
@@ -889,6 +1039,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateEntry(entry: Entry) {
|
fun updateEntry(entry: Entry) {
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
entry.entryKDB?.let { entryKDB ->
|
entry.entryKDB?.let { entryKDB ->
|
||||||
mDatabaseKDB?.updateEntry(entryKDB)
|
mDatabaseKDB?.updateEntry(entryKDB)
|
||||||
}
|
}
|
||||||
@@ -898,6 +1049,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntryFrom(entry: Entry, parent: Group) {
|
fun removeEntryFrom(entry: Entry, parent: Group) {
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
entry.entryKDB?.let { entryKDB ->
|
entry.entryKDB?.let { entryKDB ->
|
||||||
mDatabaseKDB?.removeEntryFrom(entryKDB, parent.groupKDB)
|
mDatabaseKDB?.removeEntryFrom(entryKDB, parent.groupKDB)
|
||||||
}
|
}
|
||||||
@@ -908,6 +1060,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addGroupTo(group: Group, parent: Group) {
|
fun addGroupTo(group: Group, parent: Group) {
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
group.groupKDB?.let { groupKDB ->
|
group.groupKDB?.let { groupKDB ->
|
||||||
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
|
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
|
||||||
}
|
}
|
||||||
@@ -918,6 +1071,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateGroup(group: Group) {
|
fun updateGroup(group: Group) {
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
group.groupKDB?.let { entryKDB ->
|
group.groupKDB?.let { entryKDB ->
|
||||||
mDatabaseKDB?.updateGroup(entryKDB)
|
mDatabaseKDB?.updateGroup(entryKDB)
|
||||||
}
|
}
|
||||||
@@ -927,6 +1081,7 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeGroupFrom(group: Group, parent: Group) {
|
fun removeGroupFrom(group: Group, parent: Group) {
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
group.groupKDB?.let { groupKDB ->
|
group.groupKDB?.let { groupKDB ->
|
||||||
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
|
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
|
||||||
}
|
}
|
||||||
@@ -965,12 +1120,17 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteEntry(entry: Entry) {
|
fun deleteEntry(entry: Entry) {
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
|
entry.entryKDBX?.id?.let { entryId ->
|
||||||
|
mDatabaseKDBX?.addDeletedObject(entryId)
|
||||||
|
}
|
||||||
entry.parent?.let {
|
entry.parent?.let {
|
||||||
removeEntryFrom(entry, it)
|
removeEntryFrom(entry, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteGroup(group: Group) {
|
fun deleteGroup(group: Group) {
|
||||||
|
dataModifiedSinceLastLoading = true
|
||||||
group.doForEachChildAndForIt(
|
group.doForEachChildAndForIt(
|
||||||
object : NodeHandler<Entry>() {
|
object : NodeHandler<Entry>() {
|
||||||
override fun operate(node: Entry): Boolean {
|
override fun operate(node: Entry): Boolean {
|
||||||
@@ -980,6 +1140,9 @@ class Database {
|
|||||||
},
|
},
|
||||||
object : NodeHandler<Group>() {
|
object : NodeHandler<Group>() {
|
||||||
override fun operate(node: Group): Boolean {
|
override fun operate(node: Group): Boolean {
|
||||||
|
node.groupKDBX?.id?.let { groupId ->
|
||||||
|
mDatabaseKDBX?.addDeletedObject(groupId)
|
||||||
|
}
|
||||||
node.parent?.let {
|
node.parent?.let {
|
||||||
removeGroupFrom(node, it)
|
removeGroupFrom(node, it)
|
||||||
}
|
}
|
||||||
@@ -988,24 +1151,6 @@ class Database {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun undoDeleteEntry(entry: Entry, parent: Group) {
|
|
||||||
entry.entryKDB?.let {
|
|
||||||
mDatabaseKDB?.undoDeleteEntryFrom(it, parent.groupKDB)
|
|
||||||
}
|
|
||||||
entry.entryKDBX?.let {
|
|
||||||
mDatabaseKDBX?.undoDeleteEntryFrom(it, parent.groupKDBX)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun undoDeleteGroup(group: Group, parent: Group) {
|
|
||||||
group.groupKDB?.let {
|
|
||||||
mDatabaseKDB?.undoDeleteGroupFrom(it, parent.groupKDB)
|
|
||||||
}
|
|
||||||
group.groupKDBX?.let {
|
|
||||||
mDatabaseKDBX?.undoDeleteGroupFrom(it, parent.groupKDBX)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ensureRecycleBinExists(resources: Resources) {
|
fun ensureRecycleBinExists(resources: Resources) {
|
||||||
mDatabaseKDB?.ensureBackupExists()
|
mDatabaseKDB?.ensureBackupExists()
|
||||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||||
@@ -1034,47 +1179,41 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun recycle(entry: Entry, resources: Resources) {
|
fun recycle(entry: Entry, resources: Resources) {
|
||||||
entry.entryKDB?.let {
|
ensureRecycleBinExists(resources)
|
||||||
mDatabaseKDB?.recycle(it)
|
entry.parent?.let { parent ->
|
||||||
|
removeEntryFrom(entry, parent)
|
||||||
}
|
}
|
||||||
entry.entryKDBX?.let {
|
recycleBin?.let {
|
||||||
mDatabaseKDBX?.recycle(it, resources)
|
addEntryTo(entry, it)
|
||||||
}
|
}
|
||||||
|
entry.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle(group: Group, resources: Resources) {
|
fun recycle(group: Group, resources: Resources) {
|
||||||
group.groupKDB?.let {
|
ensureRecycleBinExists(resources)
|
||||||
mDatabaseKDB?.recycle(it)
|
group.parent?.let { parent ->
|
||||||
|
removeGroupFrom(group, parent)
|
||||||
}
|
}
|
||||||
group.groupKDBX?.let {
|
recycleBin?.let {
|
||||||
mDatabaseKDBX?.recycle(it, resources)
|
addGroupTo(group, it)
|
||||||
}
|
}
|
||||||
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun undoRecycle(entry: Entry, parent: Group) {
|
fun undoRecycle(entry: Entry, parent: Group) {
|
||||||
entry.entryKDB?.let { entryKDB ->
|
recycleBin?.let { it ->
|
||||||
parent.groupKDB?.let { parentKDB ->
|
removeEntryFrom(entry, it)
|
||||||
mDatabaseKDB?.undoRecycle(entryKDB, parentKDB)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entry.entryKDBX?.let { entryKDBX ->
|
|
||||||
parent.groupKDBX?.let { parentKDBX ->
|
|
||||||
mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
addEntryTo(entry, parent)
|
||||||
|
entry.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun undoRecycle(group: Group, parent: Group) {
|
fun undoRecycle(group: Group, parent: Group) {
|
||||||
group.groupKDB?.let { groupKDB ->
|
recycleBin?.let {
|
||||||
parent.groupKDB?.let { parentKDB ->
|
removeGroupFrom(group, it)
|
||||||
mDatabaseKDB?.undoRecycle(groupKDB, parentKDB)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
group.groupKDBX?.let { entryKDBX ->
|
|
||||||
parent.groupKDBX?.let { parentKDBX ->
|
|
||||||
mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
addGroupTo(group, parent)
|
||||||
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startManageEntry(entry: Entry?) {
|
fun startManageEntry(entry: Entry?) {
|
||||||
|
|||||||
@@ -28,29 +28,18 @@ import java.util.*
|
|||||||
class DeletedObject : Parcelable {
|
class DeletedObject : Parcelable {
|
||||||
|
|
||||||
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
||||||
private var mDeletionTime: DateInstant? = null
|
var deletionTime: DateInstant = DateInstant()
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
|
|
||||||
constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) {
|
constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) {
|
||||||
this.uuid = uuid
|
this.uuid = uuid
|
||||||
this.mDeletionTime = deletionTime
|
this.deletionTime = deletionTime
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(parcel: Parcel) {
|
constructor(parcel: Parcel) {
|
||||||
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
|
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
|
||||||
mDeletionTime = parcel.readParcelable(DateInstant::class.java.classLoader)
|
deletionTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: deletionTime
|
||||||
}
|
|
||||||
|
|
||||||
fun getDeletionTime(): DateInstant {
|
|
||||||
if (mDeletionTime == null) {
|
|
||||||
mDeletionTime = DateInstant(System.currentTimeMillis())
|
|
||||||
}
|
|
||||||
return mDeletionTime!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDeletionTime(deletionTime: DateInstant) {
|
|
||||||
this.mDeletionTime = deletionTime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@@ -69,7 +58,7 @@ class DeletedObject : Parcelable {
|
|||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeParcelable(ParcelUuid(uuid), flags)
|
parcel.writeParcelable(ParcelUuid(uuid), flags)
|
||||||
parcel.writeParcelable(mDeletionTime, flags)
|
parcel.writeParcelable(deletionTime, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
|
|||||||
@@ -19,8 +19,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element
|
package com.kunzisoft.keepass.database.element
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.androidclearchroma.ChromaUtil
|
||||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
@@ -238,6 +240,42 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
entryKDBX?.notes = value
|
entryKDBX?.notes = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var backgroundColor: Int?
|
||||||
|
get() {
|
||||||
|
var colorInt: Int? = null
|
||||||
|
entryKDBX?.backgroundColor?.let {
|
||||||
|
try {
|
||||||
|
colorInt = Color.parseColor(it)
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
}
|
||||||
|
return colorInt
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryKDBX?.backgroundColor = if (value == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
ChromaUtil.getFormattedColorString(value, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var foregroundColor: Int?
|
||||||
|
get() {
|
||||||
|
var colorInt: Int? = null
|
||||||
|
entryKDBX?.foregroundColor?.let {
|
||||||
|
try {
|
||||||
|
colorInt = Color.parseColor(it)
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
}
|
||||||
|
return colorInt
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryKDBX?.foregroundColor = if (value == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
ChromaUtil.getFormattedColorString(value, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun isTan(): Boolean {
|
private fun isTan(): Boolean {
|
||||||
return title == PMS_TAN_ENTRY && username.isNotEmpty()
|
return title == PMS_TAN_ENTRY && username.isNotEmpty()
|
||||||
}
|
}
|
||||||
@@ -420,6 +458,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
entryInfo.url = url
|
entryInfo.url = url
|
||||||
entryInfo.notes = notes
|
entryInfo.notes = notes
|
||||||
entryInfo.tags = tags
|
entryInfo.tags = tags
|
||||||
|
entryInfo.backgroundColor = backgroundColor
|
||||||
|
entryInfo.foregroundColor = foregroundColor
|
||||||
entryInfo.customFields = getExtraFields().toMutableList()
|
entryInfo.customFields = getExtraFields().toMutableList()
|
||||||
// Add otpElement to generate token
|
// Add otpElement to generate token
|
||||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||||
@@ -455,6 +495,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
url = newEntryInfo.url
|
url = newEntryInfo.url
|
||||||
notes = newEntryInfo.notes
|
notes = newEntryInfo.notes
|
||||||
tags = newEntryInfo.tags
|
tags = newEntryInfo.tags
|
||||||
|
backgroundColor = newEntryInfo.backgroundColor
|
||||||
|
foregroundColor = newEntryInfo.foregroundColor
|
||||||
addExtraFields(newEntryInfo.customFields)
|
addExtraFields(newEntryInfo.customFields)
|
||||||
database?.attachmentPool?.let { binaryPool ->
|
database?.attachmentPool?.let { binaryPool ->
|
||||||
newEntryInfo.attachments.forEach { attachment ->
|
newEntryInfo.attachments.forEach { attachment ->
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import com.kunzisoft.keepass.database.element.node.*
|
|||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
@@ -308,8 +309,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
val withoutMetaStream = filters.contains(ChildFilter.META_STREAM)
|
val withoutMetaStream = filters.contains(ChildFilter.META_STREAM)
|
||||||
val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED)
|
val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED)
|
||||||
|
|
||||||
|
// TODO Change KDB parser to remove meta entries
|
||||||
return groupKDB?.getChildEntries()?.filter {
|
return groupKDB?.getChildEntries()?.filter {
|
||||||
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
|
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream()))
|
||||||
&& (!it.isCurrentlyExpires or showExpiredEntries)
|
&& (!it.isCurrentlyExpires or showExpiredEntries)
|
||||||
}?.map {
|
}?.map {
|
||||||
Entry(it)
|
Entry(it)
|
||||||
@@ -453,6 +455,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
|
|
||||||
fun getGroupInfo(): GroupInfo {
|
fun getGroupInfo(): GroupInfo {
|
||||||
val groupInfo = GroupInfo()
|
val groupInfo = GroupInfo()
|
||||||
|
groupInfo.id = groupKDBX?.nodeId?.id
|
||||||
groupInfo.title = title
|
groupInfo.title = title
|
||||||
groupInfo.icon = icon
|
groupInfo.icon = icon
|
||||||
groupInfo.creationTime = creationTime
|
groupInfo.creationTime = creationTime
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.binary
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
class AttachmentPool(binaryCache: BinaryCache) : BinaryPool<Int>(binaryCache) {
|
class AttachmentPool : BinaryPool<Int>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility method to find an unused key in the pool
|
* Utility method to find an unused key in the pool
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import android.util.Log
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
|
abstract class BinaryPool<T> {
|
||||||
|
|
||||||
protected val pool = LinkedHashMap<T, BinaryData>()
|
protected val pool = LinkedHashMap<T, BinaryData>()
|
||||||
|
|
||||||
@@ -225,9 +225,6 @@ abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun clear() {
|
fun clear() {
|
||||||
doForEachBinary { _, binary ->
|
|
||||||
binary.clear(mBinaryCache)
|
|
||||||
}
|
|
||||||
pool.clear()
|
pool.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,16 @@ import com.kunzisoft.keepass.database.element.DateInstant
|
|||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
|
class CustomIconPool : BinaryPool<UUID>() {
|
||||||
|
|
||||||
private val customIcons = HashMap<UUID, IconImageCustom>()
|
private val customIcons = HashMap<UUID, IconImageCustom>()
|
||||||
|
|
||||||
fun put(key: UUID? = null,
|
fun put(key: UUID? = null,
|
||||||
name: String,
|
name: String,
|
||||||
lastModificationTime: DateInstant?,
|
lastModificationTime: DateInstant?,
|
||||||
smallSize: Boolean,
|
builder: (uniqueBinaryId: String) -> BinaryData,
|
||||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
val keyBinary = super.put(key) { uniqueBinaryId ->
|
val keyBinary = super.put(key, builder)
|
||||||
// Create a byte array for better performance with small data
|
|
||||||
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
|
|
||||||
}
|
|
||||||
val uuid = keyBinary.keys.first()
|
val uuid = keyBinary.keys.first()
|
||||||
val customIcon = IconImageCustom(uuid, name, lastModificationTime)
|
val customIcon = IconImageCustom(uuid, name, lastModificationTime)
|
||||||
customIcons[uuid] = customIcon
|
customIcons[uuid] = customIcon
|
||||||
|
|||||||
@@ -34,23 +34,41 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||||
|
|
||||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
override var encryptionAlgorithm: EncryptionAlgorithm = EncryptionAlgorithm.AESRijndael
|
||||||
|
|
||||||
|
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm> = listOf(
|
||||||
|
EncryptionAlgorithm.AESRijndael,
|
||||||
|
EncryptionAlgorithm.Twofish
|
||||||
|
)
|
||||||
|
|
||||||
|
override var kdfEngine: KdfEngine?
|
||||||
|
get() = kdfAvailableList[0]
|
||||||
|
set(value) {
|
||||||
|
value?.let {
|
||||||
|
numberKeyEncryptionRounds = value.defaultKeyRounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val kdfAvailableList: List<KdfEngine> = listOf(
|
||||||
|
KdfFactory.aesKdf
|
||||||
|
)
|
||||||
|
|
||||||
|
override val passwordEncoding: String
|
||||||
|
get() = "ISO-8859-1"
|
||||||
|
|
||||||
|
override var numberKeyEncryptionRounds = 300L
|
||||||
|
|
||||||
override val version: String
|
override val version: String
|
||||||
get() = "KeePass 1"
|
get() = "V1"
|
||||||
|
|
||||||
init {
|
init {
|
||||||
kdfListV3.add(KdfFactory.aesKdf)
|
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
||||||
}
|
rootGroup = createGroup().apply {
|
||||||
|
icon.standard = getStandardIcon(IconImageStandard.DATABASE_ID)
|
||||||
private fun getGroupById(groupId: Int): GroupKDB? {
|
}
|
||||||
if (groupId == -1)
|
|
||||||
return null
|
|
||||||
return getGroupById(NodeIdInt(groupId))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val backupGroup: GroupKDB?
|
val backupGroup: GroupKDB?
|
||||||
@@ -63,33 +81,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return listOf(BACKUP_FOLDER_TITLE)
|
return listOf(BACKUP_FOLDER_TITLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val kdfEngine: KdfEngine
|
var defaultUserName: String = ""
|
||||||
get() = kdfListV3[0]
|
|
||||||
|
|
||||||
override val kdfAvailableList: List<KdfEngine>
|
var color: Int? = null
|
||||||
get() = kdfListV3
|
|
||||||
|
|
||||||
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
|
||||||
get() {
|
|
||||||
val list = ArrayList<EncryptionAlgorithm>()
|
|
||||||
list.add(EncryptionAlgorithm.AESRijndael)
|
|
||||||
list.add(EncryptionAlgorithm.Twofish)
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
val rootGroups: List<GroupKDB>
|
|
||||||
get() {
|
|
||||||
return rootGroup?.getChildGroups() ?: ArrayList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val passwordEncoding: String
|
|
||||||
get() = "ISO-8859-1"
|
|
||||||
|
|
||||||
override var numberKeyEncryptionRounds = 300L
|
|
||||||
|
|
||||||
init {
|
|
||||||
algorithm = EncryptionAlgorithm.AESRijndael
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates an unused random tree id
|
* Generates an unused random tree id
|
||||||
@@ -215,29 +209,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle(group: GroupKDB) {
|
fun buildNewBinaryAttachment(): BinaryData {
|
||||||
removeGroupFrom(group, group.parent)
|
|
||||||
addGroupTo(group, backupGroup)
|
|
||||||
group.afterAssignNewParent()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun recycle(entry: EntryKDB) {
|
|
||||||
removeEntryFrom(entry, entry.parent)
|
|
||||||
addEntryTo(entry, backupGroup)
|
|
||||||
entry.afterAssignNewParent()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun undoRecycle(group: GroupKDB, origParent: GroupKDB) {
|
|
||||||
removeGroupFrom(group, backupGroup)
|
|
||||||
addGroupTo(group, origParent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun undoRecycle(entry: EntryKDB, origParent: GroupKDB) {
|
|
||||||
removeEntryFrom(entry, backupGroup)
|
|
||||||
addEntryTo(entry, origParent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildNewAttachment(): BinaryData {
|
|
||||||
// Generate an unique new file
|
// Generate an unique new file
|
||||||
return attachmentPool.put { uniqueBinaryId ->
|
return attachmentPool.put { uniqueBinaryId ->
|
||||||
binaryCache.getBinaryData(uniqueBinaryId, false)
|
binaryCache.getBinaryData(uniqueBinaryId, false)
|
||||||
|
|||||||
@@ -25,10 +25,9 @@ import android.util.Log
|
|||||||
import com.kunzisoft.encrypt.HashManager
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.crypto.AesEngine
|
|
||||||
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||||
@@ -42,7 +41,9 @@ import com.kunzisoft.keepass.database.element.entry.FieldReferencesEngine
|
|||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||||
import com.kunzisoft.keepass.database.element.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
@@ -66,6 +67,7 @@ import javax.crypto.Mac
|
|||||||
import javax.xml.XMLConstants
|
import javax.xml.XMLConstants
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
import javax.xml.parsers.ParserConfigurationException
|
import javax.xml.parsers.ParserConfigurationException
|
||||||
|
import kotlin.collections.HashSet
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
@@ -73,27 +75,71 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
|
|
||||||
var hmacKey: ByteArray? = null
|
var hmacKey: ByteArray? = null
|
||||||
private set
|
private set
|
||||||
var cipherUuid = EncryptionAlgorithm.AESRijndael.uuid
|
|
||||||
private var dataEngine: CipherEngine = AesEngine()
|
override var encryptionAlgorithm: EncryptionAlgorithm = EncryptionAlgorithm.AESRijndael
|
||||||
var compressionAlgorithm = CompressionAlgorithm.GZip
|
|
||||||
|
fun setEncryptionAlgorithmFromUUID(uuid: UUID) {
|
||||||
|
encryptionAlgorithm = EncryptionAlgorithm.getFrom(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm> = listOf(
|
||||||
|
EncryptionAlgorithm.AESRijndael,
|
||||||
|
EncryptionAlgorithm.Twofish,
|
||||||
|
EncryptionAlgorithm.ChaCha20
|
||||||
|
)
|
||||||
|
|
||||||
var kdfParameters: KdfParameters? = null
|
var kdfParameters: KdfParameters? = null
|
||||||
private var kdfList: MutableList<KdfEngine> = ArrayList()
|
|
||||||
private var numKeyEncRounds: Long = 0
|
override var kdfEngine: KdfEngine?
|
||||||
var publicCustomData = VariantDictionary()
|
get() = try {
|
||||||
|
getEngineKDBX4(kdfParameters)
|
||||||
|
} catch (unknownKDF: UnknownKDF) {
|
||||||
|
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
value?.let {
|
||||||
|
if (kdfParameters?.uuid != value.defaultParameters.uuid)
|
||||||
|
kdfParameters = value.defaultParameters
|
||||||
|
numberKeyEncryptionRounds = value.defaultKeyRounds
|
||||||
|
memoryUsage = value.defaultMemoryUsage
|
||||||
|
parallelism = value.defaultParallelism
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(UnknownKDF::class)
|
||||||
|
fun getEngineKDBX4(kdfParameters: KdfParameters?): KdfEngine {
|
||||||
|
val unknownKDFException = UnknownKDF()
|
||||||
|
if (kdfParameters == null) {
|
||||||
|
throw unknownKDFException
|
||||||
|
}
|
||||||
|
for (engine in kdfAvailableList) {
|
||||||
|
if (engine.uuid == kdfParameters.uuid) {
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw unknownKDFException
|
||||||
|
}
|
||||||
|
|
||||||
|
override val kdfAvailableList: List<KdfEngine> = listOf(
|
||||||
|
KdfFactory.aesKdf,
|
||||||
|
KdfFactory.argon2dKdf,
|
||||||
|
KdfFactory.argon2idKdf
|
||||||
|
)
|
||||||
|
|
||||||
|
var compressionAlgorithm = CompressionAlgorithm.GZip
|
||||||
|
|
||||||
private val mFieldReferenceEngine = FieldReferencesEngine(this)
|
private val mFieldReferenceEngine = FieldReferencesEngine(this)
|
||||||
private val mTemplateEngine = TemplateEngineCompatible(this)
|
private val mTemplateEngine = TemplateEngineCompatible(this)
|
||||||
|
|
||||||
var kdbxVersion = UnsignedInt(0)
|
var kdbxVersion = UnsignedInt(0)
|
||||||
var name = ""
|
var name = ""
|
||||||
var nameChanged = DateInstant()
|
var nameChanged = DateInstant()
|
||||||
// TODO change setting date
|
|
||||||
var settingsChanged = DateInstant()
|
|
||||||
var description = ""
|
var description = ""
|
||||||
var descriptionChanged = DateInstant()
|
var descriptionChanged = DateInstant()
|
||||||
var defaultUserName = ""
|
var defaultUserName = ""
|
||||||
var defaultUserNameChanged = DateInstant()
|
var defaultUserNameChanged = DateInstant()
|
||||||
|
var settingsChanged = DateInstant()
|
||||||
// TODO last change date
|
|
||||||
var keyLastChanged = DateInstant()
|
var keyLastChanged = DateInstant()
|
||||||
var keyChangeRecDays: Long = -1
|
var keyChangeRecDays: Long = -1
|
||||||
var keyChangeForceDays: Long = 1
|
var keyChangeForceDays: Long = 1
|
||||||
@@ -115,17 +161,12 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
var lastSelectedGroupUUID = UUID_ZERO
|
var lastSelectedGroupUUID = UUID_ZERO
|
||||||
var lastTopVisibleGroupUUID = UUID_ZERO
|
var lastTopVisibleGroupUUID = UUID_ZERO
|
||||||
var memoryProtection = MemoryProtectionConfig()
|
var memoryProtection = MemoryProtectionConfig()
|
||||||
val deletedObjects = ArrayList<DeletedObject>()
|
val deletedObjects = HashSet<DeletedObject>()
|
||||||
|
var publicCustomData = VariantDictionary()
|
||||||
val customData = CustomData()
|
val customData = CustomData()
|
||||||
|
|
||||||
var localizedAppName = "KeePassDX"
|
var localizedAppName = "KeePassDX"
|
||||||
|
|
||||||
init {
|
|
||||||
kdfList.add(KdfFactory.aesKdf)
|
|
||||||
kdfList.add(KdfFactory.argon2dKdf)
|
|
||||||
kdfList.add(KdfFactory.argon2idKdf)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,41 +197,74 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
FILE_VERSION_41 -> "4.1"
|
FILE_VERSION_41 -> "4.1"
|
||||||
else -> "UNKNOWN"
|
else -> "UNKNOWN"
|
||||||
}
|
}
|
||||||
return "KeePass 2 - KDBX$kdbxStringVersion"
|
return "V2 - KDBX$kdbxStringVersion"
|
||||||
}
|
}
|
||||||
|
|
||||||
override val kdfEngine: KdfEngine?
|
private open class NodeOperationHandler<T: NodeKDBXInterface> : NodeHandler<T>() {
|
||||||
get() = try {
|
var containsCustomData = false
|
||||||
getEngineKDBX4(kdfParameters)
|
override fun operate(node: T): Boolean {
|
||||||
} catch (unknownKDF: UnknownKDF) {
|
if (node.customData.isNotEmpty()) {
|
||||||
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
|
containsCustomData = true
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
override val kdfAvailableList: List<KdfEngine>
|
|
||||||
get() = kdfList
|
|
||||||
|
|
||||||
@Throws(UnknownKDF::class)
|
|
||||||
fun getEngineKDBX4(kdfParameters: KdfParameters?): KdfEngine {
|
|
||||||
val unknownKDFException = UnknownKDF()
|
|
||||||
if (kdfParameters == null) {
|
|
||||||
throw unknownKDFException
|
|
||||||
}
|
|
||||||
for (engine in kdfList) {
|
|
||||||
if (engine.uuid == kdfParameters.uuid) {
|
|
||||||
return engine
|
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
throw unknownKDFException
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val availableCompressionAlgorithms: List<CompressionAlgorithm>
|
private inner class EntryOperationHandler: NodeOperationHandler<EntryKDBX>() {
|
||||||
get() {
|
var passwordQualityEstimationDisabled = false
|
||||||
val list = ArrayList<CompressionAlgorithm>()
|
override fun operate(node: EntryKDBX): Boolean {
|
||||||
list.add(CompressionAlgorithm.None)
|
if (!node.qualityCheck) {
|
||||||
list.add(CompressionAlgorithm.GZip)
|
passwordQualityEstimationDisabled = true
|
||||||
return list
|
}
|
||||||
|
return super.operate(node)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class GroupOperationHandler: NodeOperationHandler<GroupKDBX>() {
|
||||||
|
var containsTags = false
|
||||||
|
override fun operate(node: GroupKDBX): Boolean {
|
||||||
|
if (!node.tags.isEmpty())
|
||||||
|
containsTags = true
|
||||||
|
return super.operate(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMinKdbxVersion(): UnsignedInt {
|
||||||
|
val entryHandler = EntryOperationHandler()
|
||||||
|
val groupHandler = GroupOperationHandler()
|
||||||
|
rootGroup?.doForEachChildAndForIt(entryHandler, groupHandler)
|
||||||
|
|
||||||
|
// https://keepass.info/help/kb/kdbx_4.1.html
|
||||||
|
val containsGroupWithTag = groupHandler.containsTags
|
||||||
|
val containsEntryWithPasswordQualityEstimationDisabled = entryHandler.passwordQualityEstimationDisabled
|
||||||
|
val containsCustomIconWithNameOrLastModificationTime = iconsManager.containsCustomIconWithNameOrLastModificationTime()
|
||||||
|
val containsHeaderCustomDataWithLastModificationTime = customData.containsItemWithLastModificationTime()
|
||||||
|
|
||||||
|
// https://keepass.info/help/kb/kdbx_4.html
|
||||||
|
// If AES is not use, it's at least 4.0
|
||||||
|
val kdfIsNotAes = kdfParameters?.uuid != AesKdf.CIPHER_UUID
|
||||||
|
val containsHeaderCustomData = customData.isNotEmpty()
|
||||||
|
val containsNodeCustomData = entryHandler.containsCustomData || groupHandler.containsCustomData
|
||||||
|
|
||||||
|
// Check each condition to determine version
|
||||||
|
return if (containsGroupWithTag
|
||||||
|
|| containsEntryWithPasswordQualityEstimationDisabled
|
||||||
|
|| containsCustomIconWithNameOrLastModificationTime
|
||||||
|
|| containsHeaderCustomDataWithLastModificationTime) {
|
||||||
|
FILE_VERSION_41
|
||||||
|
} else if (kdfIsNotAes
|
||||||
|
|| containsHeaderCustomData
|
||||||
|
|| containsNodeCustomData) {
|
||||||
|
FILE_VERSION_40
|
||||||
|
} else {
|
||||||
|
FILE_VERSION_31
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val availableCompressionAlgorithms: List<CompressionAlgorithm> = listOf(
|
||||||
|
CompressionAlgorithm.None,
|
||||||
|
CompressionAlgorithm.GZip
|
||||||
|
)
|
||||||
|
|
||||||
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
|
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||||
newCompression: CompressionAlgorithm) {
|
newCompression: CompressionAlgorithm) {
|
||||||
@@ -245,18 +319,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
|
||||||
get() {
|
|
||||||
val list = ArrayList<EncryptionAlgorithm>()
|
|
||||||
list.add(EncryptionAlgorithm.AESRijndael)
|
|
||||||
list.add(EncryptionAlgorithm.Twofish)
|
|
||||||
list.add(EncryptionAlgorithm.ChaCha20)
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
override var numberKeyEncryptionRounds: Long
|
override var numberKeyEncryptionRounds: Long
|
||||||
get() {
|
get() {
|
||||||
val kdfEngine = kdfEngine
|
val kdfEngine = kdfEngine
|
||||||
|
var numKeyEncRounds: Long = 0
|
||||||
if (kdfEngine != null && kdfParameters != null)
|
if (kdfEngine != null && kdfParameters != null)
|
||||||
numKeyEncRounds = kdfEngine.getKeyRounds(kdfParameters!!)
|
numKeyEncRounds = kdfEngine.getKeyRounds(kdfParameters!!)
|
||||||
return numKeyEncRounds
|
return numKeyEncRounds
|
||||||
@@ -265,7 +331,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
val kdfEngine = kdfEngine
|
val kdfEngine = kdfEngine
|
||||||
if (kdfEngine != null && kdfParameters != null)
|
if (kdfEngine != null && kdfParameters != null)
|
||||||
kdfEngine.setKeyRounds(kdfParameters!!, rounds)
|
kdfEngine.setKeyRounds(kdfParameters!!, rounds)
|
||||||
numKeyEncRounds = rounds
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var memoryUsage: Long
|
var memoryUsage: Long
|
||||||
@@ -305,7 +370,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
|
|
||||||
// Retrieve recycle bin in index
|
// Retrieve recycle bin in index
|
||||||
val recycleBin: GroupKDBX?
|
val recycleBin: GroupKDBX?
|
||||||
get() = if (recycleBinUUID == UUID_ZERO) null else getGroupByUUID(recycleBinUUID)
|
get() = getGroupByUUID(recycleBinUUID)
|
||||||
|
|
||||||
val lastSelectedGroup: GroupKDBX?
|
val lastSelectedGroup: GroupKDBX?
|
||||||
get() = getGroupByUUID(lastSelectedGroupUUID)
|
get() = getGroupByUUID(lastSelectedGroupUUID)
|
||||||
@@ -313,17 +378,14 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
val lastTopVisibleGroup: GroupKDBX?
|
val lastTopVisibleGroup: GroupKDBX?
|
||||||
get() = getGroupByUUID(lastTopVisibleGroupUUID)
|
get() = getGroupByUUID(lastTopVisibleGroupUUID)
|
||||||
|
|
||||||
fun setDataEngine(dataEngine: CipherEngine) {
|
|
||||||
this.dataEngine = dataEngine
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStandardIcon(iconId: Int): IconImageStandard {
|
override fun getStandardIcon(iconId: Int): IconImageStandard {
|
||||||
return this.iconsManager.getIcon(iconId)
|
return this.iconsManager.getIcon(iconId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewCustomIcon(customIconId: UUID? = null,
|
fun buildNewCustomIcon(customIconId: UUID? = null,
|
||||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
iconsManager.buildNewCustomIcon(customIconId, result)
|
// Create a binary file for a brand new custom icon
|
||||||
|
addCustomIcon(customIconId, "", null, false, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addCustomIcon(customIconId: UUID? = null,
|
fun addCustomIcon(customIconId: UUID? = null,
|
||||||
@@ -331,14 +393,21 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
lastModificationTime: DateInstant?,
|
lastModificationTime: DateInstant?,
|
||||||
smallSize: Boolean,
|
smallSize: Boolean,
|
||||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, smallSize, result)
|
iconsManager.addCustomIcon(customIconId, name, lastModificationTime, { uniqueBinaryId ->
|
||||||
|
// Create a byte array for better performance with small data
|
||||||
|
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
|
||||||
|
}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCustomIcon(iconUuid: UUID) {
|
||||||
|
iconsManager.removeCustomIcon(iconUuid, binaryCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
|
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
|
||||||
return iconsManager.isCustomIconBinaryDuplicate(binary)
|
return iconsManager.isCustomIconBinaryDuplicate(binary)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCustomIcon(iconUuid: UUID): IconImageCustom {
|
fun getCustomIcon(iconUuid: UUID): IconImageCustom? {
|
||||||
return this.iconsManager.getIcon(iconUuid)
|
return this.iconsManager.getIcon(iconUuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,7 +424,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
val templatesGroup = firstGroupWithValidName
|
val templatesGroup = firstGroupWithValidName
|
||||||
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
|
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
|
||||||
entryTemplatesGroup = templatesGroup.id
|
entryTemplatesGroup = templatesGroup.id
|
||||||
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
|
|
||||||
} else {
|
} else {
|
||||||
removeTemplatesGroup()
|
removeTemplatesGroup()
|
||||||
}
|
}
|
||||||
@@ -363,7 +431,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
|
|
||||||
fun removeTemplatesGroup() {
|
fun removeTemplatesGroup() {
|
||||||
entryTemplatesGroup = UUID_ZERO
|
entryTemplatesGroup = UUID_ZERO
|
||||||
entryTemplatesGroupChanged = DateInstant()
|
|
||||||
mTemplateEngine.clearCache()
|
mTemplateEngine.clearCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,37 +481,37 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
|
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
|
||||||
return this.entryIndexes.values.find { entry ->
|
return findEntry { entry ->
|
||||||
entry.decodeTitleKey(recursionLevel).equals(title, true)
|
entry.decodeTitleKey(recursionLevel).equals(title, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? {
|
fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? {
|
||||||
return this.entryIndexes.values.find { entry ->
|
return findEntry { entry ->
|
||||||
entry.decodeUsernameKey(recursionLevel).equals(username, true)
|
entry.decodeUsernameKey(recursionLevel).equals(username, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? {
|
fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? {
|
||||||
return this.entryIndexes.values.find { entry ->
|
return findEntry { entry ->
|
||||||
entry.decodeUrlKey(recursionLevel).equals(url, true)
|
entry.decodeUrlKey(recursionLevel).equals(url, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? {
|
fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? {
|
||||||
return this.entryIndexes.values.find { entry ->
|
return findEntry { entry ->
|
||||||
entry.decodePasswordKey(recursionLevel).equals(password, true)
|
entry.decodePasswordKey(recursionLevel).equals(password, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? {
|
fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? {
|
||||||
return this.entryIndexes.values.find { entry ->
|
return findEntry { entry ->
|
||||||
entry.decodeNotesKey(recursionLevel).equals(notes, true)
|
entry.decodeNotesKey(recursionLevel).equals(notes, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
|
fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
|
||||||
return entryIndexes.values.find { entry ->
|
return findEntry { entry ->
|
||||||
entry.customData.containsItemWithValue(customDataValue)
|
entry.customData.containsItemWithValue(customDataValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,7 +553,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
val cmpKey = ByteArray(65)
|
val cmpKey = ByteArray(65)
|
||||||
System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
|
System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
|
||||||
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
|
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
|
||||||
finalKey = resizeKey(cmpKey, dataEngine.keyLength())
|
finalKey = resizeKey(cmpKey, encryptionAlgorithm.cipherEngine.keyLength())
|
||||||
|
|
||||||
val messageDigest: MessageDigest
|
val messageDigest: MessageDigest
|
||||||
try {
|
try {
|
||||||
@@ -724,14 +791,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
firstGroupWithValidName
|
firstGroupWithValidName
|
||||||
}
|
}
|
||||||
recycleBinUUID = recycleBinGroup.id
|
recycleBinUUID = recycleBinGroup.id
|
||||||
recycleBinChanged = recycleBinGroup.lastModificationTime
|
recycleBinChanged = DateInstant()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeRecycleBin() {
|
fun removeRecycleBin() {
|
||||||
if (recycleBin != null) {
|
if (recycleBin != null) {
|
||||||
recycleBinUUID = UUID_ZERO
|
recycleBinUUID = UUID_ZERO
|
||||||
recycleBinChanged = DateInstant()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,38 +819,18 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle(group: GroupKDBX, resources: Resources) {
|
fun getDeletedObject(nodeId: NodeId<UUID>): DeletedObject? {
|
||||||
ensureRecycleBinExists(resources)
|
return deletedObjects.find { it.uuid == nodeId.id }
|
||||||
removeGroupFrom(group, group.parent)
|
|
||||||
addGroupTo(group, recycleBin)
|
|
||||||
group.afterAssignNewParent()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun recycle(entry: EntryKDBX, resources: Resources) {
|
|
||||||
ensureRecycleBinExists(resources)
|
|
||||||
removeEntryFrom(entry, entry.parent)
|
|
||||||
addEntryTo(entry, recycleBin)
|
|
||||||
entry.afterAssignNewParent()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun undoRecycle(group: GroupKDBX, origParent: GroupKDBX) {
|
|
||||||
removeGroupFrom(group, recycleBin)
|
|
||||||
addGroupTo(group, origParent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun undoRecycle(entry: EntryKDBX, origParent: GroupKDBX) {
|
|
||||||
removeEntryFrom(entry, recycleBin)
|
|
||||||
addEntryTo(entry, origParent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDeletedObjects(): List<DeletedObject> {
|
|
||||||
return deletedObjects
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addDeletedObject(deletedObject: DeletedObject) {
|
fun addDeletedObject(deletedObject: DeletedObject) {
|
||||||
this.deletedObjects.add(deletedObject)
|
this.deletedObjects.add(deletedObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addDeletedObject(objectId: UUID) {
|
||||||
|
addDeletedObject(DeletedObject(objectId))
|
||||||
|
}
|
||||||
|
|
||||||
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
|
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
|
||||||
super.addEntryTo(newEntry, parent)
|
super.addEntryTo(newEntry, parent)
|
||||||
mFieldReferenceEngine.clear()
|
mFieldReferenceEngine.clear()
|
||||||
@@ -797,23 +843,17 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
|
|
||||||
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
|
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
|
||||||
super.removeEntryFrom(entryToRemove, parent)
|
super.removeEntryFrom(entryToRemove, parent)
|
||||||
deletedObjects.add(DeletedObject(entryToRemove.id))
|
|
||||||
mFieldReferenceEngine.clear()
|
mFieldReferenceEngine.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
|
|
||||||
super.undoDeleteEntryFrom(entry, origParent)
|
|
||||||
deletedObjects.remove(DeletedObject(entry.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun containsPublicCustomData(): Boolean {
|
fun containsPublicCustomData(): Boolean {
|
||||||
return publicCustomData.size() > 0
|
return publicCustomData.size() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewAttachment(smallSize: Boolean,
|
fun buildNewBinaryAttachment(smallSize: Boolean,
|
||||||
compression: Boolean,
|
compression: Boolean,
|
||||||
protection: Boolean,
|
protection: Boolean,
|
||||||
binaryPoolId: Int? = null): BinaryData {
|
binaryPoolId: Int? = null): BinaryData {
|
||||||
return attachmentPool.put(binaryPoolId) { uniqueBinaryId ->
|
return attachmentPool.put(binaryPoolId) { uniqueBinaryId ->
|
||||||
binaryCache.getBinaryData(uniqueBinaryId, smallSize, compression, protection)
|
binaryCache.getBinaryData(uniqueBinaryId, smallSize, compression, protection)
|
||||||
}.binary
|
}.binary
|
||||||
@@ -830,6 +870,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
|
private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
|
||||||
|
// TODO check in icon pool
|
||||||
// Build binaries to remove with all binaries known
|
// Build binaries to remove with all binaries known
|
||||||
val binariesToRemove = ArrayList<BinaryData>()
|
val binariesToRemove = ArrayList<BinaryData>()
|
||||||
if (binaries.isEmpty()) {
|
if (binaries.isEmpty()) {
|
||||||
@@ -866,11 +907,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return super.validatePasswordEncoding(password, containsKeyFile)
|
return super.validatePasswordEncoding(password, containsKeyFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearCache() {
|
override fun clearIndexes() {
|
||||||
try {
|
try {
|
||||||
super.clearCache()
|
super.clearIndexes()
|
||||||
mFieldReferenceEngine.clear()
|
mFieldReferenceEngine.clear()
|
||||||
attachmentPool.clear()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to clear cache", e)
|
Log.e(TAG, "Unable to clear cache", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.kunzisoft.encrypt.HashManager
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||||
@@ -44,51 +46,42 @@ abstract class DatabaseVersioned<
|
|||||||
Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
|
Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
|
||||||
> {
|
> {
|
||||||
|
|
||||||
|
|
||||||
// Algorithm used to encrypt the database
|
// Algorithm used to encrypt the database
|
||||||
protected var algorithm: EncryptionAlgorithm? = null
|
abstract var encryptionAlgorithm: EncryptionAlgorithm
|
||||||
|
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
||||||
|
|
||||||
abstract val kdfEngine: com.kunzisoft.keepass.database.crypto.kdf.KdfEngine?
|
abstract var kdfEngine: KdfEngine?
|
||||||
|
abstract val kdfAvailableList: List<KdfEngine>
|
||||||
|
abstract var numberKeyEncryptionRounds: Long
|
||||||
|
|
||||||
abstract val kdfAvailableList: List<com.kunzisoft.keepass.database.crypto.kdf.KdfEngine>
|
protected abstract val passwordEncoding: String
|
||||||
|
|
||||||
var masterKey = ByteArray(32)
|
var masterKey = ByteArray(32)
|
||||||
var finalKey: ByteArray? = null
|
var finalKey: ByteArray? = null
|
||||||
protected set
|
protected set
|
||||||
|
|
||||||
|
abstract val version: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To manage binaries in faster way
|
* To manage binaries in faster way
|
||||||
* Cipher key generated when the database is loaded, and destroyed when the database is closed
|
* Cipher key generated when the database is loaded, and destroyed when the database is closed
|
||||||
* Can be used to temporarily store database elements
|
* Can be used to temporarily store database elements
|
||||||
*/
|
*/
|
||||||
var binaryCache = BinaryCache()
|
var binaryCache = BinaryCache()
|
||||||
val iconsManager = IconsManager(binaryCache)
|
var iconsManager = IconsManager()
|
||||||
var attachmentPool = AttachmentPool(binaryCache)
|
var attachmentPool = AttachmentPool()
|
||||||
|
|
||||||
var changeDuplicateId = false
|
var changeDuplicateId = false
|
||||||
|
|
||||||
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
|
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
|
||||||
protected var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
|
||||||
|
|
||||||
abstract val version: String
|
|
||||||
|
|
||||||
protected abstract val passwordEncoding: String
|
|
||||||
|
|
||||||
abstract var numberKeyEncryptionRounds: Long
|
|
||||||
|
|
||||||
var encryptionAlgorithm: EncryptionAlgorithm
|
|
||||||
get() {
|
|
||||||
return algorithm ?: EncryptionAlgorithm.AESRijndael
|
|
||||||
}
|
|
||||||
set(algorithm) {
|
|
||||||
this.algorithm = algorithm
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
|
||||||
|
|
||||||
var rootGroup: Group? = null
|
var rootGroup: Group? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
value?.let {
|
value?.let {
|
||||||
|
removeGroupIndex(it)
|
||||||
addGroupIndex(it)
|
addGroupIndex(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,25 +117,29 @@ abstract class DatabaseVersioned<
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
||||||
val keyData = keyInputStream.readBytes()
|
try {
|
||||||
|
val keyData = keyInputStream.readBytes()
|
||||||
|
|
||||||
// Check XML key file
|
// Check XML key file
|
||||||
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
|
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
|
||||||
if (xmlKeyByteArray != null) {
|
if (xmlKeyByteArray != null) {
|
||||||
return xmlKeyByteArray
|
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 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? {
|
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||||
@@ -194,12 +191,6 @@ abstract class DatabaseVersioned<
|
|||||||
* -------------------------------------
|
* -------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun doForEachGroupInIndex(action: (Group) -> Unit) {
|
|
||||||
for (group in groupIndexes) {
|
|
||||||
action.invoke(group.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if an id number is already in use
|
* Determine if an id number is already in use
|
||||||
*
|
*
|
||||||
@@ -215,14 +206,7 @@ abstract class DatabaseVersioned<
|
|||||||
return groupIndexes.values
|
return groupIndexes.values
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setGroupIndexes(groupList: List<Group>) {
|
open fun getGroupById(id: NodeId<GroupId>): Group? {
|
||||||
this.groupIndexes.clear()
|
|
||||||
for (currentGroup in groupList) {
|
|
||||||
this.groupIndexes[currentGroup.nodeId] = currentGroup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGroupById(id: NodeId<GroupId>): Group? {
|
|
||||||
return this.groupIndexes[id]
|
return this.groupIndexes[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,16 +230,6 @@ abstract class DatabaseVersioned<
|
|||||||
this.groupIndexes.remove(group.nodeId)
|
this.groupIndexes.remove(group.nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun numberOfGroups(): Int {
|
|
||||||
return groupIndexes.size
|
|
||||||
}
|
|
||||||
|
|
||||||
fun doForEachEntryInIndex(action: (Entry) -> Unit) {
|
|
||||||
for (entry in entryIndexes) {
|
|
||||||
action.invoke(entry.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEntryIdUsed(id: NodeId<EntryId>): Boolean {
|
fun isEntryIdUsed(id: NodeId<EntryId>): Boolean {
|
||||||
return entryIndexes.containsKey(id)
|
return entryIndexes.containsKey(id)
|
||||||
}
|
}
|
||||||
@@ -268,6 +242,10 @@ abstract class DatabaseVersioned<
|
|||||||
return this.entryIndexes[id]
|
return this.entryIndexes[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findEntry(predicate: (Entry) -> Boolean): Entry? {
|
||||||
|
return this.entryIndexes.values.find(predicate)
|
||||||
|
}
|
||||||
|
|
||||||
fun addEntryIndex(entry: Entry) {
|
fun addEntryIndex(entry: Entry) {
|
||||||
val entryId = entry.nodeId
|
val entryId = entry.nodeId
|
||||||
if (entryIndexes.containsKey(entryId)) {
|
if (entryIndexes.containsKey(entryId)) {
|
||||||
@@ -288,11 +266,7 @@ abstract class DatabaseVersioned<
|
|||||||
this.entryIndexes.remove(entry.nodeId)
|
this.entryIndexes.remove(entry.nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun numberOfEntries(): Int {
|
open fun clearIndexes() {
|
||||||
return entryIndexes.size
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun clearCache() {
|
|
||||||
this.groupIndexes.clear()
|
this.groupIndexes.clear()
|
||||||
this.entryIndexes.clear()
|
this.entryIndexes.clear()
|
||||||
}
|
}
|
||||||
@@ -322,7 +296,7 @@ abstract class DatabaseVersioned<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
|
open fun removeGroupFrom(groupToRemove: Group, parent: Group?) {
|
||||||
// Remove tree from parent tree
|
// Remove tree from parent tree
|
||||||
parent?.removeChildGroup(groupToRemove)
|
parent?.removeChildGroup(groupToRemove)
|
||||||
removeGroupIndex(groupToRemove)
|
removeGroupIndex(groupToRemove)
|
||||||
@@ -349,15 +323,6 @@ abstract class DatabaseVersioned<
|
|||||||
removeEntryIndex(entryToRemove)
|
removeEntryIndex(entryToRemove)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Delete group
|
|
||||||
fun undoDeleteGroupFrom(group: Group, origParent: Group?) {
|
|
||||||
addGroupTo(group, origParent)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun undoDeleteEntryFrom(entry: Entry, origParent: Group?) {
|
|
||||||
addEntryTo(entry, origParent)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun isInRecycleBin(group: Group): Boolean
|
abstract fun isInRecycleBin(group: Group): Boolean
|
||||||
|
|
||||||
fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean {
|
fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean {
|
||||||
@@ -368,6 +333,39 @@ abstract class DatabaseVersioned<
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearIconsCache() {
|
||||||
|
iconsManager.doForEachCustomIcon { _, binary ->
|
||||||
|
try {
|
||||||
|
binary.clear(binaryCache)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to clear icon binary cache", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconsManager.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAttachmentsCache() {
|
||||||
|
attachmentPool.doForEachBinary { _, binary ->
|
||||||
|
try {
|
||||||
|
binary.clear(binaryCache)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to clear attachment binary cache", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attachmentPool.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearBinaries() {
|
||||||
|
binaryCache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAll() {
|
||||||
|
clearIndexes()
|
||||||
|
clearIconsCache()
|
||||||
|
clearAttachmentsCache()
|
||||||
|
clearBinaries()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "DatabaseVersioned"
|
private const val TAG = "DatabaseVersioned"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.kunzisoft.keepass.database.element.Attachment
|
|||||||
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
@@ -60,18 +61,43 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
private var binaryDataId: Int? = null
|
private var binaryDataId: Int? = null
|
||||||
|
|
||||||
// Determine if this is a MetaStream entry
|
// Determine if this is a MetaStream entry
|
||||||
val isMetaStream: Boolean
|
fun isMetaStream(): Boolean {
|
||||||
get() {
|
if (notes.isEmpty()) return false
|
||||||
if (notes.isEmpty()) return false
|
if (binaryDescription != PMS_ID_BINDESC) return false
|
||||||
if (binaryDescription != PMS_ID_BINDESC) return false
|
if (title.isEmpty()) return false
|
||||||
if (title.isEmpty()) return false
|
if (title != PMS_ID_TITLE) return false
|
||||||
if (title != PMS_ID_TITLE) return false
|
if (username.isEmpty()) return false
|
||||||
if (username.isEmpty()) return false
|
if (username != PMS_ID_USER) return false
|
||||||
if (username != PMS_ID_USER) return false
|
if (url.isEmpty()) return false
|
||||||
if (url.isEmpty()) return false
|
if (url != PMS_ID_URL) return false
|
||||||
if (url != PMS_ID_URL) return false
|
return icon.standard.id == KEY_ID
|
||||||
return icon.standard.id == KEY_ID
|
}
|
||||||
}
|
|
||||||
|
fun isMetaStreamDefaultUsername(): Boolean {
|
||||||
|
return isMetaStream() && notes == PMS_STREAM_DEFAULTUSER
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMetaStream() {
|
||||||
|
binaryDescription = PMS_ID_BINDESC
|
||||||
|
title = PMS_ID_TITLE
|
||||||
|
username = PMS_ID_USER
|
||||||
|
url = PMS_ID_URL
|
||||||
|
icon.standard = IconImageStandard(KEY_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMetaStreamDefaultUsername() {
|
||||||
|
notes = PMS_STREAM_DEFAULTUSER
|
||||||
|
setMetaStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isMetaStreamDatabaseColor(): Boolean {
|
||||||
|
return isMetaStream() && notes == PMS_STREAM_DBCOLOR
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMetaStreamDatabaseColor() {
|
||||||
|
notes = PMS_STREAM_DBCOLOR
|
||||||
|
setMetaStream()
|
||||||
|
}
|
||||||
|
|
||||||
override fun initNodeId(): NodeId<UUID> {
|
override fun initNodeId(): NodeId<UUID> {
|
||||||
return NodeIdUUID()
|
return NodeIdUUID()
|
||||||
@@ -113,8 +139,9 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
dest.writeInt(binaryDataId ?: -1)
|
dest.writeInt(binaryDataId ?: -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateWith(source: EntryKDB) {
|
fun updateWith(source: EntryKDB,
|
||||||
super.updateWith(source)
|
updateParents: Boolean = true) {
|
||||||
|
super.updateWith(source, updateParents)
|
||||||
title = source.title
|
title = source.title
|
||||||
username = source.username
|
username = source.username
|
||||||
password = source.password
|
password = source.password
|
||||||
@@ -184,6 +211,13 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
private const val PMS_ID_USER = "SYSTEM"
|
private const val PMS_ID_USER = "SYSTEM"
|
||||||
private const val PMS_ID_URL = "$"
|
private const val PMS_ID_URL = "$"
|
||||||
|
|
||||||
|
const val PMS_STREAM_SIMPLESTATE = "Simple UI State"
|
||||||
|
const val PMS_STREAM_DEFAULTUSER = "Default User Name"
|
||||||
|
const val PMS_STREAM_SEARCHHISTORYITEM = "Search History Item"
|
||||||
|
const val PMS_STREAM_CUSTOMKVP = "Custom KVP"
|
||||||
|
const val PMS_STREAM_DBCOLOR = "Database Color"
|
||||||
|
const val PMS_STREAM_KPXICON2 = "KPX_CUSTOM_ICONS_2"
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATOR: Parcelable.Creator<EntryKDB> = object : Parcelable.Creator<EntryKDB> {
|
val CREATOR: Parcelable.Creator<EntryKDB> = object : Parcelable.Creator<EntryKDB> {
|
||||||
override fun createFromParcel(parcel: Parcel): EntryKDB {
|
override fun createFromParcel(parcel: Parcel): EntryKDB {
|
||||||
|
|||||||
@@ -110,8 +110,10 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
* Update with deep copy of each entry element
|
* Update with deep copy of each entry element
|
||||||
* @param source
|
* @param source
|
||||||
*/
|
*/
|
||||||
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
|
fun updateWith(source: EntryKDBX,
|
||||||
super.updateWith(source)
|
copyHistory: Boolean = true,
|
||||||
|
updateParents: Boolean = true) {
|
||||||
|
super.updateWith(source, updateParents)
|
||||||
usageCount = source.usageCount
|
usageCount = source.usageCount
|
||||||
locationChanged = DateInstant(source.locationChanged)
|
locationChanged = DateInstant(source.locationChanged)
|
||||||
customData = CustomData(source.customData)
|
customData = CustomData(source.customData)
|
||||||
|
|||||||
@@ -53,8 +53,9 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
dest.writeInt(groupFlags)
|
dest.writeInt(groupFlags)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateWith(source: GroupKDB) {
|
fun updateWith(source: GroupKDB,
|
||||||
super.updateWith(source)
|
updateParents: Boolean = true) {
|
||||||
|
super.updateWith(source, updateParents)
|
||||||
groupFlags = source.groupFlags
|
groupFlags = source.groupFlags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,8 +102,9 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
|||||||
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
|
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateWith(source: GroupKDBX) {
|
fun updateWith(source: GroupKDBX,
|
||||||
super.updateWith(source)
|
updateParents: Boolean = true) {
|
||||||
|
super.updateWith(source, updateParents)
|
||||||
usageCount = source.usageCount
|
usageCount = source.usageCount
|
||||||
locationChanged = DateInstant(source.locationChanged)
|
locationChanged = DateInstant(source.locationChanged)
|
||||||
// Add all custom elements in map
|
// Add all custom elements in map
|
||||||
|
|||||||
@@ -51,12 +51,15 @@ abstract class GroupVersioned
|
|||||||
dest.writeString(titleGroup)
|
dest.writeString(titleGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>) {
|
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>,
|
||||||
super.updateWith(source)
|
updateParents: Boolean = true) {
|
||||||
|
super.updateWith(source, updateParents)
|
||||||
titleGroup = source.titleGroup
|
titleGroup = source.titleGroup
|
||||||
removeChildren()
|
if (updateParents) {
|
||||||
childGroups.addAll(source.childGroups)
|
removeChildren()
|
||||||
childEntries.addAll(source.childEntries)
|
childGroups.addAll(source.childGroups)
|
||||||
|
childEntries.addAll(source.childEntries)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override var title: String
|
override var title: String
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class IconImageStandard : IconImageDraw {
|
|||||||
const val CREDIT_CARD_ID = 37
|
const val CREDIT_CARD_ID = 37
|
||||||
const val TRASH_ID = 43
|
const val TRASH_ID = 43
|
||||||
const val FOLDER_ID = 48
|
const val FOLDER_ID = 48
|
||||||
|
const val DATABASE_ID = 50
|
||||||
const val LIST_ID = 57
|
const val LIST_ID = 57
|
||||||
const val BUILD_ID = 59
|
const val BUILD_ID = 59
|
||||||
const val STAR_ID = 61
|
const val STAR_ID = 61
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.K
|
|||||||
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
import com.kunzisoft.keepass.icons.IconPack.Companion.NB_ICONS
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class IconsManager(binaryCache: BinaryCache) {
|
class IconsManager {
|
||||||
|
|
||||||
private val standardCache = List(NB_ICONS) {
|
private val standardCache = List(NB_ICONS) {
|
||||||
IconImageStandard(it)
|
IconImageStandard(it)
|
||||||
}
|
}
|
||||||
private val customCache = CustomIconPool(binaryCache)
|
private val customCache = CustomIconPool()
|
||||||
|
|
||||||
fun getIcon(iconId: Int): IconImageStandard {
|
fun getIcon(iconId: Int): IconImageStandard {
|
||||||
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
|
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
|
||||||
@@ -50,29 +50,23 @@ class IconsManager(binaryCache: BinaryCache) {
|
|||||||
* Custom
|
* Custom
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun buildNewCustomIcon(key: UUID? = null,
|
|
||||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
|
||||||
// Create a binary file for a brand new custom icon
|
|
||||||
addCustomIcon(key, "", null, false, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addCustomIcon(key: UUID? = null,
|
fun addCustomIcon(key: UUID? = null,
|
||||||
name: String,
|
name: String,
|
||||||
lastModificationTime: DateInstant?,
|
lastModificationTime: DateInstant?,
|
||||||
smallSize: Boolean,
|
builder: (uniqueBinaryId: String) -> BinaryData,
|
||||||
result: (IconImageCustom, BinaryData?) -> Unit) {
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
customCache.put(key, name, lastModificationTime, smallSize, result)
|
customCache.put(key, name, lastModificationTime, builder, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIcon(iconUuid: UUID): IconImageCustom {
|
fun getIcon(iconUuid: UUID): IconImageCustom? {
|
||||||
return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid)
|
return customCache.getCustomIcon(iconUuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
|
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
|
||||||
return customCache.isBinaryDuplicate(binaryData)
|
return customCache.isBinaryDuplicate(binaryData)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeCustomIcon(binaryCache: BinaryCache, iconUuid: UUID) {
|
fun removeCustomIcon(iconUuid: UUID, binaryCache: BinaryCache) {
|
||||||
val binary = customCache[iconUuid]
|
val binary = customCache[iconUuid]
|
||||||
customCache.remove(iconUuid)
|
customCache.remove(iconUuid)
|
||||||
try {
|
try {
|
||||||
@@ -99,12 +93,8 @@ class IconsManager(binaryCache: BinaryCache) {
|
|||||||
/**
|
/**
|
||||||
* Clear the cache of icons
|
* Clear the cache of icons
|
||||||
*/
|
*/
|
||||||
fun clearCache() {
|
fun clear() {
|
||||||
try {
|
customCache.clear()
|
||||||
customCache.clear()
|
|
||||||
} catch(e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to clear cache", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ interface Node: NodeVersionedInterface<Group> {
|
|||||||
fun removeParent() {
|
fun removeParent() {
|
||||||
parent = null
|
parent = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPathString(): String {
|
||||||
|
val pathNodes = mutableListOf<Node>()
|
||||||
|
var currentNode = this
|
||||||
|
pathNodes.add(0, currentNode)
|
||||||
|
while (currentNode.containsParent()) {
|
||||||
|
currentNode.parent?.let { parent ->
|
||||||
|
currentNode = parent
|
||||||
|
pathNodes.add(0, currentNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pathNodes.joinToString("/") { it.title }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -44,4 +44,6 @@ abstract class NodeId<Id> : Parcelable {
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return id?.hashCode() ?: 0
|
return id?.hashCode() ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract fun toVisualString(): String?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ class NodeIdInt : NodeId<Int> {
|
|||||||
return id.toString()
|
return id.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toVisualString(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATOR: Parcelable.Creator<NodeIdInt> = object : Parcelable.Creator<NodeIdInt> {
|
val CREATOR: Parcelable.Creator<NodeIdInt> = object : Parcelable.Creator<NodeIdInt> {
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ class NodeIdUUID : NodeId<UUID> {
|
|||||||
return UuidUtil.toHexString(id) ?: id.toString()
|
return UuidUtil.toHexString(id) ?: id.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toVisualString(): String {
|
||||||
|
return toString()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATOR: Parcelable.Creator<NodeIdUUID> = object : Parcelable.Creator<NodeIdUUID> {
|
val CREATOR: Parcelable.Creator<NodeIdUUID> = object : Parcelable.Creator<NodeIdUUID> {
|
||||||
|
|||||||
@@ -68,9 +68,12 @@ abstract class NodeVersioned<IdType, Parent : GroupVersionedInterface<Parent, En
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun updateWith(source: NodeVersioned<IdType, Parent, Entry>) {
|
protected fun updateWith(source: NodeVersioned<IdType, Parent, Entry>,
|
||||||
|
updateParents: Boolean = true) {
|
||||||
this.nodeId = copyNodeId(source.nodeId)
|
this.nodeId = copyNodeId(source.nodeId)
|
||||||
this.parent = source.parent
|
if (updateParents) {
|
||||||
|
this.parent = source.parent
|
||||||
|
}
|
||||||
this.icon = source.icon
|
this.icon = source.icon
|
||||||
this.creationTime = DateInstant(source.creationTime)
|
this.creationTime = DateInstant(source.creationTime)
|
||||||
this.lastModificationTime = DateInstant(source.lastModificationTime)
|
this.lastModificationTime = DateInstant(source.lastModificationTime)
|
||||||
|
|||||||
@@ -23,10 +23,8 @@ import android.os.ParcelUuid
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.LinkedHashMap
|
|
||||||
|
|
||||||
class Template : Parcelable {
|
class Template : Parcelable {
|
||||||
|
|
||||||
@@ -34,6 +32,8 @@ class Template : Parcelable {
|
|||||||
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
var uuid: UUID = DatabaseVersioned.UUID_ZERO
|
||||||
var title = ""
|
var title = ""
|
||||||
var icon = IconImage()
|
var icon = IconImage()
|
||||||
|
var backgroundColor: Int? = null
|
||||||
|
var foregroundColor: Int? = null
|
||||||
var sections: MutableList<TemplateSection> = ArrayList()
|
var sections: MutableList<TemplateSection> = ArrayList()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -41,7 +41,8 @@ class Template : Parcelable {
|
|||||||
title: String,
|
title: String,
|
||||||
icon: IconImage,
|
icon: IconImage,
|
||||||
section: TemplateSection,
|
section: TemplateSection,
|
||||||
version: Int = 1): this(uuid, title, icon, ArrayList<TemplateSection>().apply {
|
version: Int = 1)
|
||||||
|
: this(uuid, title, icon, ArrayList<TemplateSection>().apply {
|
||||||
add(section)
|
add(section)
|
||||||
}, version)
|
}, version)
|
||||||
|
|
||||||
@@ -49,11 +50,22 @@ class Template : Parcelable {
|
|||||||
title: String,
|
title: String,
|
||||||
icon: IconImage,
|
icon: IconImage,
|
||||||
sections: List<TemplateSection>,
|
sections: List<TemplateSection>,
|
||||||
|
version: Int = 1)
|
||||||
|
: this(uuid, title, icon, null, null, sections, version)
|
||||||
|
|
||||||
|
constructor(uuid: UUID,
|
||||||
|
title: String,
|
||||||
|
icon: IconImage,
|
||||||
|
backgroundColor: Int?,
|
||||||
|
foregroundColor: Int?,
|
||||||
|
sections: List<TemplateSection>,
|
||||||
version: Int = 1) {
|
version: Int = 1) {
|
||||||
this.version = version
|
this.version = version
|
||||||
this.uuid = uuid
|
this.uuid = uuid
|
||||||
this.title = title
|
this.title = title
|
||||||
this.icon = icon
|
this.icon = icon
|
||||||
|
this.backgroundColor = backgroundColor
|
||||||
|
this.foregroundColor = foregroundColor
|
||||||
this.sections.clear()
|
this.sections.clear()
|
||||||
this.sections.addAll(sections)
|
this.sections.addAll(sections)
|
||||||
}
|
}
|
||||||
@@ -63,6 +75,8 @@ class Template : Parcelable {
|
|||||||
this.uuid = template.uuid
|
this.uuid = template.uuid
|
||||||
this.title = template.title
|
this.title = template.title
|
||||||
this.icon = template.icon
|
this.icon = template.icon
|
||||||
|
this.backgroundColor = template.backgroundColor
|
||||||
|
this.foregroundColor = template.foregroundColor
|
||||||
this.sections.clear()
|
this.sections.clear()
|
||||||
this.sections.addAll(template.sections)
|
this.sections.addAll(template.sections)
|
||||||
}
|
}
|
||||||
@@ -72,6 +86,8 @@ class Template : Parcelable {
|
|||||||
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: uuid
|
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: uuid
|
||||||
title = parcel.readString() ?: title
|
title = parcel.readString() ?: title
|
||||||
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
|
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
|
||||||
|
backgroundColor = parcel.readInt()
|
||||||
|
foregroundColor = parcel.readInt()
|
||||||
parcel.readList(sections, TemplateSection::class.java.classLoader)
|
parcel.readList(sections, TemplateSection::class.java.classLoader)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +96,8 @@ class Template : Parcelable {
|
|||||||
parcel.writeParcelable(ParcelUuid(uuid), flags)
|
parcel.writeParcelable(ParcelUuid(uuid), flags)
|
||||||
parcel.writeString(title)
|
parcel.writeString(title)
|
||||||
parcel.writeParcelable(icon, flags)
|
parcel.writeParcelable(icon, flags)
|
||||||
|
parcel.writeInt(backgroundColor ?: -1)
|
||||||
|
parcel.writeInt(foregroundColor ?: -1)
|
||||||
parcel.writeList(sections)
|
parcel.writeList(sections)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.element.template
|
package com.kunzisoft.keepass.database.element.template
|
||||||
|
|
||||||
enum class TemplateAttributeAction {
|
enum class TemplateAttributeAction {
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.element.template
|
package com.kunzisoft.keepass.database.element.template
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.element.template
|
package com.kunzisoft.keepass.database.element.template
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.LinkedHashMap
|
|
||||||
|
|
||||||
class TemplateBuilder {
|
class TemplateBuilder {
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
package com.kunzisoft.keepass.database.element.template
|
package com.kunzisoft.keepass.database.element.template
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Color
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
@@ -26,7 +44,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
|
|||||||
if (templateGroup != null) {
|
if (templateGroup != null) {
|
||||||
templates.add(Template.STANDARD)
|
templates.add(Template.STANDARD)
|
||||||
templateGroup.getChildEntries().forEach { templateEntry ->
|
templateGroup.getChildEntries().forEach { templateEntry ->
|
||||||
getTemplateFromTemplateEntry(templateEntry)?.let {
|
getTemplateFromTemplateEntry(templateEntry).let {
|
||||||
mCacheTemplates[templateEntry.id] = it
|
mCacheTemplates[templateEntry.id] = it
|
||||||
templates.add(it)
|
templates.add(it)
|
||||||
}
|
}
|
||||||
@@ -70,7 +88,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
|
|||||||
return mCacheTemplates[uuid]
|
return mCacheTemplates[uuid]
|
||||||
else {
|
else {
|
||||||
mDatabase.getEntryById(uuid)?.let { templateEntry ->
|
mDatabase.getEntryById(uuid)?.let { templateEntry ->
|
||||||
getTemplateFromTemplateEntry(templateEntry)?.let { newTemplate ->
|
getTemplateFromTemplateEntry(templateEntry).let { newTemplate ->
|
||||||
mCacheTemplates[uuid] = newTemplate
|
mCacheTemplates[uuid] = newTemplate
|
||||||
return newTemplate
|
return newTemplate
|
||||||
}
|
}
|
||||||
@@ -134,7 +152,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
|
|||||||
return TemplateSection(sectionAttributes)
|
return TemplateSection(sectionAttributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTemplateFromTemplateEntry(templateEntry: EntryKDBX): Template? {
|
private fun getTemplateFromTemplateEntry(templateEntry: EntryKDBX): Template {
|
||||||
|
|
||||||
val templateEntryDecoded = decodeTemplateEntry(templateEntry)
|
val templateEntryDecoded = decodeTemplateEntry(templateEntry)
|
||||||
val templateSections = mutableListOf<TemplateSection>()
|
val templateSections = mutableListOf<TemplateSection>()
|
||||||
@@ -149,7 +167,28 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
|
|||||||
}
|
}
|
||||||
templateSections.add(buildTemplateSectionFromFields(sectionFields))
|
templateSections.add(buildTemplateSectionFromFields(sectionFields))
|
||||||
|
|
||||||
return Template(templateEntry.id, templateEntry.title, templateEntry.icon, templateSections, getVersion())
|
var backgroundColor: Int? = null
|
||||||
|
templateEntry.backgroundColor.let {
|
||||||
|
try {
|
||||||
|
backgroundColor = Color.parseColor(it)
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
}
|
||||||
|
var foregroundColor: Int? = null
|
||||||
|
templateEntry.foregroundColor.let {
|
||||||
|
try {
|
||||||
|
foregroundColor = Color.parseColor(it)
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Template(
|
||||||
|
templateEntry.id,
|
||||||
|
templateEntry.title,
|
||||||
|
templateEntry.icon,
|
||||||
|
backgroundColor,
|
||||||
|
foregroundColor,
|
||||||
|
templateSections,
|
||||||
|
getVersion()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.element.template
|
package com.kunzisoft.keepass.database.element.template
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -208,16 +226,8 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
|||||||
when (attribute.type) {
|
when (attribute.type) {
|
||||||
TemplateAttributeType.TEXT -> {
|
TemplateAttributeType.TEXT -> {
|
||||||
try {
|
try {
|
||||||
when (attribute.options.getNumberLines()) {
|
// It's always a number of lines...
|
||||||
1 -> {
|
attribute.options.setNumberLines(defaultOption.toInt())
|
||||||
// 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to transform default text option", e)
|
Log.e(TAG, "Unable to transform default text option", e)
|
||||||
}
|
}
|
||||||
@@ -265,6 +275,9 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
|||||||
entryCopy.putField(field)
|
entryCopy.putField(field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Add colors
|
||||||
|
entryCopy.foregroundColor = templateEntry.foregroundColor
|
||||||
|
entryCopy.backgroundColor = templateEntry.backgroundColor
|
||||||
|
|
||||||
return entryCopy
|
return entryCopy
|
||||||
}
|
}
|
||||||
@@ -367,6 +380,9 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Add colors
|
||||||
|
entryCopy.foregroundColor = templateEntry.foregroundColor
|
||||||
|
entryCopy.backgroundColor = templateEntry.backgroundColor
|
||||||
return entryCopy
|
return entryCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.element.template
|
package com.kunzisoft.keepass.database.element.template
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -27,7 +45,7 @@ object TemplateField {
|
|||||||
const val LABEL_DATE_OF_ISSUE = "Date of issue"
|
const val LABEL_DATE_OF_ISSUE = "Date of issue"
|
||||||
const val LABEL_EMAIL = "Email"
|
const val LABEL_EMAIL = "Email"
|
||||||
const val LABEL_EMAIL_ADDRESS = "Email address"
|
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_SSID = "SSID"
|
||||||
const val LABEL_TYPE = "Type"
|
const val LABEL_TYPE = "Type"
|
||||||
const val LABEL_CRYPTOCURRENCY = "Cryptocurrency wallet"
|
const val LABEL_CRYPTOCURRENCY = "Cryptocurrency wallet"
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.element.template
|
package com.kunzisoft.keepass.database.element.template
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ open class LoadDatabaseException : DatabaseException {
|
|||||||
@StringRes
|
@StringRes
|
||||||
override var errorId: Int = R.string.error_load_database
|
override var errorId: Int = R.string.error_load_database
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
|
constructor(string: String) : super(string)
|
||||||
constructor(throwable: Throwable) : super(throwable)
|
constructor(throwable: Throwable) : super(throwable)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ class FileNotFoundDatabaseException : LoadDatabaseException {
|
|||||||
@StringRes
|
@StringRes
|
||||||
override var errorId: Int = R.string.file_not_found_content
|
override var errorId: Int = R.string.file_not_found_content
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
|
constructor(string: String) : super(string)
|
||||||
constructor(exception: Throwable) : super(exception)
|
constructor(exception: Throwable) : super(exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ class IODatabaseException : LoadDatabaseException {
|
|||||||
@StringRes
|
@StringRes
|
||||||
override var errorId: Int = R.string.error_load_database
|
override var errorId: Int = R.string.error_load_database
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
|
constructor(string: String) : super(string)
|
||||||
constructor(exception: Throwable) : super(exception)
|
constructor(exception: Throwable) : super(exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file
|
package com.kunzisoft.keepass.database.file
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
|
|
||||||
abstract class DatabaseHeader {
|
abstract class DatabaseHeader {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,8 +31,4 @@ abstract class DatabaseHeader {
|
|||||||
*/
|
*/
|
||||||
var encryptionIV = ByteArray(16)
|
var encryptionIV = ByteArray(16)
|
||||||
|
|
||||||
companion object {
|
|
||||||
val PWM_DBSIG_1 = UnsignedInt(-0x655d26fd)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class DatabaseHeaderKDB : DatabaseHeader() {
|
|||||||
*/
|
*/
|
||||||
var transformSeed = ByteArray(32)
|
var transformSeed = ByteArray(32)
|
||||||
|
|
||||||
var signature1 = UnsignedInt(0) // = PWM_DBSIG_1
|
var signature1 = UnsignedInt(0) // = DBSIG_1
|
||||||
var signature2 = UnsignedInt(0) // = DBSIG_2
|
var signature2 = UnsignedInt(0) // = DBSIG_2
|
||||||
var flags= UnsignedInt(0)
|
var flags= UnsignedInt(0)
|
||||||
var version= UnsignedInt(0)
|
var version= UnsignedInt(0)
|
||||||
@@ -84,9 +84,9 @@ class DatabaseHeaderKDB : DatabaseHeader() {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
// DB sig from KeePass 1.03
|
// DB sig from KeePass 1.03
|
||||||
val DBSIG_2 = UnsignedInt(-0x4ab4049b)
|
val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
|
||||||
// DB sig from KeePass 1.03
|
val DBSIG_2 = UnsignedInt(-0x4ab4049b) // 0xB54BFB65
|
||||||
val DBVER_DW = UnsignedInt(0x00030003)
|
val DBVER_DW = UnsignedInt(0x00030004)
|
||||||
|
|
||||||
val FLAG_SHA2 = UnsignedInt(1)
|
val FLAG_SHA2 = UnsignedInt(1)
|
||||||
val FLAG_RIJNDAEL = UnsignedInt(2)
|
val FLAG_RIJNDAEL = UnsignedInt(2)
|
||||||
@@ -97,7 +97,7 @@ class DatabaseHeaderKDB : DatabaseHeader() {
|
|||||||
const val BUF_SIZE = 124
|
const val BUF_SIZE = 124
|
||||||
|
|
||||||
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
|
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
|
||||||
return sig1.toKotlinInt() == PWM_DBSIG_1.toKotlinInt() && sig2.toKotlinInt() == DBSIG_2.toKotlinInt()
|
return sig1.toKotlinInt() == DBSIG_1.toKotlinInt() && sig2.toKotlinInt() == DBSIG_2.toKotlinInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun compatibleHeaders(one: UnsignedInt, two: UnsignedInt): Boolean {
|
fun compatibleHeaders(one: UnsignedInt, two: UnsignedInt): Boolean {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.database.file
|
package com.kunzisoft.keepass.database.file
|
||||||
|
|
||||||
import com.kunzisoft.encrypt.HashManager
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
|
||||||
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||||
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
|
import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
|
||||||
@@ -28,9 +27,6 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
|||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
|
||||||
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
||||||
import com.kunzisoft.keepass.stream.CopyInputStream
|
import com.kunzisoft.keepass.stream.CopyInputStream
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
@@ -87,71 +83,10 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
inner class HeaderAndHash(var header: ByteArray, var hash: ByteArray)
|
inner class HeaderAndHash(var header: ByteArray, var hash: ByteArray)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.version = getMinKdbxVersion(databaseV4) // Only for writing
|
this.version = databaseV4.getMinKdbxVersion()
|
||||||
this.masterSeed = ByteArray(32)
|
this.masterSeed = ByteArray(32)
|
||||||
}
|
}
|
||||||
|
|
||||||
private open class NodeOperationHandler<T: NodeKDBXInterface> : NodeHandler<T>() {
|
|
||||||
var containsCustomData = false
|
|
||||||
override fun operate(node: T): Boolean {
|
|
||||||
if (node.customData.isNotEmpty()) {
|
|
||||||
containsCustomData = true
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class EntryOperationHandler: NodeOperationHandler<EntryKDBX>() {
|
|
||||||
var passwordQualityEstimationDisabled = false
|
|
||||||
override fun operate(node: EntryKDBX): Boolean {
|
|
||||||
if (!node.qualityCheck) {
|
|
||||||
passwordQualityEstimationDisabled = true
|
|
||||||
}
|
|
||||||
return super.operate(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class GroupOperationHandler: NodeOperationHandler<GroupKDBX>() {
|
|
||||||
var containsTags = false
|
|
||||||
override fun operate(node: GroupKDBX): Boolean {
|
|
||||||
if (!node.tags.isEmpty())
|
|
||||||
containsTags = true
|
|
||||||
return super.operate(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMinKdbxVersion(databaseKDBX: DatabaseKDBX): UnsignedInt {
|
|
||||||
val entryHandler = EntryOperationHandler()
|
|
||||||
val groupHandler = GroupOperationHandler()
|
|
||||||
databaseKDBX.rootGroup?.doForEachChildAndForIt(entryHandler, groupHandler)
|
|
||||||
|
|
||||||
// https://keepass.info/help/kb/kdbx_4.1.html
|
|
||||||
val containsGroupWithTag = groupHandler.containsTags
|
|
||||||
val containsEntryWithPasswordQualityEstimationDisabled = entryHandler.passwordQualityEstimationDisabled
|
|
||||||
val containsCustomIconWithNameOrLastModificationTime = databaseKDBX.iconsManager.containsCustomIconWithNameOrLastModificationTime()
|
|
||||||
val containsHeaderCustomDataWithLastModificationTime = databaseKDBX.customData.containsItemWithLastModificationTime()
|
|
||||||
|
|
||||||
// https://keepass.info/help/kb/kdbx_4.html
|
|
||||||
// If AES is not use, it's at least 4.0
|
|
||||||
val kdfIsNotAes = databaseKDBX.kdfParameters?.uuid != AesKdf.CIPHER_UUID
|
|
||||||
val containsHeaderCustomData = databaseKDBX.customData.isNotEmpty()
|
|
||||||
val containsNodeCustomData = entryHandler.containsCustomData || groupHandler.containsCustomData
|
|
||||||
|
|
||||||
// Check each condition to determine version
|
|
||||||
return if (containsGroupWithTag
|
|
||||||
|| containsEntryWithPasswordQualityEstimationDisabled
|
|
||||||
|| containsCustomIconWithNameOrLastModificationTime
|
|
||||||
|| containsHeaderCustomDataWithLastModificationTime) {
|
|
||||||
FILE_VERSION_41
|
|
||||||
} else if (kdfIsNotAes
|
|
||||||
|| containsHeaderCustomData
|
|
||||||
|| containsNodeCustomData) {
|
|
||||||
FILE_VERSION_40
|
|
||||||
} else {
|
|
||||||
FILE_VERSION_31
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Assumes the input stream is at the beginning of the .kdbx file
|
/** Assumes the input stream is at the beginning of the .kdbx file
|
||||||
* @param inputStream
|
* @param inputStream
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
@@ -256,8 +191,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
if (pbId == null || pbId.size != 16) {
|
if (pbId == null || pbId.size != 16) {
|
||||||
throw IOException("Invalid cipher ID.")
|
throw IOException("Invalid cipher ID.")
|
||||||
}
|
}
|
||||||
|
databaseV4.setEncryptionAlgorithmFromUUID(bytes16ToUuid(pbId))
|
||||||
databaseV4.cipherUuid = bytes16ToUuid(pbId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setTransformRound(roundsByte: ByteArray) {
|
private fun setTransformRound(roundsByte: ByteArray) {
|
||||||
@@ -311,8 +245,9 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a)
|
val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
|
||||||
val DBSIG_2 = UnsignedInt(-0x4ab40499)
|
val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a) // 0xB54BFB66
|
||||||
|
val DBSIG_2 = UnsignedInt(-0x4ab40499) // 0xB54BFB67
|
||||||
|
|
||||||
private val FILE_VERSION_CRITICAL_MASK = UnsignedInt(-0x10000)
|
private val FILE_VERSION_CRITICAL_MASK = UnsignedInt(-0x10000)
|
||||||
val FILE_VERSION_31 = UnsignedInt(0x00030001)
|
val FILE_VERSION_31 = UnsignedInt(0x00030001)
|
||||||
@@ -335,7 +270,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
|
fun matchesHeader(sig1: UnsignedInt, sig2: UnsignedInt): Boolean {
|
||||||
return sig1 == PWM_DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
|
return sig1 == DBSIG_1 && (sig2 == DBSIG_PRE2 || sig2 == DBSIG_2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,16 +21,12 @@ package com.kunzisoft.keepass.database.file.input
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>>
|
abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var mDatabase: D) {
|
||||||
(protected val cacheDirectory: File,
|
|
||||||
protected val isRAMSufficient: (memoryWanted: Long) -> Boolean) {
|
|
||||||
|
|
||||||
private var startTimeKey = System.currentTimeMillis()
|
private var startTimeKey = System.currentTimeMillis()
|
||||||
private var startTimeContent = System.currentTimeMillis()
|
private var startTimeContent = System.currentTimeMillis()
|
||||||
@@ -49,17 +45,13 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>>
|
|||||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyfileInputStream: InputStream?,
|
keyfileInputStream: InputStream?,
|
||||||
loadedCipherKey: LoadedKey,
|
progressTaskUpdater: ProgressTaskUpdater?): D
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
|
||||||
fixDuplicateUUID: Boolean = false): D
|
|
||||||
|
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||||
masterKey: ByteArray,
|
masterKey: ByteArray,
|
||||||
loadedCipherKey: LoadedKey,
|
progressTaskUpdater: ProgressTaskUpdater?): D
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
|
||||||
fixDuplicateUUID: Boolean = false): D
|
|
||||||
|
|
||||||
protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) {
|
protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
||||||
|
|||||||
@@ -20,17 +20,16 @@
|
|||||||
|
|
||||||
package com.kunzisoft.keepass.database.file.input
|
package com.kunzisoft.keepass.database.file.input
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
import com.kunzisoft.encrypt.HashManager
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
@@ -46,21 +45,15 @@ import kotlin.collections.HashMap
|
|||||||
/**
|
/**
|
||||||
* Load a KDB database file.
|
* Load a KDB database file.
|
||||||
*/
|
*/
|
||||||
class DatabaseInputKDB(cacheDirectory: File,
|
class DatabaseInputKDB(database: DatabaseKDB)
|
||||||
isRAMSufficient: (memoryWanted: Long) -> Boolean)
|
: DatabaseInput<DatabaseKDB>(database) {
|
||||||
: DatabaseInput<DatabaseKDB>(cacheDirectory, isRAMSufficient) {
|
|
||||||
|
|
||||||
private lateinit var mDatabase: DatabaseKDB
|
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyfileInputStream: InputStream?,
|
keyfileInputStream: InputStream?,
|
||||||
loadedCipherKey: LoadedKey,
|
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDB {
|
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
|
||||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
|
||||||
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,11 +61,8 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
masterKey: ByteArray,
|
masterKey: ByteArray,
|
||||||
loadedCipherKey: LoadedKey,
|
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDB {
|
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
|
||||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
|
||||||
mDatabase.masterKey = masterKey
|
mDatabase.masterKey = masterKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +70,6 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
private fun openDatabase(databaseInputStream: InputStream,
|
private fun openDatabase(databaseInputStream: InputStream,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean,
|
|
||||||
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
|
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -98,7 +87,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
if (fileSize != (contentSize + DatabaseHeaderKDB.BUF_SIZE))
|
if (fileSize != (contentSize + DatabaseHeaderKDB.BUF_SIZE))
|
||||||
throw IOException("Header corrupted")
|
throw IOException("Header corrupted")
|
||||||
|
|
||||||
if (header.signature1 != DatabaseHeader.PWM_DBSIG_1
|
if (header.signature1 != DatabaseHeaderKDB.DBSIG_1
|
||||||
|| header.signature2 != DatabaseHeaderKDB.DBSIG_2) {
|
|| header.signature2 != DatabaseHeaderKDB.DBSIG_2) {
|
||||||
throw SignatureDatabaseException()
|
throw SignatureDatabaseException()
|
||||||
}
|
}
|
||||||
@@ -107,10 +96,6 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
throw VersionDatabaseException()
|
throw VersionDatabaseException()
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabase = DatabaseKDB()
|
|
||||||
mDatabase.binaryCache.cacheDirectory = cacheDirectory
|
|
||||||
|
|
||||||
mDatabase.changeDuplicateId = fixDuplicateUUID
|
|
||||||
assignMasterKey?.invoke()
|
assignMasterKey?.invoke()
|
||||||
|
|
||||||
// Select algorithm
|
// Select algorithm
|
||||||
@@ -153,10 +138,6 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
|
||||||
val newRoot = mDatabase.createGroup()
|
|
||||||
mDatabase.rootGroup = newRoot
|
|
||||||
|
|
||||||
// Import all nodes
|
// Import all nodes
|
||||||
val groupLevelList = HashMap<GroupKDB, Int>()
|
val groupLevelList = HashMap<GroupKDB, Int>()
|
||||||
var newGroup: GroupKDB? = null
|
var newGroup: GroupKDB? = null
|
||||||
@@ -285,7 +266,7 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
0x000E -> {
|
0x000E -> {
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
if (fieldSize > 0) {
|
if (fieldSize > 0) {
|
||||||
val binaryData = mDatabase.buildNewAttachment()
|
val binaryData = mDatabase.buildNewBinaryAttachment()
|
||||||
entry.putBinary(binaryData, mDatabase.attachmentPool)
|
entry.putBinary(binaryData, mDatabase.attachmentPool)
|
||||||
BufferedOutputStream(binaryData.getOutputDataStream(mDatabase.binaryCache)).use { outputStream ->
|
BufferedOutputStream(binaryData.getOutputDataStream(mDatabase.binaryCache)).use { outputStream ->
|
||||||
cipherInputStream.readBytes(fieldSize) { buffer ->
|
cipherInputStream.readBytes(fieldSize) { buffer ->
|
||||||
@@ -303,7 +284,34 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
newGroup = null
|
newGroup = null
|
||||||
}
|
}
|
||||||
newEntry?.let { entry ->
|
newEntry?.let { entry ->
|
||||||
mDatabase.addEntryIndex(entry)
|
// Parse meta info
|
||||||
|
when {
|
||||||
|
entry.isMetaStreamDefaultUsername() -> {
|
||||||
|
var defaultUser = ""
|
||||||
|
entry.getBinary(mDatabase.attachmentPool)
|
||||||
|
?.getInputDataStream(mDatabase.binaryCache)?.use {
|
||||||
|
defaultUser = String(it.readBytes())
|
||||||
|
}
|
||||||
|
mDatabase.defaultUserName = defaultUser
|
||||||
|
}
|
||||||
|
entry.isMetaStreamDatabaseColor() -> {
|
||||||
|
var color: Int? = null
|
||||||
|
entry.getBinary(mDatabase.attachmentPool)
|
||||||
|
?.getInputDataStream(mDatabase.binaryCache)?.use {
|
||||||
|
val reverseColor = UnsignedInt(it.readBytes4ToUInt()).toKotlinInt()
|
||||||
|
color = Color.rgb(
|
||||||
|
Color.blue(reverseColor),
|
||||||
|
Color.green(reverseColor),
|
||||||
|
Color.red(reverseColor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
mDatabase.color = color
|
||||||
|
}
|
||||||
|
// TODO manager other meta stream
|
||||||
|
else -> {
|
||||||
|
mDatabase.addEntryIndex(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
currentEntryNumber++
|
currentEntryNumber++
|
||||||
newEntry = null
|
newEntry = null
|
||||||
}
|
}
|
||||||
@@ -323,16 +331,16 @@ class DatabaseInputKDB(cacheDirectory: File,
|
|||||||
stopContentTimer()
|
stopContentTimer()
|
||||||
|
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
mDatabase.clearCache()
|
mDatabase.clearAll()
|
||||||
throw e
|
throw e
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
mDatabase.clearCache()
|
mDatabase.clearAll()
|
||||||
throw IODatabaseException(e)
|
throw IODatabaseException(e)
|
||||||
} catch (e: OutOfMemoryError) {
|
} catch (e: OutOfMemoryError) {
|
||||||
mDatabase.clearCache()
|
mDatabase.clearAll()
|
||||||
throw NoMemoryDatabaseException(e)
|
throw NoMemoryDatabaseException(e)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
mDatabase.clearCache()
|
mDatabase.clearAll()
|
||||||
throw LoadDatabaseException(e)
|
throw LoadDatabaseException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
|||||||
import com.kunzisoft.keepass.database.crypto.HmacBlock
|
import com.kunzisoft.keepass.database.crypto.HmacBlock
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
@@ -50,7 +50,6 @@ import com.kunzisoft.keepass.utils.*
|
|||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import org.xmlpull.v1.XmlPullParserException
|
import org.xmlpull.v1.XmlPullParserException
|
||||||
import org.xmlpull.v1.XmlPullParserFactory
|
import org.xmlpull.v1.XmlPullParserFactory
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
@@ -63,12 +62,10 @@ import javax.crypto.CipherInputStream
|
|||||||
import javax.crypto.Mac
|
import javax.crypto.Mac
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class DatabaseInputKDBX(cacheDirectory: File,
|
class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||||
isRAMSufficient: (memoryWanted: Long) -> Boolean)
|
: DatabaseInput<DatabaseKDBX>(database) {
|
||||||
: DatabaseInput<DatabaseKDBX>(cacheDirectory, isRAMSufficient) {
|
|
||||||
|
|
||||||
private var randomStream: StreamCipher? = null
|
private var randomStream: StreamCipher? = null
|
||||||
private lateinit var mDatabase: DatabaseKDBX
|
|
||||||
|
|
||||||
private var hashOfHeader: ByteArray? = null
|
private var hashOfHeader: ByteArray? = null
|
||||||
|
|
||||||
@@ -97,15 +94,18 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
private var entryCustomDataKey: String? = null
|
private var entryCustomDataKey: String? = null
|
||||||
private var entryCustomDataValue: String? = null
|
private var entryCustomDataValue: String? = null
|
||||||
|
|
||||||
|
private var isRAMSufficient: (memoryWanted: Long) -> Boolean = {true}
|
||||||
|
|
||||||
|
fun setMethodToCheckIfRAMIsSufficient(method: (memoryWanted: Long) -> Boolean) {
|
||||||
|
this.isRAMSufficient = method
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyfileInputStream: InputStream?,
|
keyfileInputStream: InputStream?,
|
||||||
loadedCipherKey: LoadedKey,
|
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
|
||||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
|
||||||
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,11 +113,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
override fun openDatabase(databaseInputStream: InputStream,
|
override fun openDatabase(databaseInputStream: InputStream,
|
||||||
masterKey: ByteArray,
|
masterKey: ByteArray,
|
||||||
loadedCipherKey: LoadedKey,
|
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||||
fixDuplicateUUID: Boolean): DatabaseKDBX {
|
|
||||||
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
|
|
||||||
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
|
|
||||||
mDatabase.masterKey = masterKey
|
mDatabase.masterKey = masterKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,14 +122,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
private fun openDatabase(databaseInputStream: InputStream,
|
private fun openDatabase(databaseInputStream: InputStream,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?,
|
progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
fixDuplicateUUID: Boolean,
|
|
||||||
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
|
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
|
||||||
try {
|
try {
|
||||||
startKeyTimer(progressTaskUpdater)
|
startKeyTimer(progressTaskUpdater)
|
||||||
mDatabase = DatabaseKDBX()
|
|
||||||
mDatabase.binaryCache.cacheDirectory = cacheDirectory
|
|
||||||
|
|
||||||
mDatabase.changeDuplicateId = fixDuplicateUUID
|
|
||||||
|
|
||||||
val header = DatabaseHeaderKDBX(mDatabase)
|
val header = DatabaseHeaderKDBX(mDatabase)
|
||||||
|
|
||||||
@@ -148,13 +140,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
stopKeyTimer()
|
stopKeyTimer()
|
||||||
startContentTimer(progressTaskUpdater)
|
startContentTimer(progressTaskUpdater)
|
||||||
|
|
||||||
val engine: CipherEngine
|
|
||||||
val cipher: Cipher
|
val cipher: Cipher
|
||||||
try {
|
try {
|
||||||
engine = EncryptionAlgorithm.getFrom(mDatabase.cipherUuid).cipherEngine
|
val engine: CipherEngine = mDatabase.encryptionAlgorithm.cipherEngine
|
||||||
engine.forcePaddingCompatibility = true
|
engine.forcePaddingCompatibility = true
|
||||||
mDatabase.setDataEngine(engine)
|
|
||||||
mDatabase.encryptionAlgorithm = engine.getEncryptionAlgorithm()
|
|
||||||
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
|
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
|
||||||
engine.forcePaddingCompatibility = false
|
engine.forcePaddingCompatibility = false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -288,7 +277,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||||
val byteLength = size - 1
|
val byteLength = size - 1
|
||||||
// No compression at this level
|
// No compression at this level
|
||||||
val protectedBinary = mDatabase.buildNewAttachment(
|
val protectedBinary = mDatabase.buildNewBinaryAttachment(
|
||||||
isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag)
|
isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag)
|
||||||
protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
|
protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
|
||||||
dataInputStream.readBytes(byteLength) { buffer ->
|
dataInputStream.readBytes(byteLength) { buffer ->
|
||||||
@@ -524,7 +513,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
||||||
ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
||||||
ctxGroup?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
|
val iconUUID = readUuid(xpp)
|
||||||
|
ctxGroup?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemTags, ignoreCase = true)) {
|
||||||
ctxGroup?.tags = readTags(xpp)
|
ctxGroup?.tags = readTags(xpp)
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
|
||||||
@@ -583,7 +573,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
|
||||||
ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemCustomIconID, ignoreCase = true)) {
|
||||||
ctxEntry?.icon?.custom = mDatabase.getCustomIcon(readUuid(xpp))
|
val iconUUID = readUuid(xpp)
|
||||||
|
ctxEntry?.icon?.custom = mDatabase.getCustomIcon(iconUUID) ?: IconImageCustom(iconUUID)
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemFgColor, ignoreCase = true)) {
|
||||||
ctxEntry?.foregroundColor = readString(xpp)
|
ctxEntry?.foregroundColor = readString(xpp)
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemBgColor, ignoreCase = true)) {
|
||||||
@@ -704,7 +695,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
|
KdbContext.DeletedObject -> if (name.equals(DatabaseKDBXXML.ElemUuid, ignoreCase = true)) {
|
||||||
ctxDeletedObject?.uuid = readUuid(xpp)
|
ctxDeletedObject?.uuid = readUuid(xpp)
|
||||||
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
|
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
|
||||||
ctxDeletedObject?.setDeletionTime(readDateInstant(xpp))
|
ctxDeletedObject?.deletionTime = readDateInstant(xpp)
|
||||||
} else {
|
} else {
|
||||||
readUnknown(xpp)
|
readUnknown(xpp)
|
||||||
}
|
}
|
||||||
@@ -1009,7 +1000,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
var binaryRetrieve = mDatabase.attachmentPool[id]
|
var binaryRetrieve = mDatabase.attachmentPool[id]
|
||||||
// Create empty binary if not retrieved in pool
|
// Create empty binary if not retrieved in pool
|
||||||
if (binaryRetrieve == null) {
|
if (binaryRetrieve == null) {
|
||||||
binaryRetrieve = mDatabase.buildNewAttachment(
|
binaryRetrieve = mDatabase.buildNewBinaryAttachment(
|
||||||
smallSize = false,
|
smallSize = false,
|
||||||
compression = false,
|
compression = false,
|
||||||
protection = false,
|
protection = false,
|
||||||
@@ -1049,7 +1040,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
// Build the new binary and compress
|
// Build the new binary and compress
|
||||||
val binaryAttachment = mDatabase.buildNewAttachment(
|
val binaryAttachment = mDatabase.buildNewBinaryAttachment(
|
||||||
isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId)
|
isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId)
|
||||||
try {
|
try {
|
||||||
binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
|
binaryAttachment.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
|||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
|
||||||
import com.kunzisoft.keepass.stream.MacOutputStream
|
import com.kunzisoft.keepass.stream.MacOutputStream
|
||||||
@@ -68,11 +67,11 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
|||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun output() {
|
fun output() {
|
||||||
|
|
||||||
mos.write4BytesUInt(DatabaseHeader.PWM_DBSIG_1)
|
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_1)
|
||||||
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2)
|
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2)
|
||||||
mos.write4BytesUInt(header.version)
|
mos.write4BytesUInt(header.version)
|
||||||
|
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.cipherUuid))
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CipherID, uuidTo16Bytes(databaseKDBX.encryptionAlgorithm.uuid))
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.CompressionFlags, uIntTo4Bytes(DatabaseHeaderKDBX.getFlagFromCompression(databaseKDBX.compressionAlgorithm)))
|
||||||
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
|
writeHeaderField(DatabaseHeaderKDBX.PwDbHeaderV4Fields.MasterSeed, header.masterSeed)
|
||||||
|
|
||||||
@@ -130,6 +129,6 @@ constructor(private val databaseKDBX: DatabaseKDBX,
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.file.output
|
package com.kunzisoft.keepass.database.file.output
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
import com.kunzisoft.encrypt.HashManager
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||||
@@ -34,7 +36,6 @@ import java.io.ByteArrayOutputStream
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.*
|
import java.security.*
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherOutputStream
|
import javax.crypto.CipherOutputStream
|
||||||
|
|
||||||
@@ -44,6 +45,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
|
|
||||||
private var headerHashBlock: ByteArray? = null
|
private var headerHashBlock: ByteArray? = null
|
||||||
|
|
||||||
|
private var mGroupList = mutableListOf<GroupKDB>()
|
||||||
|
private var mEntryList = mutableListOf<EntryKDB>()
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
fun getFinalKey(header: DatabaseHeader): ByteArray? {
|
fun getFinalKey(header: DatabaseHeader): ByteArray? {
|
||||||
try {
|
try {
|
||||||
@@ -61,7 +65,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
// and remove any orphaned nodes that are no longer part of the tree hierarchy
|
// and remove any orphaned nodes that are no longer part of the tree hierarchy
|
||||||
// also remove the virtual root not present in kdb
|
// also remove the virtual root not present in kdb
|
||||||
val rootGroup = mDatabaseKDB.rootGroup
|
val rootGroup = mDatabaseKDB.rootGroup
|
||||||
sortGroupsForOutput()
|
sortNodesForOutput()
|
||||||
|
|
||||||
val header = outputHeader(mOutputStream)
|
val header = outputHeader(mOutputStream)
|
||||||
|
|
||||||
@@ -91,6 +95,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
} finally {
|
} finally {
|
||||||
// Add again the virtual root group for better management
|
// Add again the virtual root group for better management
|
||||||
mDatabaseKDB.rootGroup = rootGroup
|
mDatabaseKDB.rootGroup = rootGroup
|
||||||
|
clearParser()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +110,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB {
|
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB {
|
||||||
// Build header
|
// Build header
|
||||||
val header = DatabaseHeaderKDB()
|
val header = DatabaseHeaderKDB()
|
||||||
header.signature1 = DatabaseHeader.PWM_DBSIG_1
|
header.signature1 = DatabaseHeaderKDB.DBSIG_1
|
||||||
header.signature2 = DatabaseHeaderKDB.DBSIG_2
|
header.signature2 = DatabaseHeaderKDB.DBSIG_2
|
||||||
header.flags = DatabaseHeaderKDB.FLAG_SHA2
|
header.flags = DatabaseHeaderKDB.FLAG_SHA2
|
||||||
|
|
||||||
@@ -120,8 +125,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
}
|
}
|
||||||
|
|
||||||
header.version = DatabaseHeaderKDB.DBVER_DW
|
header.version = DatabaseHeaderKDB.DBVER_DW
|
||||||
header.numGroups = UnsignedInt(mDatabaseKDB.numberOfGroups())
|
// To remove root
|
||||||
header.numEntries = UnsignedInt(mDatabaseKDB.numberOfEntries())
|
header.numGroups = UnsignedInt(mGroupList.size)
|
||||||
|
header.numEntries = UnsignedInt(mEntryList.size)
|
||||||
header.numKeyEncRounds = UnsignedInt.fromKotlinLong(mDatabaseKDB.numberKeyEncryptionRounds)
|
header.numKeyEncRounds = UnsignedInt.fromKotlinLong(mDatabaseKDB.numberKeyEncryptionRounds)
|
||||||
|
|
||||||
setIVs(header)
|
setIVs(header)
|
||||||
@@ -194,31 +200,89 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Groups
|
// Groups
|
||||||
mDatabaseKDB.doForEachGroupInIndex { group ->
|
mGroupList.forEach { group ->
|
||||||
GroupOutputKDB(group, outputStream).output()
|
if (group != mDatabaseKDB.rootGroup) {
|
||||||
|
GroupOutputKDB(group, outputStream).output()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Entries
|
// Entries
|
||||||
mDatabaseKDB.doForEachEntryInIndex { entry ->
|
mEntryList.forEach { entry ->
|
||||||
EntryOutputKDB(mDatabaseKDB, entry, outputStream).output()
|
EntryOutputKDB(mDatabaseKDB, entry, outputStream).output()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sortGroupsForOutput() {
|
private fun clearParser() {
|
||||||
val groupList = ArrayList<GroupKDB>()
|
mGroupList.clear()
|
||||||
// Rebuild list according to sorting order removing any orphaned groups
|
mEntryList.clear()
|
||||||
for (rootGroup in mDatabaseKDB.rootGroups) {
|
|
||||||
sortGroup(rootGroup, groupList)
|
|
||||||
}
|
|
||||||
mDatabaseKDB.setGroupIndexes(groupList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sortGroup(group: GroupKDB, groupList: MutableList<GroupKDB>) {
|
private fun sortNodesForOutput() {
|
||||||
|
clearParser()
|
||||||
|
// Rebuild list according to sorting order removing any orphaned groups
|
||||||
|
// Do not keep root
|
||||||
|
mDatabaseKDB.rootGroup?.getChildGroups()?.let { rootSubGroups ->
|
||||||
|
for (rootGroup in rootSubGroups) {
|
||||||
|
sortGroup(rootGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortGroup(group: GroupKDB) {
|
||||||
// Add current tree
|
// Add current tree
|
||||||
groupList.add(group)
|
mGroupList.add(group)
|
||||||
|
|
||||||
|
for (childEntry in group.getChildEntries()) {
|
||||||
|
if (!childEntry.isMetaStreamDefaultUsername()
|
||||||
|
&& !childEntry.isMetaStreamDatabaseColor()) {
|
||||||
|
mEntryList.add(childEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add MetaStream
|
||||||
|
if (mDatabaseKDB.defaultUserName.isNotEmpty()) {
|
||||||
|
val metaEntry = EntryKDB().apply {
|
||||||
|
setMetaStreamDefaultUsername()
|
||||||
|
setDefaultUsername(this)
|
||||||
|
}
|
||||||
|
mDatabaseKDB.addEntryTo(metaEntry, group)
|
||||||
|
mEntryList.add(metaEntry)
|
||||||
|
}
|
||||||
|
if (mDatabaseKDB.color != null) {
|
||||||
|
val metaEntry = EntryKDB().apply {
|
||||||
|
setMetaStreamDatabaseColor()
|
||||||
|
setDatabaseColor(this)
|
||||||
|
}
|
||||||
|
mDatabaseKDB.addEntryTo(metaEntry, group)
|
||||||
|
mEntryList.add(metaEntry)
|
||||||
|
}
|
||||||
|
|
||||||
// Recurse over children
|
// Recurse over children
|
||||||
for (childGroup in group.getChildGroups()) {
|
for (childGroup in group.getChildGroups()) {
|
||||||
sortGroup(childGroup, groupList)
|
sortGroup(childGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setDefaultUsername(entryKDB: EntryKDB) {
|
||||||
|
val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
|
||||||
|
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
|
||||||
|
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
|
||||||
|
outputStream.write(mDatabaseKDB.defaultUserName.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setDatabaseColor(entryKDB: EntryKDB) {
|
||||||
|
val binaryData = mDatabaseKDB.buildNewBinaryAttachment()
|
||||||
|
entryKDB.putBinary(binaryData, mDatabaseKDB.attachmentPool)
|
||||||
|
BufferedOutputStream(binaryData.getOutputDataStream(mDatabaseKDB.binaryCache)).use { outputStream ->
|
||||||
|
var reversColor = Color.BLACK
|
||||||
|
mDatabaseKDB.color?.let {
|
||||||
|
reversColor = Color.rgb(
|
||||||
|
Color.blue(it),
|
||||||
|
Color.green(it),
|
||||||
|
Color.red(it)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
outputStream.write4BytesUInt(UnsignedInt(reversColor))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ import android.util.Log
|
|||||||
import android.util.Xml
|
import android.util.Xml
|
||||||
import com.kunzisoft.encrypt.StreamCipher
|
import com.kunzisoft.encrypt.StreamCipher
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
|
||||||
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
@@ -48,11 +46,9 @@ import com.kunzisoft.keepass.database.file.DateKDBXUtil
|
|||||||
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
||||||
import com.kunzisoft.keepass.stream.HmacBlockOutputStream
|
import com.kunzisoft.keepass.stream.HmacBlockOutputStream
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import org.joda.time.DateTime
|
|
||||||
import org.xmlpull.v1.XmlSerializer
|
import org.xmlpull.v1.XmlSerializer
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
@@ -70,18 +66,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
private var header: DatabaseHeaderKDBX? = null
|
private var header: DatabaseHeaderKDBX? = null
|
||||||
private var hashOfHeader: ByteArray? = null
|
private var hashOfHeader: ByteArray? = null
|
||||||
private var headerHmac: ByteArray? = null
|
private var headerHmac: ByteArray? = null
|
||||||
private var engine: CipherEngine? = null
|
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
override fun output() {
|
override fun output() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
|
||||||
engine = EncryptionAlgorithm.getFrom(mDatabaseKDBX.cipherUuid).cipherEngine
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw DatabaseOutputException("No such cipher", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
header = outputHeader(mOutputStream)
|
header = outputHeader(mOutputStream)
|
||||||
|
|
||||||
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) {
|
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) {
|
||||||
@@ -241,6 +230,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
writeString(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG)))
|
writeString(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE_64_FLAG)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeDateInstant(DatabaseKDBXXML.ElemSettingsChanged, mDatabaseKDBX.settingsChanged)
|
||||||
writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true)
|
writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true)
|
||||||
writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged)
|
writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged)
|
||||||
writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true)
|
writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true)
|
||||||
@@ -280,7 +270,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
private fun attachStreamEncryptor(header: DatabaseHeaderKDBX, os: OutputStream): CipherOutputStream {
|
private fun attachStreamEncryptor(header: DatabaseHeaderKDBX, os: OutputStream): CipherOutputStream {
|
||||||
val cipher: Cipher
|
val cipher: Cipher
|
||||||
try {
|
try {
|
||||||
cipher = engine!!.getCipher(Cipher.ENCRYPT_MODE, mDatabaseKDBX.finalKey!!, header.encryptionIV)
|
cipher = mDatabaseKDBX
|
||||||
|
.encryptionAlgorithm
|
||||||
|
.cipherEngine
|
||||||
|
.getCipher(Cipher.ENCRYPT_MODE, mDatabaseKDBX.finalKey!!, header.encryptionIV)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw DatabaseOutputException("Invalid algorithm.", e)
|
throw DatabaseOutputException("Invalid algorithm.", e)
|
||||||
}
|
}
|
||||||
@@ -293,7 +286,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
val random = super.setIVs(header)
|
val random = super.setIVs(header)
|
||||||
random.nextBytes(header.masterSeed)
|
random.nextBytes(header.masterSeed)
|
||||||
|
|
||||||
val ivLength = engine!!.ivLength()
|
val ivLength = mDatabaseKDBX.encryptionAlgorithm.cipherEngine.ivLength()
|
||||||
if (ivLength != header.encryptionIV.size) {
|
if (ivLength != header.encryptionIV.size) {
|
||||||
header.encryptionIV = ByteArray(ivLength)
|
header.encryptionIV = ByteArray(ivLength)
|
||||||
}
|
}
|
||||||
@@ -592,7 +585,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
||||||
|
|
||||||
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
|
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
|
||||||
writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime())
|
writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime)
|
||||||
|
|
||||||
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
|
||||||
}
|
}
|
||||||
@@ -618,7 +611,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
private fun writeDeletedObjects(value: List<DeletedObject>) {
|
private fun writeDeletedObjects(value: Collection<DeletedObject>) {
|
||||||
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObjects)
|
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObjects)
|
||||||
|
|
||||||
for (pdo in value) {
|
for (pdo in value) {
|
||||||
@@ -765,7 +758,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
|||||||
var character: Char
|
var character: Char
|
||||||
for (element in text) {
|
for (element in text) {
|
||||||
character = element
|
character = element
|
||||||
val hexChar = character.toInt()
|
val hexChar = character.code
|
||||||
if (
|
if (
|
||||||
hexChar in 0x20..0xD7FF ||
|
hexChar in 0x20..0xD7FF ||
|
||||||
hexChar == 0x9 ||
|
hexChar == 0x9 ||
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.database.merge
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.CustomData
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
|
import com.kunzisoft.keepass.utils.readAllBytes
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||||
|
|
||||||
|
var isRAMSufficient: (memoryWanted: Long) -> Boolean = {true}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge a KDB database in a KDBX database, by default all data are copied from the KDB
|
||||||
|
*/
|
||||||
|
fun merge(databaseToMerge: DatabaseKDB) {
|
||||||
|
// TODO Test KDB merge
|
||||||
|
val rootGroup = database.rootGroup
|
||||||
|
val rootGroupId = rootGroup?.nodeId
|
||||||
|
val rootGroupToMerge = databaseToMerge.rootGroup
|
||||||
|
val rootGroupIdToMerge = rootGroupToMerge?.nodeId
|
||||||
|
|
||||||
|
if (rootGroupId == null || rootGroupIdToMerge == null) {
|
||||||
|
throw IOException("Database is not open")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge children
|
||||||
|
rootGroupToMerge.doForEachChild(
|
||||||
|
object : NodeHandler<EntryKDB>() {
|
||||||
|
override fun operate(node: EntryKDB): Boolean {
|
||||||
|
mergeEntry(rootGroup.nodeId, node, databaseToMerge)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
object : NodeHandler<GroupKDB>() {
|
||||||
|
override fun operate(node: GroupKDB): Boolean {
|
||||||
|
mergeGroup(rootGroup.nodeId, node, databaseToMerge)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to transform KDB id nodes in KDBX id nodes
|
||||||
|
*/
|
||||||
|
private fun getNodeIdUUIDFrom(seed: NodeId<UUID>, intId: NodeId<Int>): NodeId<UUID> {
|
||||||
|
val seedUUID = seed.id
|
||||||
|
val idInt = intId.id
|
||||||
|
return NodeIdUUID(UUID(seedUUID.mostSignificantBits, seedUUID.leastSignificantBits + idInt))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to merge a KDB entry
|
||||||
|
*/
|
||||||
|
private fun mergeEntry(seed: NodeId<UUID>, nodeToMerge: EntryKDB, databaseToMerge: DatabaseKDB) {
|
||||||
|
val entryId: NodeId<UUID> = nodeToMerge.nodeId
|
||||||
|
val entry = database.getEntryById(entryId)
|
||||||
|
|
||||||
|
databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge ->
|
||||||
|
// Retrieve parent in current database
|
||||||
|
var parentEntryToMerge: GroupKDBX? = null
|
||||||
|
srcEntryToMerge.parent?.nodeId?.let {
|
||||||
|
val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
|
||||||
|
parentEntryToMerge = database.getGroupById(parentGroupIdToMerge)
|
||||||
|
}
|
||||||
|
val entryToMerge = EntryKDBX().apply {
|
||||||
|
this.nodeId = srcEntryToMerge.nodeId
|
||||||
|
this.icon = srcEntryToMerge.icon
|
||||||
|
this.creationTime = DateInstant(srcEntryToMerge.creationTime)
|
||||||
|
this.lastModificationTime = DateInstant(srcEntryToMerge.lastModificationTime)
|
||||||
|
this.lastAccessTime = DateInstant(srcEntryToMerge.lastAccessTime)
|
||||||
|
this.expiryTime = DateInstant(srcEntryToMerge.expiryTime)
|
||||||
|
this.expires = srcEntryToMerge.expires
|
||||||
|
this.title = srcEntryToMerge.title
|
||||||
|
this.username = srcEntryToMerge.username
|
||||||
|
this.password = srcEntryToMerge.password
|
||||||
|
this.url = srcEntryToMerge.url
|
||||||
|
this.notes = srcEntryToMerge.notes
|
||||||
|
// TODO attachment
|
||||||
|
}
|
||||||
|
if (entry != null) {
|
||||||
|
entry.updateWith(entryToMerge, false)
|
||||||
|
} else if (parentEntryToMerge != null) {
|
||||||
|
database.addEntryTo(entryToMerge, parentEntryToMerge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to merge a KDB group
|
||||||
|
*/
|
||||||
|
private fun mergeGroup(seed: NodeId<UUID>, nodeToMerge: GroupKDB, databaseToMerge: DatabaseKDB) {
|
||||||
|
val groupId: NodeId<Int> = nodeToMerge.nodeId
|
||||||
|
val group = database.getGroupById(getNodeIdUUIDFrom(seed, groupId))
|
||||||
|
|
||||||
|
databaseToMerge.getGroupById(groupId)?.let { srcGroupToMerge ->
|
||||||
|
// Retrieve parent in current database
|
||||||
|
var parentGroupToMerge: GroupKDBX? = null
|
||||||
|
srcGroupToMerge.parent?.nodeId?.let {
|
||||||
|
val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
|
||||||
|
parentGroupToMerge = database.getGroupById(parentGroupIdToMerge)
|
||||||
|
}
|
||||||
|
val groupToMerge = GroupKDBX().apply {
|
||||||
|
this.nodeId = getNodeIdUUIDFrom(seed, srcGroupToMerge.nodeId)
|
||||||
|
this.icon = srcGroupToMerge.icon
|
||||||
|
this.creationTime = DateInstant(srcGroupToMerge.creationTime)
|
||||||
|
this.lastModificationTime = DateInstant(srcGroupToMerge.lastModificationTime)
|
||||||
|
this.lastAccessTime = DateInstant(srcGroupToMerge.lastAccessTime)
|
||||||
|
this.expiryTime = DateInstant(srcGroupToMerge.expiryTime)
|
||||||
|
this.expires = srcGroupToMerge.expires
|
||||||
|
this.title = srcGroupToMerge.title
|
||||||
|
}
|
||||||
|
if (group != null) {
|
||||||
|
group.updateWith(groupToMerge, false)
|
||||||
|
} else if (parentGroupToMerge != null) {
|
||||||
|
database.addGroupTo(groupToMerge, parentGroupToMerge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge a KDB> database in a KDBX database,
|
||||||
|
* Try to take into account the modification date of each element
|
||||||
|
* To make a merge as accurate as possible
|
||||||
|
*/
|
||||||
|
fun merge(databaseToMerge: DatabaseKDBX) {
|
||||||
|
|
||||||
|
// Merge settings
|
||||||
|
if (database.nameChanged.date.before(databaseToMerge.nameChanged.date)) {
|
||||||
|
database.name = databaseToMerge.name
|
||||||
|
database.nameChanged = databaseToMerge.nameChanged
|
||||||
|
}
|
||||||
|
if (database.descriptionChanged.date.before(databaseToMerge.descriptionChanged.date)) {
|
||||||
|
database.description = databaseToMerge.description
|
||||||
|
database.descriptionChanged = databaseToMerge.descriptionChanged
|
||||||
|
}
|
||||||
|
if (database.defaultUserNameChanged.date.before(databaseToMerge.defaultUserNameChanged.date)) {
|
||||||
|
database.defaultUserName = databaseToMerge.defaultUserName
|
||||||
|
database.defaultUserNameChanged = databaseToMerge.defaultUserNameChanged
|
||||||
|
}
|
||||||
|
if (database.keyLastChanged.date.before(databaseToMerge.keyLastChanged.date)) {
|
||||||
|
database.keyChangeRecDays = databaseToMerge.keyChangeRecDays
|
||||||
|
database.keyChangeForceDays = databaseToMerge.keyChangeForceDays
|
||||||
|
database.isKeyChangeForceOnce = databaseToMerge.isKeyChangeForceOnce
|
||||||
|
database.keyLastChanged = databaseToMerge.keyLastChanged
|
||||||
|
}
|
||||||
|
if (database.recycleBinChanged.date.before(databaseToMerge.recycleBinChanged.date)) {
|
||||||
|
database.isRecycleBinEnabled = databaseToMerge.isRecycleBinEnabled
|
||||||
|
database.recycleBinUUID = databaseToMerge.recycleBinUUID
|
||||||
|
database.recycleBinChanged = databaseToMerge.recycleBinChanged
|
||||||
|
}
|
||||||
|
if (database.entryTemplatesGroupChanged.date.before(databaseToMerge.entryTemplatesGroupChanged.date)) {
|
||||||
|
database.entryTemplatesGroup = databaseToMerge.entryTemplatesGroup
|
||||||
|
database.entryTemplatesGroupChanged = databaseToMerge.entryTemplatesGroupChanged
|
||||||
|
}
|
||||||
|
if (database.settingsChanged.date.before(databaseToMerge.settingsChanged.date)) {
|
||||||
|
database.color = databaseToMerge.color
|
||||||
|
database.compressionAlgorithm = databaseToMerge.compressionAlgorithm
|
||||||
|
database.historyMaxItems = databaseToMerge.historyMaxItems
|
||||||
|
database.historyMaxSize = databaseToMerge.historyMaxSize
|
||||||
|
database.encryptionAlgorithm = databaseToMerge.encryptionAlgorithm
|
||||||
|
database.kdfEngine = databaseToMerge.kdfEngine
|
||||||
|
database.numberKeyEncryptionRounds = databaseToMerge.numberKeyEncryptionRounds
|
||||||
|
database.memoryUsage = databaseToMerge.memoryUsage
|
||||||
|
database.parallelism = databaseToMerge.parallelism
|
||||||
|
database.settingsChanged = databaseToMerge.settingsChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
val rootGroup = database.rootGroup
|
||||||
|
val rootGroupId = rootGroup?.nodeId
|
||||||
|
val rootGroupToMerge = databaseToMerge.rootGroup
|
||||||
|
val rootGroupIdToMerge = rootGroupToMerge?.nodeId
|
||||||
|
|
||||||
|
if (rootGroupId == null || rootGroupIdToMerge == null) {
|
||||||
|
throw IOException("Database is not open")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UUID of the root group to merge is unknown
|
||||||
|
if (database.getGroupById(rootGroupIdToMerge) == null) {
|
||||||
|
// Change it to copy children database root
|
||||||
|
databaseToMerge.removeGroupIndex(rootGroupToMerge)
|
||||||
|
rootGroupToMerge.nodeId = rootGroupId
|
||||||
|
databaseToMerge.addGroupIndex(rootGroupToMerge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge root group
|
||||||
|
if (rootGroup.lastModificationTime.date
|
||||||
|
.before(rootGroupToMerge.lastModificationTime.date)) {
|
||||||
|
rootGroup.updateWith(rootGroupToMerge, updateParents = false)
|
||||||
|
}
|
||||||
|
// Merge children
|
||||||
|
rootGroupToMerge.doForEachChild(
|
||||||
|
object : NodeHandler<EntryKDBX>() {
|
||||||
|
override fun operate(node: EntryKDBX): Boolean {
|
||||||
|
mergeEntry(node, databaseToMerge)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
object : NodeHandler<GroupKDBX>() {
|
||||||
|
override fun operate(node: GroupKDBX): Boolean {
|
||||||
|
mergeGroup(node, databaseToMerge)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Merge custom data in database header
|
||||||
|
mergeCustomData(database.customData, databaseToMerge.customData)
|
||||||
|
|
||||||
|
// Merge icons
|
||||||
|
databaseToMerge.iconsManager.doForEachCustomIcon { iconImageCustom, binaryData ->
|
||||||
|
val customIconUuid = iconImageCustom.uuid
|
||||||
|
// If custom icon not present, add it
|
||||||
|
val customIcon = database.iconsManager.getIcon(customIconUuid)
|
||||||
|
if (customIcon == null) {
|
||||||
|
database.addCustomIcon(
|
||||||
|
customIconUuid,
|
||||||
|
iconImageCustom.name,
|
||||||
|
iconImageCustom.lastModificationTime,
|
||||||
|
false
|
||||||
|
) { _, newBinaryData ->
|
||||||
|
binaryData.getInputDataStream(databaseToMerge.binaryCache).use { inputStream ->
|
||||||
|
newBinaryData?.getOutputDataStream(database.binaryCache).use { outputStream ->
|
||||||
|
inputStream.readAllBytes { buffer ->
|
||||||
|
outputStream?.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val customIconModification = customIcon.lastModificationTime
|
||||||
|
val customIconToMerge = databaseToMerge.iconsManager.getIcon(customIconUuid)
|
||||||
|
val customIconModificationToMerge = customIconToMerge?.lastModificationTime
|
||||||
|
if (customIconModification != null && customIconModificationToMerge != null) {
|
||||||
|
if (customIconModification.date.before(customIconModificationToMerge.date)) {
|
||||||
|
customIcon.updateWith(customIconToMerge)
|
||||||
|
}
|
||||||
|
} else if (customIconModificationToMerge != null) {
|
||||||
|
customIcon.updateWith(customIconToMerge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage deleted objects
|
||||||
|
databaseToMerge.deletedObjects.forEach { deletedObject ->
|
||||||
|
val deletedObjectId = deletedObject.uuid
|
||||||
|
val databaseEntry = database.getEntryById(deletedObjectId)
|
||||||
|
val databaseGroup = database.getGroupById(deletedObjectId)
|
||||||
|
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
|
||||||
|
val databaseIconModificationTime = databaseIcon?.lastModificationTime
|
||||||
|
if (databaseEntry != null
|
||||||
|
&& deletedObject.deletionTime.date
|
||||||
|
.after(databaseEntry.lastModificationTime.date)) {
|
||||||
|
database.removeEntryFrom(databaseEntry, databaseEntry.parent)
|
||||||
|
}
|
||||||
|
if (databaseGroup != null
|
||||||
|
&& deletedObject.deletionTime.date
|
||||||
|
.after(databaseGroup.lastModificationTime.date)) {
|
||||||
|
database.removeGroupFrom(databaseGroup, databaseGroup.parent)
|
||||||
|
}
|
||||||
|
if (databaseIcon != null
|
||||||
|
&& (
|
||||||
|
databaseIconModificationTime == null
|
||||||
|
|| (deletedObject.deletionTime.date.after(databaseIconModificationTime.date))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
database.removeCustomIcon(deletedObjectId)
|
||||||
|
}
|
||||||
|
// Attachments are removed and optimized during the database save
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge [customDataToMerge] in [customData]
|
||||||
|
*/
|
||||||
|
private fun mergeCustomData(customData: CustomData, customDataToMerge: CustomData) {
|
||||||
|
customDataToMerge.doForEachItems { customDataItemToMerge ->
|
||||||
|
val customDataItem = customData.get(customDataItemToMerge.key)
|
||||||
|
if (customDataItem == null) {
|
||||||
|
customData.put(customDataItemToMerge)
|
||||||
|
} else {
|
||||||
|
val customDataItemModification = customDataItem.lastModificationTime
|
||||||
|
val customDataItemToMergeModification = customDataItemToMerge.lastModificationTime
|
||||||
|
if (customDataItemModification != null && customDataItemToMergeModification != null) {
|
||||||
|
if (customDataItemModification.date
|
||||||
|
.before(customDataItemToMergeModification.date)) {
|
||||||
|
customData.put(customDataItemToMerge)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
customData.put(customDataItemToMerge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to merge a KDBX entry
|
||||||
|
*/
|
||||||
|
private fun mergeEntry(nodeToMerge: EntryKDBX, databaseToMerge: DatabaseKDBX) {
|
||||||
|
val entryId = nodeToMerge.nodeId
|
||||||
|
val entry = database.getEntryById(entryId)
|
||||||
|
val deletedObject = database.getDeletedObject(entryId)
|
||||||
|
|
||||||
|
databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge ->
|
||||||
|
// Retrieve parent in current database
|
||||||
|
var parentEntryToMerge: GroupKDBX? = null
|
||||||
|
srcEntryToMerge.parent?.nodeId?.let {
|
||||||
|
parentEntryToMerge = database.getGroupById(it)
|
||||||
|
}
|
||||||
|
val entryToMerge = EntryKDBX().apply {
|
||||||
|
updateWith(srcEntryToMerge, copyHistory = true, updateParents = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy attachments in main pool
|
||||||
|
val newAttachments = mutableListOf<Attachment>()
|
||||||
|
entryToMerge.getAttachments(databaseToMerge.attachmentPool).forEach { attachment ->
|
||||||
|
val binarySize = attachment.binaryData.getSize()
|
||||||
|
val binaryData = database.buildNewBinaryAttachment(
|
||||||
|
isRAMSufficient.invoke(binarySize),
|
||||||
|
attachment.binaryData.isCompressed,
|
||||||
|
attachment.binaryData.isProtected
|
||||||
|
)
|
||||||
|
attachment.binaryData.getInputDataStream(databaseToMerge.binaryCache).use { inputStream ->
|
||||||
|
binaryData.getOutputDataStream(database.binaryCache).use { outputStream ->
|
||||||
|
inputStream.readAllBytes { buffer ->
|
||||||
|
outputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newAttachments.add(Attachment(attachment.name, binaryData))
|
||||||
|
}
|
||||||
|
entryToMerge.removeAttachments()
|
||||||
|
newAttachments.forEach { newAttachment ->
|
||||||
|
entryToMerge.putAttachment(newAttachment, database.attachmentPool)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry == null) {
|
||||||
|
// If it's a deleted object, but another instance was updated
|
||||||
|
// If entry parent to add exists and in current database
|
||||||
|
if ((deletedObject == null
|
||||||
|
|| deletedObject.deletionTime.date
|
||||||
|
.before(entryToMerge.lastModificationTime.date))
|
||||||
|
&& parentEntryToMerge != null) {
|
||||||
|
database.addEntryTo(entryToMerge, parentEntryToMerge)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Merge independently custom data
|
||||||
|
mergeCustomData(entry.customData, entryToMerge.customData)
|
||||||
|
// Merge by modification time
|
||||||
|
if (entry.lastModificationTime.date
|
||||||
|
.before(entryToMerge.lastModificationTime.date)
|
||||||
|
) {
|
||||||
|
addHistory(entry, entryToMerge)
|
||||||
|
if (parentEntryToMerge == entry.parent) {
|
||||||
|
entry.updateWith(entryToMerge, copyHistory = true, updateParents = false)
|
||||||
|
} else {
|
||||||
|
// Update entry with databaseEntryToMerge and merge history
|
||||||
|
database.removeEntryFrom(entry, entry.parent)
|
||||||
|
if (parentEntryToMerge != null) {
|
||||||
|
database.addEntryTo(entryToMerge, parentEntryToMerge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (entry.lastModificationTime.date
|
||||||
|
.after(entryToMerge.lastModificationTime.date)
|
||||||
|
) {
|
||||||
|
addHistory(entryToMerge, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to merge an history from an [entryA] to an [entryB],
|
||||||
|
* [entryB] is modified
|
||||||
|
*/
|
||||||
|
private fun addHistory(entryA: EntryKDBX, entryB: EntryKDBX) {
|
||||||
|
// Keep entry as history if already not present
|
||||||
|
entryA.history.forEach { history ->
|
||||||
|
// If history not present
|
||||||
|
if (!entryB.history.any {
|
||||||
|
it.lastModificationTime == history.lastModificationTime
|
||||||
|
}) {
|
||||||
|
entryB.addEntryToHistory(history)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Last entry not present
|
||||||
|
if (entryB.history.find {
|
||||||
|
it.lastModificationTime == entryA.lastModificationTime
|
||||||
|
} == null) {
|
||||||
|
val history = EntryKDBX().apply {
|
||||||
|
updateWith(entryA, copyHistory = false, updateParents = false)
|
||||||
|
parent = null
|
||||||
|
}
|
||||||
|
entryB.addEntryToHistory(history)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to merge a KDBX group
|
||||||
|
*/
|
||||||
|
private fun mergeGroup(nodeToMerge: GroupKDBX, databaseToMerge: DatabaseKDBX) {
|
||||||
|
val groupId = nodeToMerge.nodeId
|
||||||
|
val group = database.getGroupById(groupId)
|
||||||
|
val deletedObject = database.getDeletedObject(groupId)
|
||||||
|
|
||||||
|
databaseToMerge.getGroupById(groupId)?.let { srcGroupToMerge ->
|
||||||
|
// Retrieve parent in current database
|
||||||
|
var parentGroupToMerge: GroupKDBX? = null
|
||||||
|
srcGroupToMerge.parent?.nodeId?.let {
|
||||||
|
parentGroupToMerge = database.getGroupById(it)
|
||||||
|
}
|
||||||
|
val groupToMerge = GroupKDBX().apply {
|
||||||
|
updateWith(srcGroupToMerge, updateParents = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group == null) {
|
||||||
|
// If group parent to add exists and in current database
|
||||||
|
if ((deletedObject == null
|
||||||
|
|| deletedObject.deletionTime.date
|
||||||
|
.before(groupToMerge.lastModificationTime.date))
|
||||||
|
&& parentGroupToMerge != null) {
|
||||||
|
database.addGroupTo(groupToMerge, parentGroupToMerge)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Merge independently custom data
|
||||||
|
mergeCustomData(group.customData, groupToMerge.customData)
|
||||||
|
// Merge by modification time
|
||||||
|
if (group.lastModificationTime.date
|
||||||
|
.before(groupToMerge.lastModificationTime.date)
|
||||||
|
) {
|
||||||
|
if (parentGroupToMerge == group.parent) {
|
||||||
|
group.updateWith(groupToMerge, false)
|
||||||
|
} else {
|
||||||
|
database.removeGroupFrom(group, group.parent)
|
||||||
|
if (parentGroupToMerge != null) {
|
||||||
|
database.addGroupTo(groupToMerge, parentGroupToMerge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,7 +119,7 @@ class GroupActivityEducation(activity: Activity)
|
|||||||
.outerCircleColorInt(getCircleColor())
|
.outerCircleColorInt(getCircleColor())
|
||||||
.outerCircleAlpha(getCircleAlpha())
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
.textColorInt(getTextColor())
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(true)
|
.tintTarget(false)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
override fun onTargetClick(view: TapTargetView) {
|
override fun onTargetClick(view: TapTargetView) {
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ class PasswordActivityEducation(activity: Activity)
|
|||||||
activity.getString(R.string.education_unlock_summary))
|
activity.getString(R.string.education_unlock_summary))
|
||||||
.outerCircleColorInt(getCircleColor())
|
.outerCircleColorInt(getCircleColor())
|
||||||
.outerCircleAlpha(getCircleAlpha())
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
.icon(ContextCompat.getDrawable(activity, R.mipmap.ic_launcher_round))
|
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_lock_open_white_24dp))
|
||||||
.textColorInt(getTextColor())
|
.textColorInt(getTextColor())
|
||||||
.tintTarget(false)
|
.tintTarget(true)
|
||||||
.cancelable(true),
|
.cancelable(true),
|
||||||
object : TapTargetView.Listener() {
|
object : TapTargetView.Listener() {
|
||||||
override fun onTargetClick(view: TapTargetView) {
|
override fun onTargetClick(view: TapTargetView) {
|
||||||
|
|||||||
@@ -0,0 +1,902 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2008-2009 Google Inc. 2022 J-Jamet
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kunzisoft.keepass.magikeyboard;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.content.res.XmlResourceParser;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.util.Xml;
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.R;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
|
||||||
|
* consists of rows of keys.
|
||||||
|
* <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
|
||||||
|
* <pre>
|
||||||
|
* <Keyboard
|
||||||
|
* android:keyWidth="%10p"
|
||||||
|
* android:keyHeight="50px"
|
||||||
|
* android:horizontalGap="2px"
|
||||||
|
* android:verticalGap="2px" >
|
||||||
|
* <Row android:keyWidth="32px" >
|
||||||
|
* <Key android:keyLabel="A" />
|
||||||
|
* ...
|
||||||
|
* </Row>
|
||||||
|
* ...
|
||||||
|
* </Keyboard>
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* ref android.R.styleable#Keyboard_keyWidth
|
||||||
|
* ref android.R.styleable#Keyboard_keyHeight
|
||||||
|
* ref android.R.styleable#Keyboard_horizontalGap
|
||||||
|
* ref android.R.styleable#Keyboard_verticalGap
|
||||||
|
*/
|
||||||
|
public class Keyboard {
|
||||||
|
|
||||||
|
static final String TAG = "Keyboard";
|
||||||
|
|
||||||
|
// Keyboard XML Tags
|
||||||
|
private static final String TAG_KEYBOARD = "Keyboard";
|
||||||
|
private static final String TAG_ROW = "Row";
|
||||||
|
private static final String TAG_KEY = "Key";
|
||||||
|
|
||||||
|
public static final int EDGE_LEFT = 0x01;
|
||||||
|
public static final int EDGE_RIGHT = 0x02;
|
||||||
|
public static final int EDGE_TOP = 0x04;
|
||||||
|
public static final int EDGE_BOTTOM = 0x08;
|
||||||
|
|
||||||
|
public static final int KEYCODE_SHIFT = -1;
|
||||||
|
public static final int KEYCODE_MODE_CHANGE = -2;
|
||||||
|
public static final int KEYCODE_CANCEL = -3;
|
||||||
|
public static final int KEYCODE_DONE = -4;
|
||||||
|
public static final int KEYCODE_DELETE = -5;
|
||||||
|
public static final int KEYCODE_ALT = -6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Horizontal gap default for all rows
|
||||||
|
*/
|
||||||
|
private int mDefaultHorizontalGap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default key width
|
||||||
|
*/
|
||||||
|
private int mDefaultWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default key height
|
||||||
|
*/
|
||||||
|
private int mDefaultHeight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default gap between rows
|
||||||
|
*/
|
||||||
|
private int mDefaultVerticalGap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the keyboard in the shifted state
|
||||||
|
*/
|
||||||
|
private boolean mShifted;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key instance for the shift key, if present
|
||||||
|
*/
|
||||||
|
private Key[] mShiftKeys = {null, null};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key index for the shift key, if present
|
||||||
|
*/
|
||||||
|
private int[] mShiftKeyIndices = {-1, -1};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total height of the keyboard, including the padding and keys
|
||||||
|
*/
|
||||||
|
private int mTotalHeight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total width of the keyboard, including left side gaps and keys, but not any gaps on the
|
||||||
|
* right side.
|
||||||
|
*/
|
||||||
|
private int mTotalWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of keys in this keyboard
|
||||||
|
*/
|
||||||
|
private List<Key> mKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of modifier keys such as Shift & Alt, if any
|
||||||
|
*/
|
||||||
|
private List<Key> mModifierKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Width of the screen available to fit the keyboard
|
||||||
|
*/
|
||||||
|
private int mDisplayWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Height of the screen
|
||||||
|
*/
|
||||||
|
private int mDisplayHeight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keyboard mode, or zero, if none.
|
||||||
|
*/
|
||||||
|
private int mKeyboardMode;
|
||||||
|
|
||||||
|
// Variables for pre-computing nearest keys.
|
||||||
|
|
||||||
|
private static final int GRID_WIDTH = 10;
|
||||||
|
private static final int GRID_HEIGHT = 5;
|
||||||
|
private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
|
||||||
|
private int mCellWidth;
|
||||||
|
private int mCellHeight;
|
||||||
|
private int[][] mGridNeighbors;
|
||||||
|
private int mProximityThreshold;
|
||||||
|
/**
|
||||||
|
* Number of key widths from current touch point to search for nearest keys.
|
||||||
|
*/
|
||||||
|
private static float SEARCH_DISTANCE = 1.8f;
|
||||||
|
|
||||||
|
private ArrayList<Row> rows = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
|
||||||
|
* Some of the key size defaults can be overridden per row from what the {@link Keyboard}
|
||||||
|
* defines.
|
||||||
|
*
|
||||||
|
* ref R.styleable#Keyboard_keyWidth
|
||||||
|
* ref R.styleable#Keyboard_keyHeight
|
||||||
|
* ref R.styleable#Keyboard_horizontalGap
|
||||||
|
* ref R.styleable#Keyboard_verticalGap
|
||||||
|
* ref R.styleable#Keyboard_Row_rowEdgeFlags
|
||||||
|
* ref R.styleable#Keyboard_Row_keyboardMode
|
||||||
|
*/
|
||||||
|
public static class Row {
|
||||||
|
/**
|
||||||
|
* Default width of a key in this row.
|
||||||
|
*/
|
||||||
|
public int defaultWidth;
|
||||||
|
/**
|
||||||
|
* Default height of a key in this row.
|
||||||
|
*/
|
||||||
|
public int defaultHeight;
|
||||||
|
/**
|
||||||
|
* Default horizontal gap between keys in this row.
|
||||||
|
*/
|
||||||
|
public int defaultHorizontalGap;
|
||||||
|
/**
|
||||||
|
* Vertical gap following this row.
|
||||||
|
*/
|
||||||
|
public int verticalGap;
|
||||||
|
|
||||||
|
ArrayList<Key> mKeys = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edge flags for this row of keys. Possible values that can be assigned are
|
||||||
|
* {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM}
|
||||||
|
*/
|
||||||
|
public int rowEdgeFlags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The keyboard mode for this row
|
||||||
|
*/
|
||||||
|
public int mode;
|
||||||
|
|
||||||
|
private Keyboard parent;
|
||||||
|
|
||||||
|
public Row(Keyboard parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Row(Resources res, Keyboard parent, XmlResourceParser parser) {
|
||||||
|
this.parent = parent;
|
||||||
|
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
|
||||||
|
R.styleable.Keyboard);
|
||||||
|
defaultWidth = getDimensionOrFraction(a, R.styleable.Keyboard_keyWidth,
|
||||||
|
parent.mDisplayWidth, parent.mDefaultWidth);
|
||||||
|
defaultHeight = getDimensionOrFraction(a, R.styleable.Keyboard_keyHeight,
|
||||||
|
parent.mDisplayHeight, parent.mDefaultHeight);
|
||||||
|
defaultHorizontalGap = getDimensionOrFraction(a, R.styleable.Keyboard_horizontalGap,
|
||||||
|
parent.mDisplayWidth, parent.mDefaultHorizontalGap);
|
||||||
|
verticalGap = getDimensionOrFraction(a, R.styleable.Keyboard_verticalGap,
|
||||||
|
parent.mDisplayHeight, parent.mDefaultVerticalGap);
|
||||||
|
a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard_Row);
|
||||||
|
rowEdgeFlags = a.getInt(R.styleable.Keyboard_Row_rowEdgeFlags, 0);
|
||||||
|
mode = a.getResourceId(R.styleable.Keyboard_Row_keyboardMode, 0);
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for describing the position and characteristics of a single key in the keyboard.
|
||||||
|
*
|
||||||
|
* ref R.styleable#Keyboard_keyWidth
|
||||||
|
* ref R.styleable#Keyboard_keyHeight
|
||||||
|
* ref R.styleable#Keyboard_horizontalGap
|
||||||
|
* ref R.styleable#Keyboard_Key_codes
|
||||||
|
* ref R.styleable#Keyboard_Key_keyIcon
|
||||||
|
* ref R.styleable#Keyboard_Key_keyLabel
|
||||||
|
* ref R.styleable#Keyboard_Key_iconPreview
|
||||||
|
* ref R.styleable#Keyboard_Key_isSticky
|
||||||
|
* ref R.styleable#Keyboard_Key_isRepeatable
|
||||||
|
* ref R.styleable#Keyboard_Key_isModifier
|
||||||
|
* ref R.styleable#Keyboard_Key_popupKeyboard
|
||||||
|
* ref R.styleable#Keyboard_Key_popupCharacters
|
||||||
|
* ref R.styleable#Keyboard_Key_keyOutputText
|
||||||
|
* ref R.styleable#Keyboard_Key_keyEdgeFlags
|
||||||
|
*/
|
||||||
|
public static class Key {
|
||||||
|
/**
|
||||||
|
* All the key codes (unicode or custom code) that this key could generate, zero'th
|
||||||
|
* being the most important.
|
||||||
|
*/
|
||||||
|
public int[] codes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label to display
|
||||||
|
*/
|
||||||
|
public CharSequence label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon to display instead of a label. Icon takes precedence over a label
|
||||||
|
*/
|
||||||
|
public Drawable icon;
|
||||||
|
/**
|
||||||
|
* Preview version of the icon, for the preview popup
|
||||||
|
*/
|
||||||
|
public Drawable iconPreview;
|
||||||
|
/**
|
||||||
|
* Width of the key, not including the gap
|
||||||
|
*/
|
||||||
|
public int width;
|
||||||
|
/**
|
||||||
|
* Height of the key, not including the gap
|
||||||
|
*/
|
||||||
|
public int height;
|
||||||
|
/**
|
||||||
|
* The horizontal gap before this key
|
||||||
|
*/
|
||||||
|
public int gap;
|
||||||
|
/**
|
||||||
|
* Whether this key is sticky, i.e., a toggle key
|
||||||
|
*/
|
||||||
|
public boolean sticky;
|
||||||
|
/**
|
||||||
|
* X coordinate of the key in the keyboard layout
|
||||||
|
*/
|
||||||
|
public int x;
|
||||||
|
/**
|
||||||
|
* Y coordinate of the key in the keyboard layout
|
||||||
|
*/
|
||||||
|
public int y;
|
||||||
|
/**
|
||||||
|
* The current pressed state of this key
|
||||||
|
*/
|
||||||
|
public boolean pressed;
|
||||||
|
/**
|
||||||
|
* If this is a sticky key, is it on?
|
||||||
|
*/
|
||||||
|
public boolean on;
|
||||||
|
/**
|
||||||
|
* Text to output when pressed. This can be multiple characters, like ".com"
|
||||||
|
*/
|
||||||
|
public CharSequence text;
|
||||||
|
/**
|
||||||
|
* Popup characters
|
||||||
|
*/
|
||||||
|
public CharSequence popupCharacters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flags that specify the anchoring to edges of the keyboard for detecting touch events
|
||||||
|
* that are just out of the boundary of the key. This is a bit mask of
|
||||||
|
* {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, {@link Keyboard#EDGE_TOP} and
|
||||||
|
* {@link Keyboard#EDGE_BOTTOM}.
|
||||||
|
*/
|
||||||
|
public int edgeFlags;
|
||||||
|
/**
|
||||||
|
* Whether this is a modifier key, such as Shift or Alt
|
||||||
|
*/
|
||||||
|
public boolean modifier;
|
||||||
|
/**
|
||||||
|
* The keyboard that this key belongs to
|
||||||
|
*/
|
||||||
|
private Keyboard keyboard;
|
||||||
|
/**
|
||||||
|
* If this key pops up a mini keyboard, this is the resource id for the XML layout for that
|
||||||
|
* keyboard.
|
||||||
|
*/
|
||||||
|
public int popupResId;
|
||||||
|
/**
|
||||||
|
* Whether this key repeats itself when held down
|
||||||
|
*/
|
||||||
|
public boolean repeatable;
|
||||||
|
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_NORMAL_ON = {
|
||||||
|
android.R.attr.state_checkable,
|
||||||
|
android.R.attr.state_checked
|
||||||
|
};
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_PRESSED_ON = {
|
||||||
|
android.R.attr.state_pressed,
|
||||||
|
android.R.attr.state_checkable,
|
||||||
|
android.R.attr.state_checked
|
||||||
|
};
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_NORMAL_OFF = {
|
||||||
|
android.R.attr.state_checkable
|
||||||
|
};
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_PRESSED_OFF = {
|
||||||
|
android.R.attr.state_pressed,
|
||||||
|
android.R.attr.state_checkable
|
||||||
|
};
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_NORMAL = {
|
||||||
|
};
|
||||||
|
|
||||||
|
private final static int[] KEY_STATE_PRESSED = {
|
||||||
|
android.R.attr.state_pressed
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an empty key with no attributes.
|
||||||
|
*/
|
||||||
|
public Key(Row parent) {
|
||||||
|
keyboard = parent.parent;
|
||||||
|
height = parent.defaultHeight;
|
||||||
|
width = parent.defaultWidth;
|
||||||
|
gap = parent.defaultHorizontalGap;
|
||||||
|
edgeFlags = parent.rowEdgeFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a key with the given top-left coordinate and extract its attributes from
|
||||||
|
* the XML parser.
|
||||||
|
*
|
||||||
|
* @param res resources associated with the caller's context
|
||||||
|
* @param parent the row that this key belongs to. The row must already be attached to
|
||||||
|
* a {@link Keyboard}.
|
||||||
|
* @param x the x coordinate of the top-left
|
||||||
|
* @param y the y coordinate of the top-left
|
||||||
|
* @param parser the XML parser containing the attributes for this key
|
||||||
|
*/
|
||||||
|
public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) {
|
||||||
|
this(parent);
|
||||||
|
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
|
||||||
|
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard);
|
||||||
|
|
||||||
|
width = getDimensionOrFraction(a, R.styleable.Keyboard_keyWidth,
|
||||||
|
keyboard.mDisplayWidth, parent.defaultWidth);
|
||||||
|
height = getDimensionOrFraction(a, R.styleable.Keyboard_keyHeight,
|
||||||
|
keyboard.mDisplayHeight, parent.defaultHeight);
|
||||||
|
gap = getDimensionOrFraction(a, R.styleable.Keyboard_horizontalGap,
|
||||||
|
keyboard.mDisplayWidth, parent.defaultHorizontalGap);
|
||||||
|
a.recycle();
|
||||||
|
a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
|
||||||
|
this.x += gap;
|
||||||
|
TypedValue codesValue = new TypedValue();
|
||||||
|
a.getValue(R.styleable.Keyboard_Key_codes, codesValue);
|
||||||
|
if (codesValue.type == TypedValue.TYPE_INT_DEC
|
||||||
|
|| codesValue.type == TypedValue.TYPE_INT_HEX) {
|
||||||
|
codes = new int[]{codesValue.data};
|
||||||
|
} else if (codesValue.type == TypedValue.TYPE_STRING) {
|
||||||
|
codes = parseCSV(codesValue.string.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
iconPreview = a.getDrawable(R.styleable.Keyboard_Key_iconPreview);
|
||||||
|
if (iconPreview != null) {
|
||||||
|
iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(),
|
||||||
|
iconPreview.getIntrinsicHeight());
|
||||||
|
}
|
||||||
|
popupCharacters = a.getText(R.styleable.Keyboard_Key_popupCharacters);
|
||||||
|
popupResId = a.getResourceId(R.styleable.Keyboard_Key_popupKeyboard, 0);
|
||||||
|
repeatable = a.getBoolean(R.styleable.Keyboard_Key_isRepeatable, false);
|
||||||
|
modifier = a.getBoolean(R.styleable.Keyboard_Key_isModifier, false);
|
||||||
|
sticky = a.getBoolean(R.styleable.Keyboard_Key_isSticky, false);
|
||||||
|
edgeFlags = a.getInt(R.styleable.Keyboard_Key_keyEdgeFlags, 0);
|
||||||
|
edgeFlags |= parent.rowEdgeFlags;
|
||||||
|
|
||||||
|
icon = a.getDrawable(R.styleable.Keyboard_Key_keyIcon);
|
||||||
|
if (icon != null) {
|
||||||
|
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
|
||||||
|
}
|
||||||
|
label = a.getText(R.styleable.Keyboard_Key_keyLabel);
|
||||||
|
text = a.getText(R.styleable.Keyboard_Key_keyOutputText);
|
||||||
|
|
||||||
|
if (codes == null && !TextUtils.isEmpty(label)) {
|
||||||
|
codes = new int[]{label.charAt(0)};
|
||||||
|
}
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Informs the key that it has been pressed, in case it needs to change its appearance or
|
||||||
|
* state.
|
||||||
|
*
|
||||||
|
* @see #onReleased(boolean)
|
||||||
|
*/
|
||||||
|
public void onPressed() {
|
||||||
|
pressed = !pressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the pressed state of the key.
|
||||||
|
*
|
||||||
|
* <p>Toggled state of the key will be flipped when all the following conditions are
|
||||||
|
* fulfilled:</p>
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>This is a sticky key, that is, {@link #sticky} is {@code true}.
|
||||||
|
* <li>The parameter {@code inside} is {@code true}.
|
||||||
|
* <li>{@link android.os.Build.VERSION#SDK_INT} is greater than
|
||||||
|
* {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param inside whether the finger was released inside the key. Works only on Android M and
|
||||||
|
* later. See the method document for details.
|
||||||
|
* @see #onPressed()
|
||||||
|
*/
|
||||||
|
public void onReleased(boolean inside) {
|
||||||
|
pressed = !pressed;
|
||||||
|
if (sticky && inside) {
|
||||||
|
on = !on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] parseCSV(String value) {
|
||||||
|
int count = 0;
|
||||||
|
int lastIndex = 0;
|
||||||
|
if (value.length() > 0) {
|
||||||
|
count++;
|
||||||
|
while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int[] values = new int[count];
|
||||||
|
count = 0;
|
||||||
|
StringTokenizer st = new StringTokenizer(value, ",");
|
||||||
|
while (st.hasMoreTokens()) {
|
||||||
|
try {
|
||||||
|
values[count++] = Integer.parseInt(st.nextToken());
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
Log.e(TAG, "Error parsing keycodes " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects if a point falls inside this key.
|
||||||
|
*
|
||||||
|
* @param x the x-coordinate of the point
|
||||||
|
* @param y the y-coordinate of the point
|
||||||
|
* @return whether or not the point falls inside the key. If the key is attached to an edge,
|
||||||
|
* it will assume that all points between the key and the edge are considered to be inside
|
||||||
|
* the key.
|
||||||
|
*/
|
||||||
|
public boolean isInside(int x, int y) {
|
||||||
|
boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0;
|
||||||
|
boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0;
|
||||||
|
boolean topEdge = (edgeFlags & EDGE_TOP) > 0;
|
||||||
|
boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0;
|
||||||
|
return (x >= this.x || (leftEdge && x <= this.x + this.width))
|
||||||
|
&& (x < this.x + this.width || (rightEdge && x >= this.x))
|
||||||
|
&& (y >= this.y || (topEdge && y <= this.y + this.height))
|
||||||
|
&& (y < this.y + this.height || (bottomEdge && y >= this.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the square of the distance between the center of the key and the given point.
|
||||||
|
*
|
||||||
|
* @param x the x-coordinate of the point
|
||||||
|
* @param y the y-coordinate of the point
|
||||||
|
* @return the square of the distance of the point from the center of the key
|
||||||
|
*/
|
||||||
|
public int squaredDistanceFrom(int x, int y) {
|
||||||
|
int xDist = this.x + width / 2 - x;
|
||||||
|
int yDist = this.y + height / 2 - y;
|
||||||
|
return xDist * xDist + yDist * yDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the drawable state for the key, based on the current state and type of the key.
|
||||||
|
*
|
||||||
|
* @return the drawable state of the key.
|
||||||
|
* @see android.graphics.drawable.StateListDrawable#setState(int[])
|
||||||
|
*/
|
||||||
|
public int[] getCurrentDrawableState() {
|
||||||
|
int[] states = KEY_STATE_NORMAL;
|
||||||
|
|
||||||
|
if (on) {
|
||||||
|
if (pressed) {
|
||||||
|
states = KEY_STATE_PRESSED_ON;
|
||||||
|
} else {
|
||||||
|
states = KEY_STATE_NORMAL_ON;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sticky) {
|
||||||
|
if (pressed) {
|
||||||
|
states = KEY_STATE_PRESSED_OFF;
|
||||||
|
} else {
|
||||||
|
states = KEY_STATE_NORMAL_OFF;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pressed) {
|
||||||
|
states = KEY_STATE_PRESSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return states;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a keyboard from the given xml key layout file.
|
||||||
|
*
|
||||||
|
* @param context the application or service context
|
||||||
|
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||||
|
*/
|
||||||
|
public Keyboard(Context context, int xmlLayoutResId) {
|
||||||
|
this(context, xmlLayoutResId, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a keyboard from the given xml key layout file. Weeds out rows
|
||||||
|
* that have a keyboard mode defined but don't match the specified mode.
|
||||||
|
*
|
||||||
|
* @param context the application or service context
|
||||||
|
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||||
|
* @param modeId keyboard mode identifier
|
||||||
|
* @param width sets width of keyboard
|
||||||
|
* @param height sets height of keyboard
|
||||||
|
*/
|
||||||
|
public Keyboard(Context context, int xmlLayoutResId, int modeId, int width, int height) {
|
||||||
|
mDisplayWidth = width;
|
||||||
|
mDisplayHeight = height;
|
||||||
|
|
||||||
|
mDefaultHorizontalGap = 0;
|
||||||
|
mDefaultWidth = mDisplayWidth / 10;
|
||||||
|
mDefaultVerticalGap = 0;
|
||||||
|
mDefaultHeight = mDefaultWidth;
|
||||||
|
mKeys = new ArrayList<>();
|
||||||
|
mModifierKeys = new ArrayList<>();
|
||||||
|
mKeyboardMode = modeId;
|
||||||
|
loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a keyboard from the given xml key layout file. Weeds out rows
|
||||||
|
* that have a keyboard mode defined but don't match the specified mode.
|
||||||
|
*
|
||||||
|
* @param context the application or service context
|
||||||
|
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||||
|
* @param modeId keyboard mode identifier
|
||||||
|
*/
|
||||||
|
public Keyboard(Context context, int xmlLayoutResId, int modeId) {
|
||||||
|
DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
||||||
|
mDisplayWidth = dm.widthPixels;
|
||||||
|
mDisplayHeight = dm.heightPixels;
|
||||||
|
//Log.v(TAG, "keyboard's display metrics:" + dm);
|
||||||
|
|
||||||
|
mDefaultHorizontalGap = 0;
|
||||||
|
mDefaultWidth = mDisplayWidth / 10;
|
||||||
|
mDefaultVerticalGap = 0;
|
||||||
|
mDefaultHeight = mDefaultWidth;
|
||||||
|
mKeys = new ArrayList<>();
|
||||||
|
mModifierKeys = new ArrayList<>();
|
||||||
|
mKeyboardMode = modeId;
|
||||||
|
loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Creates a blank keyboard from the given resource file and populates it with the specified
|
||||||
|
* characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
|
||||||
|
* </p>
|
||||||
|
* <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
|
||||||
|
* possible in each row.</p>
|
||||||
|
*
|
||||||
|
* @param context the application or service context
|
||||||
|
* @param layoutTemplateResId the layout template file, containing no keys.
|
||||||
|
* @param characters the list of characters to display on the keyboard. One key will be created
|
||||||
|
* for each character.
|
||||||
|
* @param columns the number of columns of keys to display. If this number is greater than the
|
||||||
|
* number of keys that can fit in a row, it will be ignored. If this number is -1, the
|
||||||
|
* keyboard will fit as many keys as possible in each row.
|
||||||
|
*/
|
||||||
|
public Keyboard(Context context, int layoutTemplateResId,
|
||||||
|
CharSequence characters, int columns, int horizontalPadding) {
|
||||||
|
this(context, layoutTemplateResId);
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int column = 0;
|
||||||
|
mTotalWidth = 0;
|
||||||
|
|
||||||
|
Row row = new Row(this);
|
||||||
|
row.defaultHeight = mDefaultHeight;
|
||||||
|
row.defaultWidth = mDefaultWidth;
|
||||||
|
row.defaultHorizontalGap = mDefaultHorizontalGap;
|
||||||
|
row.verticalGap = mDefaultVerticalGap;
|
||||||
|
row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM;
|
||||||
|
final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
|
||||||
|
for (int i = 0; i < characters.length(); i++) {
|
||||||
|
char c = characters.charAt(i);
|
||||||
|
if (column >= maxColumns
|
||||||
|
|| x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
|
||||||
|
x = 0;
|
||||||
|
y += mDefaultVerticalGap + mDefaultHeight;
|
||||||
|
column = 0;
|
||||||
|
}
|
||||||
|
final Key key = new Key(row);
|
||||||
|
key.x = x;
|
||||||
|
key.y = y;
|
||||||
|
key.label = String.valueOf(c);
|
||||||
|
key.codes = new int[]{c};
|
||||||
|
column++;
|
||||||
|
x += key.width + key.gap;
|
||||||
|
mKeys.add(key);
|
||||||
|
row.mKeys.add(key);
|
||||||
|
if (x > mTotalWidth) {
|
||||||
|
mTotalWidth = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mTotalHeight = y + mDefaultHeight;
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
final void resize(int newWidth, int newHeight) {
|
||||||
|
int numRows = rows.size();
|
||||||
|
for (int rowIndex = 0; rowIndex < numRows; ++rowIndex) {
|
||||||
|
Row row = rows.get(rowIndex);
|
||||||
|
int numKeys = row.mKeys.size();
|
||||||
|
int totalGap = 0;
|
||||||
|
int totalWidth = 0;
|
||||||
|
for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
|
||||||
|
Key key = row.mKeys.get(keyIndex);
|
||||||
|
if (keyIndex > 0) {
|
||||||
|
totalGap += key.gap;
|
||||||
|
}
|
||||||
|
totalWidth += key.width;
|
||||||
|
}
|
||||||
|
if (totalGap + totalWidth > newWidth) {
|
||||||
|
int x = 0;
|
||||||
|
float scaleFactor = (float) (newWidth - totalGap) / totalWidth;
|
||||||
|
for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
|
||||||
|
Key key = row.mKeys.get(keyIndex);
|
||||||
|
key.width *= scaleFactor;
|
||||||
|
key.x = x;
|
||||||
|
x += key.width + key.gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mTotalWidth = newWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Key> getKeys() {
|
||||||
|
return mKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total height of the keyboard
|
||||||
|
*
|
||||||
|
* @return the total height of the keyboard
|
||||||
|
*/
|
||||||
|
public int getHeight() {
|
||||||
|
return mTotalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinWidth() {
|
||||||
|
return mTotalWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setShifted(boolean shiftState) {
|
||||||
|
for (Key shiftKey : mShiftKeys) {
|
||||||
|
if (shiftKey != null) {
|
||||||
|
shiftKey.on = shiftState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mShifted != shiftState) {
|
||||||
|
mShifted = shiftState;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isShifted() {
|
||||||
|
return mShifted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void computeNearestNeighbors() {
|
||||||
|
// Round-up so we don't have any pixels outside the grid
|
||||||
|
mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
|
||||||
|
mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
|
||||||
|
mGridNeighbors = new int[GRID_SIZE][];
|
||||||
|
int[] indices = new int[mKeys.size()];
|
||||||
|
final int gridWidth = GRID_WIDTH * mCellWidth;
|
||||||
|
final int gridHeight = GRID_HEIGHT * mCellHeight;
|
||||||
|
for (int x = 0; x < gridWidth; x += mCellWidth) {
|
||||||
|
for (int y = 0; y < gridHeight; y += mCellHeight) {
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < mKeys.size(); i++) {
|
||||||
|
final Key key = mKeys.get(i);
|
||||||
|
if (key.squaredDistanceFrom(x, y) < mProximityThreshold ||
|
||||||
|
key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold ||
|
||||||
|
key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1)
|
||||||
|
< mProximityThreshold ||
|
||||||
|
key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold) {
|
||||||
|
indices[count++] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int[] cell = new int[count];
|
||||||
|
System.arraycopy(indices, 0, cell, 0, count);
|
||||||
|
mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the indices of the keys that are closest to the given point.
|
||||||
|
*
|
||||||
|
* @param x the x-coordinate of the point
|
||||||
|
* @param y the y-coordinate of the point
|
||||||
|
* @return the array of integer indices for the nearest keys to the given point. If the given
|
||||||
|
* point is out of range, then an array of size zero is returned.
|
||||||
|
*/
|
||||||
|
public int[] getNearestKeys(int x, int y) {
|
||||||
|
if (mGridNeighbors == null) computeNearestNeighbors();
|
||||||
|
if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
|
||||||
|
int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
|
||||||
|
if (index < GRID_SIZE) {
|
||||||
|
return mGridNeighbors[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new int[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Row createRowFromXml(Resources res, XmlResourceParser parser) {
|
||||||
|
return new Row(res, this, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Key createKeyFromXml(Resources res, Row parent, int x, int y,
|
||||||
|
XmlResourceParser parser) {
|
||||||
|
return new Key(res, parent, x, y, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadKeyboard(Context context, XmlResourceParser parser) {
|
||||||
|
boolean inKey = false;
|
||||||
|
boolean inRow = false;
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
Key key = null;
|
||||||
|
Row currentRow = null;
|
||||||
|
Resources res = context.getResources();
|
||||||
|
boolean skipRow;
|
||||||
|
|
||||||
|
try {
|
||||||
|
int event;
|
||||||
|
while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
|
||||||
|
if (event == XmlResourceParser.START_TAG) {
|
||||||
|
String tag = parser.getName();
|
||||||
|
if (TAG_ROW.equals(tag)) {
|
||||||
|
inRow = true;
|
||||||
|
x = 0;
|
||||||
|
currentRow = createRowFromXml(res, parser);
|
||||||
|
rows.add(currentRow);
|
||||||
|
skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
|
||||||
|
if (skipRow) {
|
||||||
|
skipToEndOfRow(parser);
|
||||||
|
inRow = false;
|
||||||
|
}
|
||||||
|
} else if (TAG_KEY.equals(tag)) {
|
||||||
|
inKey = true;
|
||||||
|
key = createKeyFromXml(res, currentRow, x, y, parser);
|
||||||
|
mKeys.add(key);
|
||||||
|
if (key.codes[0] == KEYCODE_SHIFT) {
|
||||||
|
// Find available shift key slot and put this shift key in it
|
||||||
|
for (int i = 0; i < mShiftKeys.length; i++) {
|
||||||
|
if (mShiftKeys[i] == null) {
|
||||||
|
mShiftKeys[i] = key;
|
||||||
|
mShiftKeyIndices[i] = mKeys.size() - 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mModifierKeys.add(key);
|
||||||
|
} else if (key.codes[0] == KEYCODE_ALT) {
|
||||||
|
mModifierKeys.add(key);
|
||||||
|
}
|
||||||
|
currentRow.mKeys.add(key);
|
||||||
|
} else if (TAG_KEYBOARD.equals(tag)) {
|
||||||
|
parseKeyboardAttributes(res, parser);
|
||||||
|
}
|
||||||
|
} else if (event == XmlResourceParser.END_TAG) {
|
||||||
|
if (inKey) {
|
||||||
|
inKey = false;
|
||||||
|
x += key.gap + key.width;
|
||||||
|
if (x > mTotalWidth) {
|
||||||
|
mTotalWidth = x;
|
||||||
|
}
|
||||||
|
} else if (inRow) {
|
||||||
|
inRow = false;
|
||||||
|
y += currentRow.verticalGap;
|
||||||
|
y += currentRow.defaultHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Parse error:" + e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
mTotalHeight = y - mDefaultVerticalGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void skipToEndOfRow(XmlResourceParser parser)
|
||||||
|
throws XmlPullParserException, IOException {
|
||||||
|
int event;
|
||||||
|
while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
|
||||||
|
if (event == XmlResourceParser.END_TAG
|
||||||
|
&& parser.getName().equals(TAG_ROW)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) {
|
||||||
|
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard);
|
||||||
|
|
||||||
|
mDefaultWidth = getDimensionOrFraction(a, R.styleable.Keyboard_keyWidth,
|
||||||
|
mDisplayWidth, mDisplayWidth / 10);
|
||||||
|
mDefaultHeight = getDimensionOrFraction(a, R.styleable.Keyboard_keyHeight,
|
||||||
|
mDisplayHeight, 50);
|
||||||
|
mDefaultHorizontalGap = getDimensionOrFraction(a, R.styleable.Keyboard_horizontalGap,
|
||||||
|
mDisplayWidth, 0);
|
||||||
|
mDefaultVerticalGap = getDimensionOrFraction(a, R.styleable.Keyboard_verticalGap,
|
||||||
|
mDisplayHeight, 0);
|
||||||
|
mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE);
|
||||||
|
mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) {
|
||||||
|
TypedValue value = a.peekValue(index);
|
||||||
|
if (value == null) return defValue;
|
||||||
|
if (value.type == TypedValue.TYPE_DIMENSION) {
|
||||||
|
return a.getDimensionPixelOffset(index, defValue);
|
||||||
|
} else if (value.type == TypedValue.TYPE_FRACTION) {
|
||||||
|
// Round it to avoid values like 47.9999 from getting truncated
|
||||||
|
return Math.round(a.getFraction(index, base, base, defValue));
|
||||||
|
}
|
||||||
|
return defValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -17,15 +17,12 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard
|
package com.kunzisoft.keepass.magikeyboard
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.inputmethodservice.InputMethodService
|
import android.inputmethodservice.InputMethodService
|
||||||
import android.inputmethodservice.Keyboard
|
|
||||||
import android.inputmethodservice.KeyboardView
|
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -99,7 +96,6 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
popupCustomKeys = PopupWindow(context).apply {
|
popupCustomKeys = PopupWindow(context).apply {
|
||||||
width = WindowManager.LayoutParams.WRAP_CONTENT
|
width = WindowManager.LayoutParams.WRAP_CONTENT
|
||||||
height = WindowManager.LayoutParams.WRAP_CONTENT
|
height = WindowManager.LayoutParams.WRAP_CONTENT
|
||||||
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
|
||||||
inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
|
inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
|
||||||
contentView = popupFieldsView
|
contentView = popupFieldsView
|
||||||
}
|
}
|
||||||
@@ -130,8 +126,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
removeEntryInfo()
|
removeEntryInfo()
|
||||||
}
|
}
|
||||||
assignKeyboardView()
|
assignKeyboardView()
|
||||||
keyboardView?.setOnKeyboardActionListener(this)
|
keyboardView?.onKeyboardActionListener = this
|
||||||
keyboardView?.isPreviewEnabled = false
|
|
||||||
|
|
||||||
return rootKeyboardView
|
return rootKeyboardView
|
||||||
}
|
}
|
||||||
@@ -206,6 +201,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
switchToPreviousInputMethod()
|
switchToPreviousInputMethod()
|
||||||
} else {
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
window.window?.let { window ->
|
window.window?.let { window ->
|
||||||
imeManager?.switchToLastInputMethod(window.attributes.token)
|
imeManager?.switchToLastInputMethod(window.attributes.token)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ class EntryInfo : NodeInfo {
|
|||||||
var url: String = ""
|
var url: String = ""
|
||||||
var notes: String = ""
|
var notes: String = ""
|
||||||
var tags: Tags = Tags()
|
var tags: Tags = Tags()
|
||||||
|
var backgroundColor: Int? = null
|
||||||
|
var foregroundColor: Int? = null
|
||||||
var customFields: MutableList<Field> = mutableListOf()
|
var customFields: MutableList<Field> = mutableListOf()
|
||||||
var attachments: MutableList<Attachment> = mutableListOf()
|
var attachments: MutableList<Attachment> = mutableListOf()
|
||||||
var otpModel: OtpModel? = null
|
var otpModel: OtpModel? = null
|
||||||
@@ -52,6 +54,10 @@ class EntryInfo : NodeInfo {
|
|||||||
url = parcel.readString() ?: url
|
url = parcel.readString() ?: url
|
||||||
notes = parcel.readString() ?: notes
|
notes = parcel.readString() ?: notes
|
||||||
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
|
tags = parcel.readParcelable(Tags::class.java.classLoader) ?: tags
|
||||||
|
val readBgColor = parcel.readInt()
|
||||||
|
backgroundColor = if (readBgColor == -1) null else readBgColor
|
||||||
|
val readFgColor = parcel.readInt()
|
||||||
|
foregroundColor = if (readFgColor == -1) null else readFgColor
|
||||||
parcel.readList(customFields, Field::class.java.classLoader)
|
parcel.readList(customFields, Field::class.java.classLoader)
|
||||||
parcel.readList(attachments, Attachment::class.java.classLoader)
|
parcel.readList(attachments, Attachment::class.java.classLoader)
|
||||||
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
|
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
|
||||||
@@ -70,6 +76,8 @@ class EntryInfo : NodeInfo {
|
|||||||
parcel.writeString(url)
|
parcel.writeString(url)
|
||||||
parcel.writeString(notes)
|
parcel.writeString(notes)
|
||||||
parcel.writeParcelable(tags, flags)
|
parcel.writeParcelable(tags, flags)
|
||||||
|
parcel.writeInt(backgroundColor ?: -1)
|
||||||
|
parcel.writeInt(foregroundColor ?: -1)
|
||||||
parcel.writeList(customFields)
|
parcel.writeList(customFields)
|
||||||
parcel.writeList(attachments)
|
parcel.writeList(attachments)
|
||||||
parcel.writeParcelable(otpModel, flags)
|
parcel.writeParcelable(otpModel, flags)
|
||||||
@@ -197,6 +205,8 @@ class EntryInfo : NodeInfo {
|
|||||||
if (url != other.url) return false
|
if (url != other.url) return false
|
||||||
if (notes != other.notes) return false
|
if (notes != other.notes) return false
|
||||||
if (tags != other.tags) return false
|
if (tags != other.tags) return false
|
||||||
|
if (backgroundColor != other.backgroundColor) return false
|
||||||
|
if (foregroundColor != other.foregroundColor) return false
|
||||||
if (customFields != other.customFields) return false
|
if (customFields != other.customFields) return false
|
||||||
if (attachments != other.attachments) return false
|
if (attachments != other.attachments) return false
|
||||||
if (otpModel != other.otpModel) return false
|
if (otpModel != other.otpModel) return false
|
||||||
@@ -213,6 +223,8 @@ class EntryInfo : NodeInfo {
|
|||||||
result = 31 * result + url.hashCode()
|
result = 31 * result + url.hashCode()
|
||||||
result = 31 * result + notes.hashCode()
|
result = 31 * result + notes.hashCode()
|
||||||
result = 31 * result + tags.hashCode()
|
result = 31 * result + tags.hashCode()
|
||||||
|
result = 31 * result + backgroundColor.hashCode()
|
||||||
|
result = 31 * result + foregroundColor.hashCode()
|
||||||
result = 31 * result + customFields.hashCode()
|
result = 31 * result + customFields.hashCode()
|
||||||
result = 31 * result + attachments.hashCode()
|
result = 31 * result + attachments.hashCode()
|
||||||
result = 31 * result + (otpModel?.hashCode() ?: 0)
|
result = 31 * result + (otpModel?.hashCode() ?: 0)
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package com.kunzisoft.keepass.model
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
|
import android.os.ParcelUuid
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER_ID
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER_ID
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class GroupInfo : NodeInfo {
|
class GroupInfo : NodeInfo {
|
||||||
|
|
||||||
|
var id: UUID? = null
|
||||||
var notes: String? = null
|
var notes: String? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -16,11 +19,14 @@ class GroupInfo : NodeInfo {
|
|||||||
constructor(): super()
|
constructor(): super()
|
||||||
|
|
||||||
constructor(parcel: Parcel): super(parcel) {
|
constructor(parcel: Parcel): super(parcel) {
|
||||||
|
id = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: id
|
||||||
notes = parcel.readString()
|
notes = parcel.readString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
super.writeToParcel(parcel, flags)
|
super.writeToParcel(parcel, flags)
|
||||||
|
val uuid = if (id != null) ParcelUuid(id) else null
|
||||||
|
parcel.writeParcelable(uuid, flags)
|
||||||
parcel.writeString(notes)
|
parcel.writeString(notes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +35,7 @@ class GroupInfo : NodeInfo {
|
|||||||
if (other !is GroupInfo) return false
|
if (other !is GroupInfo) return false
|
||||||
if (!super.equals(other)) return false
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
if (notes != other.notes) return false
|
if (notes != other.notes) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -36,6 +43,7 @@ class GroupInfo : NodeInfo {
|
|||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + (id?.hashCode() ?: 0)
|
||||||
result = 31 * result + (notes?.hashCode() ?: 0)
|
result = 31 * result + (notes?.hashCode() ?: 0)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user