Merge branch 'develop' into feature/Tags

This commit is contained in:
J-Jamet
2022-01-26 18:43:21 +01:00
247 changed files with 10470 additions and 3874 deletions

View File

@@ -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]

View File

@@ -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)

View File

@@ -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.

View File

@@ -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"
} }

View File

@@ -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"/>

View File

@@ -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,

View File

@@ -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
)
} }
} }
} }

View File

@@ -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
) )

View File

@@ -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)
} }

View File

@@ -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)

View File

@@ -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) )
} }
} }
} }

View File

@@ -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()

View File

@@ -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"

View File

@@ -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)
}
}
}
}
}

View File

@@ -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)

View File

@@ -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
}
}
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)
} }

View File

@@ -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)
} }

View File

@@ -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
} }

View File

@@ -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)

View File

@@ -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

View File

@@ -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 {

View File

@@ -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()
} }

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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
} }
} }

View File

@@ -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"
}
} }
} }

View File

@@ -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
} }

View File

@@ -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?)

View File

@@ -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()
}
} }
} }

View File

@@ -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)
}
}
}

View File

@@ -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())
} }
} }

View File

@@ -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

View File

@@ -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)

View File

@@ -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()
}
} }
} }

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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)
} }

View File

@@ -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)
}
}

View File

@@ -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)
} }
} }

View File

@@ -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)

View File

@@ -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()
} }

View File

@@ -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)
} }

View File

@@ -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())

View File

@@ -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)
} }

View File

@@ -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"
}
}

View File

@@ -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++
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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))))
}
}

View File

@@ -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"
}
}

View File

@@ -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?) {

View File

@@ -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 {

View File

@@ -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 ->

View File

@@ -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

View File

@@ -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

View File

@@ -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()
} }

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
} }

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 }
}
} }
/** /**

View File

@@ -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?
} }

View File

@@ -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> {

View File

@@ -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> {

View File

@@ -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)

View File

@@ -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)
} }

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
} }

View File

@@ -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"

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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)
}
} }

View File

@@ -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 {

View File

@@ -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)
} }
} }
} }

View File

@@ -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)

View File

@@ -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)
} }

View File

@@ -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 ->

View File

@@ -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())
} }
} }

View File

@@ -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))
} }
} }

View File

@@ -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 ||

View File

@@ -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)
}
}
}
}
}
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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>
* &lt;Keyboard
* android:keyWidth="%10p"
* android:keyHeight="50px"
* android:horizontalGap="2px"
* android:verticalGap="2px" &gt;
* &lt;Row android:keyWidth="32px" &gt;
* &lt;Key android:keyLabel="A" /&gt;
* ...
* &lt;/Row&gt;
* ...
* &lt;/Keyboard&gt;
* </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

View File

@@ -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)
} }

View File

@@ -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)

View File

@@ -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