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]
- Version: [e.g. 2]
- 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]
- 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)
* 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
* 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)
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)

View File

@@ -15,6 +15,7 @@
- Material design with **themes**.
- **Auto-Fill** and Integration.
- Field filling **keyboard**.
- Dynamic **templates**
- **History** of each entry.
- Precise management of **settings**.
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
@@ -71,7 +72,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
## 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.

View File

@@ -3,16 +3,16 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
compileSdkVersion 31
buildToolsVersion "31.0.0"
ndkVersion "21.4.7075529"
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 30
versionCode = 90
versionName = "3.1.0"
targetSdkVersion 31
versionCode = 93
versionName = "3.2.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"
@@ -99,22 +99,22 @@ android {
}
}
def room_version = "2.2.6"
def room_version = "2.4.1"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.media:media:1.4.3'
// Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:1.3.2"
implementation 'androidx.fragment:fragment-ktx:1.2.5'
// WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
implementation 'com.google.android.material:material:1.1.0'
implementation "androidx.core:core-ktx:$android_core_version"
implementation 'androidx.fragment:fragment-ktx:1.4.0'
implementation "com.google.android.material:material:$android_material_version"
// Token auto complete
implementation "com.splitwise:tokenautocomplete:4.0.0-beta04"
// Database
@@ -123,11 +123,11 @@ dependencies {
// Autofill
implementation "androidx.autofill:autofill:1.1.0"
// Time
implementation 'joda-time:joda-time:2.10.6'
implementation 'joda-time:joda-time:2.10.13'
// Color
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
// Education
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
// Apache Commons
implementation 'commons-io:commons-io:2.8.0'
implementation 'commons-codec:commons-codec:1.15'
@@ -138,6 +138,6 @@ dependencies {
implementation project(path: ':icon-pack-material')
// Tests
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation "androidx.test:runner:$android_test_version"
androidTestImplementation "androidx.test:rules:$android_test_version"
}

View File

@@ -10,15 +10,12 @@
android:anyDensity="true" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission
android:name="android.permission.USE_BIOMETRIC" />
<uses-permission
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 -->
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
@@ -30,12 +27,13 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:name="com.kunzisoft.keepass.app.App"
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:largeHeap="true"
android:resizeableActivity="true"
android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="n">
tools:targetApi="s">
<meta-data
android:name="com.google.android.backup.api_key"
android:value="${googleAndroidBackupAPIKey}" />
@@ -44,6 +42,7 @@
android:theme="@style/KeepassDXStyle.SplashScreen"
android:label="@string/app_name"
android:launchMode="singleTop"
android:exported="true"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
<intent-filter>
@@ -53,6 +52,7 @@
</activity>
<activity
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
android:exported="true"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustResize|stateUnchanged">
<intent-filter>
@@ -111,6 +111,7 @@
<!-- Main Activity -->
<activity
android:name="com.kunzisoft.keepass.activities.GroupActivity"
android:exported="false"
android:configChanges="keyboardHidden"
android:windowSoftInputMode="adjustPan">
<meta-data
@@ -154,7 +155,8 @@
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent">
android:theme="@style/Theme.Transparent"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
@@ -173,7 +175,8 @@
android:theme="@style/Theme.Transparent" />
<activity
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
android:label="@string/keyboard_setting_label">
android:label="@string/keyboard_setting_label"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
@@ -199,6 +202,7 @@
<service
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
android:label="@string/autofill_service_name"
android:exported="true"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<meta-data
android:name="android.autofill"
@@ -210,6 +214,7 @@
<service
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
android:label="@string/keyboard_label"
android:exported="true"
android:permission="android.permission.BIND_INPUT_METHOD" >
<meta-data android:name="android.view.im"
android:resource="@xml/keyboard_method"/>

View File

@@ -23,28 +23,33 @@ import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.view.inputmethod.InlineSuggestionsRequest
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
import com.kunzisoft.keepass.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.LOCK_ACTION
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() {
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this, true)
else null
override fun applyCustomStyle(): Boolean {
return false
}
@@ -60,17 +65,37 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
when (specialMode) {
SpecialMode.SELECTION -> {
// Build search param
val searchInfo = SearchInfo().apply {
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false)
}
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
launchSelection(database, searchInfo)
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
// To pass extra inline request
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION)
}
// Build search param
bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
SearchInfo.getConcreteWebDomain(
this,
searchInfo.webDomain
) { concreteWebDomain ->
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val assistStructure = AutofillHelper
.retrieveAutofillComponent(intent)
?.assistStructure
val newAutofillComponent = if (assistStructure != null) {
AutofillComponent(
assistStructure,
compatInlineSuggestionsRequest
)
} else {
null
}
searchInfo.webDomain = concreteWebDomain
launchSelection(database, newAutofillComponent, searchInfo)
}
}
}
// Remove bundle
intent.removeExtra(KEY_SELECTION_BUNDLE)
}
SpecialMode.REGISTRATION -> {
// To register info
@@ -91,10 +116,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
private fun launchSelection(database: Database?,
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo) {
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
if (autofillComponent == null) {
setResult(Activity.RESULT_CANCELED)
finish()
@@ -119,6 +142,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
// Show the database UI to select the entry
GroupActivity.launchForAutofillResult(this,
openedDatabase,
mAutofillActivityResultLauncher,
autofillComponent,
searchInfo,
false)
@@ -126,6 +150,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
{
// If database not open
FileDatabaseSelectActivity.launchForAutofillResult(this,
mAutofillActivityResultLauncher,
autofillComponent,
searchInfo)
}
@@ -186,55 +211,47 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
// Close the database
sendBroadcast(Intent(LOCK_ACTION))
}
super.onActivityResult(requestCode, resultCode, data)
}
companion object {
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION"
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
fun getPendingIntentForSelection(context: Context,
searchInfo: SearchInfo? = null,
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent {
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
return PendingIntent.getActivity(context, 0,
// Doesn't work with Parcelable (don't know why?)
Intent(context, AutofillLauncherActivity::class.java).apply {
searchInfo?.let {
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
}
// Doesn't work with direct extra Parcelable (don't know why?)
// Wrap into a bundle to bypass the problem
Intent(context, AutofillLauncherActivity::class.java).apply {
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
putParcelable(KEY_SEARCH_INFO, searchInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let {
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
}
},
PendingIntent.FLAG_CANCEL_CURRENT)
})
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
}
fun getPendingIntentForRegistration(context: Context,
registerInfo: RegisterInfo): PendingIntent {
return PendingIntent.getActivity(context, 0,
Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo)
},
PendingIntent.FLAG_CANCEL_CURRENT)
Intent(context, AutofillLauncherActivity::class.java).apply {
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
putExtra(KEY_REGISTER_INFO, registerInfo)
},
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
})
}
fun launchForRegistration(context: Context,

View File

@@ -32,12 +32,18 @@ import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.recyclerview.widget.LinearLayoutManager
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.progressindicator.LinearProgressIndicator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.fragments.EntryFragment
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.AttachmentFileBinderManager
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.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.*
import kotlin.collections.HashMap
class EntryActivity : DatabaseLockActivity() {
private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null
private var titleIconView: ImageView? = null
private var historyView: View? = null
private var tagsListView: RecyclerView? = null
private var tagsAdapter: TagsAdapter? = null
private var entryProgress: ProgressBar? = null
private var entryProgress: LinearProgressIndicator? = null
private var lockView: View? = null
private var toolbar: Toolbar? = null
private var loadingView: ProgressBar? = null
@@ -89,11 +99,21 @@ class EntryActivity : DatabaseLockActivity() {
private var mEntryLoaded = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
private var mExternalFileHelper: ExternalFileHelper? = null
private var mAttachmentSelected: Attachment? = null
private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) {
// Reload the current id from database
mEntryViewModel.loadDatabase(mDatabase)
}
private var mIcon: IconImage? = null
private var mIconColor: Int = 0
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?) {
super.onCreate(savedInstanceState)
@@ -108,6 +128,7 @@ class EntryActivity : DatabaseLockActivity() {
// Get views
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar)
titleIconView = findViewById(R.id.entry_icon)
historyView = findViewById(R.id.history_container)
tagsListView = findViewById(R.id.entry_tags_list_view)
@@ -119,10 +140,19 @@ class EntryActivity : DatabaseLockActivity() {
collapsingToolbarLayout?.title = " "
toolbar?.title = " "
// Retrieve the textColor to tint the icon
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
mIconColor = taIconColor.getColor(0, Color.BLACK)
taIconColor.recycle()
// Retrieve the textColor to tint the toolbar
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
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
tagsAdapter = TagsAdapter(this)
@@ -146,6 +176,15 @@ class EntryActivity : DatabaseLockActivity() {
// Init SAF manager
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
mAttachmentSelected?.let { attachment ->
if (createdFileUri != null) {
mAttachmentFileBinderManager
?.startDownloadAttachment(createdFileUri, attachment)
}
mAttachmentSelected = null
}
}
// Init attachment service binder manager
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
@@ -165,10 +204,8 @@ class EntryActivity : DatabaseLockActivity() {
// Assign history dedicated view
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
if (entryIsHistory) {
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
collapsingToolbarLayout?.contentScrim =
ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
taColorAccent.recycle()
ColorDrawable(mColorAccent)
}
val entryInfo = entryInfoHistory.entryInfo
@@ -183,12 +220,9 @@ class EntryActivity : DatabaseLockActivity() {
}
// Assign title icon
mIcon = entryInfo.icon
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
}
// Assign title text
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
toolbar?.title = entryTitle
mUrl = entryInfo.url
@@ -196,6 +230,9 @@ class EntryActivity : DatabaseLockActivity() {
val tags = entryInfo.tags
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
tagsAdapter?.setTags(tags)
// Assign colors
mBackgroundColor = entryInfo.backgroundColor
mForegroundColor = entryInfo.foregroundColor
loadingView?.hideByFading()
mEntryLoaded = true
@@ -207,9 +244,9 @@ class EntryActivity : DatabaseLockActivity() {
}
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
if (otpElement == null)
if (otpElement == null) {
entryProgress?.visibility = View.GONE
when (otpElement?.type) {
} else when (otpElement.type) {
// Only add token if HOTP
OtpType.HOTP -> {
entryProgress?.visibility = View.GONE
@@ -218,7 +255,7 @@ class EntryActivity : DatabaseLockActivity() {
OtpType.TOTP -> {
entryProgress?.apply {
max = otpElement.period
progress = otpElement.secondsRemaining
setProgressCompat(otpElement.secondsRemaining, true)
visibility = View.VISIBLE
}
}
@@ -226,9 +263,8 @@ class EntryActivity : DatabaseLockActivity() {
}
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentSelected
}
mAttachmentSelected = attachmentSelected
mExternalFileHelper?.createDocument(attachmentSelected.name)
}
mEntryViewModel.historySelected.observe(this) { historySelected ->
@@ -237,7 +273,8 @@ class EntryActivity : DatabaseLockActivity() {
this,
database,
historySelected.nodeId,
historySelected.historyPosition
historySelected.historyPosition,
mEntryActivityResultLauncher
)
}
}
@@ -255,13 +292,6 @@ class EntryActivity : DatabaseLockActivity() {
super.onDatabaseRetrieved(database)
mEntryViewModel.loadDatabase(database)
// Assign title icon
mIcon?.let { icon ->
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor)
}
}
}
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() {
@@ -307,24 +342,27 @@ class EntryActivity : DatabaseLockActivity() {
super.onPause()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
// Reload the current id from database
mEntryViewModel.loadDatabase(mDatabase)
}
}
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
if (createdFileUri != null) {
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
mAttachmentFileBinderManager
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
}
private fun applyToolbarColors() {
appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary)
val backgroundDarker = if (mBackgroundColor != null) {
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
} else {
mColorBackground
}
titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
mIcon?.let { icon ->
titleIconView?.let { iconView ->
mIconDrawableFactory?.assignDatabaseIcon(
iconView,
icon,
mForegroundColor ?: mColorAccent
)
}
}
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -358,11 +396,17 @@ class EntryActivity : DatabaseLockActivity() {
}
if (mEntryIsHistory || mDatabaseReadOnly) {
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
}
if (!mMergeDataAllowed) {
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
}
if (mSpecialMode != SpecialMode.DEFAULT) {
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
}
applyToolbarColors()
return super.onPrepareOptionsMenu(menu)
}
@@ -408,7 +452,8 @@ class EntryActivity : DatabaseLockActivity() {
EntryEditActivity.launchToUpdate(
this,
database,
entryId
entryId,
mEntryActivityResultLauncher
)
}
}
@@ -437,6 +482,9 @@ class EntryActivity : DatabaseLockActivity() {
R.id.menu_save_database -> {
saveDatabase()
}
R.id.menu_merge_database -> {
mergeDatabase()
}
R.id.menu_reload_database -> {
reloadDatabase()
}
@@ -449,7 +497,7 @@ class EntryActivity : DatabaseLockActivity() {
// Transit data in previous Activity after an update
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this)
setResult(Activity.RESULT_OK, this)
}
super.finish()
}
@@ -467,15 +515,13 @@ class EntryActivity : DatabaseLockActivity() {
*/
fun launch(activity: Activity,
database: Database,
entryId: NodeId<UUID>) {
entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
activity.startActivityForResult(
intent,
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
)
activityResultLauncher.launch(intent)
}
}
}
@@ -486,16 +532,14 @@ class EntryActivity : DatabaseLockActivity() {
fun launch(activity: Activity,
database: Database,
entryId: NodeId<UUID>,
historyPosition: Int) {
historyPosition: Int,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
activity.startActivityForResult(
intent,
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
)
activityResultLauncher.launch(intent)
}
}
}

View File

@@ -33,12 +33,17 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.*
@@ -69,6 +74,7 @@ import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.*
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import org.joda.time.DateTime
import java.util.*
@@ -96,6 +102,9 @@ class EntryEditActivity : DatabaseLockActivity(),
private var mTemplate: Template? = null
private var mIsTemplate: Boolean = false
private var mEntryLoaded: Boolean = false
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
private val mColorPickerViewModel: ColorPickerViewModel by viewModels()
private var mAllowCustomFields = false
private var mAllowOTP = false
@@ -106,6 +115,10 @@ class EntryEditActivity : DatabaseLockActivity(),
// Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
mEntryEditViewModel.selectIcon(icon)
}
// To ask data lost only one time
private var backPressedAlreadyApproved = false
@@ -154,6 +167,21 @@ class EntryEditActivity : DatabaseLockActivity(),
// To retrieve attachment
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { attachmentToUploadUri ->
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
documentFile.name?.let { fileName ->
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
.show(supportFragmentManager, "fileTooBigFragment")
} else {
mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName)
}
}
}
}
}
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
// Verify the education views
entryEditActivityEducation = EntryEditActivityEducation(this)
@@ -175,11 +203,13 @@ class EntryEditActivity : DatabaseLockActivity(),
templateSelectorSpinner?.apply {
// Build template selector
if (templates.isNotEmpty()) {
adapter = TemplatesSelectorAdapter(
mTemplatesSelectorAdapter = TemplatesSelectorAdapter(
this@EntryEditActivity,
mIconDrawableFactory,
templates
)
).apply {
iconDrawableFactory = mIconDrawableFactory
}
adapter = mTemplatesSelectorAdapter
val selectedTemplate = if (mTemplate != null)
mTemplate
else
@@ -213,7 +243,16 @@ class EntryEditActivity : DatabaseLockActivity(),
// View model listeners
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 ->
@@ -321,6 +360,10 @@ class EntryEditActivity : DatabaseLockActivity(),
mAllowCustomFields = database?.allowEntryCustomFields() == true
mAllowOTP = database?.allowOTP == true
mEntryEditViewModel.loadDatabase(database)
mTemplatesSelectorAdapter?.apply {
iconDrawableFactory = mIconDrawableFactory
notifyDataSetChanged()
}
}
override fun onDatabaseActionFinished(
@@ -472,29 +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
*/
@@ -585,7 +605,7 @@ class EntryEditActivity : DatabaseLockActivity(),
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
attachmentView,
{
mExternalFileHelper?.openDocument()
addNewAttachment()
},
{
performedNextEducation(entryEditActivityEducation)
@@ -686,7 +706,7 @@ class EntryEditActivity : DatabaseLockActivity(),
val intentEntry = Intent()
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
intentEntry.putExtras(bundle)
setResult(ADD_OR_UPDATE_ENTRY_RESULT_CODE, intentEntry)
setResult(Activity.RESULT_OK, intentEntry)
super.finish()
} catch (e: Exception) {
// Exception when parcelable can't be done
@@ -701,23 +721,46 @@ class EntryEditActivity : DatabaseLockActivity(),
// Keys for current Activity
const val KEY_ENTRY = "entry"
const val KEY_PARENT = "parent"
// Keys for callback
const val ADD_OR_UPDATE_ENTRY_RESULT_CODE = 31
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
fun registerForEntryResult(fragment: Fragment,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
)
} else {
entryAddedOrUpdatedListener.invoke(null)
}
}
}
fun registerForEntryResult(activity: FragmentActivity,
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
entryAddedOrUpdatedListener.invoke(
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
)
} else {
entryAddedOrUpdatedListener.invoke(null)
}
}
}
/**
* Launch EntryEditActivity to update an existing entry by his [entryId]
*/
fun launchToUpdate(activity: Activity,
database: Database,
entryId: NodeId<UUID>) {
entryId: NodeId<UUID>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
activityResultLauncher.launch(intent)
}
}
}
@@ -727,12 +770,13 @@ class EntryEditActivity : DatabaseLockActivity(),
*/
fun launchToCreate(activity: Activity,
database: Database,
groupId: NodeId<*>) {
groupId: NodeId<*>,
activityResultLauncher: ActivityResultLauncher<Intent>) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
activityResultLauncher.launch(intent)
}
}
}
@@ -795,8 +839,9 @@ class EntryEditActivity : DatabaseLockActivity(),
* Launch EntryEditActivity to add a new entry in autofill selection
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity,
fun launchForAutofillResult(activity: AppCompatActivity,
database: Database,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
groupId: NodeId<*>,
searchInfo: SearchInfo? = null) {
@@ -807,6 +852,7 @@ class EntryEditActivity : DatabaseLockActivity(),
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
activityResultLauncher,
autofillComponent,
searchInfo
)

View File

@@ -31,8 +31,10 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.recyclerview.widget.LinearLayoutManager
@@ -85,6 +87,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
AutofillHelper.buildActivityResultLauncher(this)
else null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -109,6 +116,22 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
// Open database button
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let {
launchPasswordActivityWithPath(uri)
}
}
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
} else {
val error = getString(R.string.error_create_database)
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error)
}
}
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
@@ -256,8 +279,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
* Create a new file by calling the content provider
*/
private fun createNewFile() {
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) +
getString(R.string.database_file_extension_default), "application/x-keepass")
mExternalFileHelper?.createDocument(
getString(R.string.database_file_name_default) +
getString(R.string.database_file_extension_default))
}
private fun fileNoFoundAction(e: FileNotFoundException) {
@@ -274,7 +298,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
fileNoFoundAction(exception)
},
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() })
{ onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher)
}
private fun launchGroupActivityIfLoaded(database: Database) {
@@ -283,7 +308,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
database,
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() })
{ onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher)
}
}
@@ -359,33 +385,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
if (uri != null) {
launchPasswordActivityWithPath(uri)
}
}
// Retrieve the created URI from the file manager
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
mDatabaseFileUri = databaseFileCreatedUri
if (mDatabaseFileUri != null) {
AssignMasterKeyDialogFragment.getInstance(true)
.show(supportFragmentManager, "passwordDialog")
} else {
val error = getString(R.string.error_create_database)
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
Log.e(TAG, error)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
@@ -499,11 +498,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity,
fun launchForAutofillResult(activity: AppCompatActivity,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) {
AutofillHelper.startActivityForAutofillResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
autofillComponent,
searchInfo)
}

View File

@@ -25,7 +25,7 @@ import android.app.TimePickerDialog
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.PorterDuff
import android.os.*
import android.util.Log
import android.view.Menu
@@ -33,18 +33,23 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.fragments.GroupFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
import com.kunzisoft.keepass.autofill.AutofillComponent
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.SearchInfo
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.getListNodesFromBundle
import com.kunzisoft.keepass.settings.PreferencesUtil
@@ -75,6 +81,7 @@ class GroupActivity : DatabaseLockActivity(),
GroupFragment.NodeClickListener,
GroupFragment.NodesActionMenuListener,
GroupFragment.OnScrollListener,
GroupFragment.GroupRefreshedListener,
SortDialogFragment.SortSelectionListener {
// Views
@@ -82,18 +89,25 @@ class GroupActivity : DatabaseLockActivity(),
private var coordinatorLayout: CoordinatorLayout? = null
private var lockView: View? = 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 toolbarAction: ToolbarAction? = null
private var iconView: ImageView? = null
private var numberChildrenView: TextView? = null
private var addNodeButtonView: AddNodeButtonView? = null
private var groupNameView: TextView? = null
private var groupMetaView: TextView? = null
private var breadcrumbListView: RecyclerView? = null
private var loadingView: ProgressBar? = null
private val mGroupViewModel: GroupViewModel by viewModels()
private val mGroupEditViewModel: GroupEditViewModel by viewModels()
private var mBreadcrumbAdapter: BreadcrumbAdapter? = null
private var mGroupFragment: GroupFragment? = null
private var mRecyclingBinEnabled = false
private var mRecyclingBinIsCurrentGroup = false
@@ -111,7 +125,15 @@ class GroupActivity : DatabaseLockActivity(),
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = 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?) {
super.onCreate(savedInstanceState)
@@ -122,13 +144,18 @@ class GroupActivity : DatabaseLockActivity(),
// Initialize views
rootContainerView = findViewById(R.id.activity_group_container_view)
coordinatorLayout = findViewById(R.id.group_coordinator)
iconView = findViewById(R.id.group_icon)
numberChildrenView = findViewById(R.id.group_numbers)
addNodeButtonView = findViewById(R.id.add_node_button)
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)
groupNameView = findViewById(R.id.group_name)
groupMetaView = findViewById(R.id.group_meta)
breadcrumbListView = findViewById(R.id.breadcrumb_list)
toolbarAction = findViewById(R.id.toolbar_action)
lockView = findViewById(R.id.lock_button)
loadingView = findViewById(R.id.loading)
@@ -140,10 +167,42 @@ class GroupActivity : DatabaseLockActivity(),
toolbar?.title = ""
setSupportActionBar(toolbar)
// Retrieve the textColor to tint the icon
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
mIconColor = taTextColor.getColor(0, Color.WHITE)
taTextColor.recycle()
mBreadcrumbAdapter = BreadcrumbAdapter(this).apply {
// Open group on breadcrumb click
onItemClickListener = { node, _ ->
// 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
manageIntent(intent)
@@ -201,21 +260,22 @@ class GroupActivity : DatabaseLockActivity(),
// Add listeners to the add buttons
addNodeButtonView?.setAddGroupClickListener {
GroupEditDialogFragment.create(GroupInfo().apply {
if (currentGroup.allowAddNoteInGroup) {
notes = ""
}
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
launchDialogForGroupCreation(currentGroup)
}
addNodeButtonView?.setAddEntryClickListener {
mDatabase?.let { database ->
EntrySelectionHelper.doSpecialAction(intent,
{
EntryEditActivity.launchToCreate(
this@GroupActivity,
database,
currentGroup.nodeId
)
mCurrentGroup?.nodeId?.let { currentParentGroupId ->
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
EntryEditActivity.launchToCreate(
this@GroupActivity,
database,
currentParentGroupId,
resultLauncher
)
}
}
},
{
// Search not used
@@ -243,6 +303,7 @@ class GroupActivity : DatabaseLockActivity(),
EntryEditActivity.launchForAutofillResult(
this@GroupActivity,
database,
mAutofillActivityResultLauncher,
autofillComponent,
currentGroup.nodeId,
searchInfo
@@ -266,9 +327,6 @@ class GroupActivity : DatabaseLockActivity(),
}
}
assignGroupViewElements(currentGroup)
invalidateOptionsMenu()
loadingView?.hideByFading()
}
@@ -277,7 +335,7 @@ class GroupActivity : DatabaseLockActivity(),
}
mGroupEditViewModel.requestIconSelection.observe(this) { iconImage ->
IconPickerActivity.launch(this@GroupActivity, iconImage)
IconPickerActivity.launch(this@GroupActivity, iconImage, mIconSelectionActivityResultLauncher)
}
mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
@@ -319,6 +377,29 @@ class GroupActivity : DatabaseLockActivity(),
return rootContainerView
}
private fun loadGroup(database: Database?) {
when {
Intent.ACTION_SEARCH == intent.action -> {
finishNodeAction()
val searchString =
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
mGroupViewModel.loadGroupFromSearch(
database,
searchString,
PreferencesUtil.omitBackup(this)
)
}
mCurrentGroupState == null -> {
mRootGroup?.let { rootGroup ->
mGroupViewModel.loadGroup(database, rootGroup, 0)
}
}
else -> {
mGroupViewModel.loadGroup(database, mCurrentGroupState)
}
}
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
@@ -328,17 +409,23 @@ class GroupActivity : DatabaseLockActivity(),
&& database?.isRecycleBinEnabled == true
mRootGroup = database?.rootGroup
if (mCurrentGroupState == null) {
mRootGroup?.let { rootGroup ->
mGroupViewModel.loadGroup(database, rootGroup, 0)
}
} else {
mGroupViewModel.loadGroup(database, mCurrentGroupState)
}
loadGroup(database)
// Search suggestion
database?.let {
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)
mBreadcrumbAdapter?.iconDrawableFactory = it.iconDrawableFactory
mOnSuggestionListener = object : SearchView.OnSuggestionListener {
override fun onSuggestionClick(position: Int): Boolean {
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)
if (!result.isSuccess) {
reloadCurrentGroup()
}
finishNodeAction()
refreshNumberOfChildren(mCurrentGroup)
}
/**
@@ -447,16 +545,7 @@ class GroupActivity : DatabaseLockActivity(),
}
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
transformSearchInfoIntent(intent)
if (Intent.ACTION_SEARCH == intent.action) {
finishNodeAction()
val searchString =
intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
mGroupViewModel.loadGroupFromSearch(
mDatabase,
searchString,
PreferencesUtil.omitBackup(this)
)
}
loadGroup(mDatabase)
}
}
@@ -476,62 +565,44 @@ class GroupActivity : DatabaseLockActivity(),
super.onSaveInstanceState(outState)
}
override fun onGroupRefreshed() {
mCurrentGroup?.let { currentGroup ->
assignGroupViewElements(currentGroup)
}
}
private fun assignGroupViewElements(group: Group?) {
// 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) {
searchTitleView?.visibility = View.VISIBLE
if (toolbar != null) {
toolbar?.navigationIcon = null
}
iconView?.visibility = View.GONE
searchContainer?.visibility = View.VISIBLE
val title = group.title
searchString?.text = if (title.isNotEmpty()) title else ""
searchNumbers?.text = group.numberOfChildEntries.toString()
databaseNameContainer?.visibility = View.GONE
toolbarBreadcrumb?.navigationIcon = null
toolbarBreadcrumb?.collapse()
} else {
searchTitleView?.visibility = View.GONE
// Assign the group icon depending of IconPack or custom icon
iconView?.visibility = View.VISIBLE
group?.let { currentGroup ->
iconView?.let { imageView ->
mIconDrawableFactory?.assignDatabaseIcon(
imageView,
currentGroup.icon,
mIconColor
)
}
if (toolbar != null) {
if (group.containsParent())
toolbar?.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp)
else {
toolbar?.navigationIcon = null
}
searchContainer?.visibility = View.GONE
databaseNameContainer?.visibility = View.VISIBLE
// Refresh breadcrumb
if (toolbarBreadcrumb?.isVisible != true) {
toolbarBreadcrumb?.expand {
setBreadcrumbNode(group)
}
} else {
// Add breadcrumb
setBreadcrumbNode(group)
}
}
// Assign number of children
refreshNumberOfChildren(group)
// Hide button
initAddButton(group)
invalidateOptionsMenu()
}
private fun setBreadcrumbNode(group: Group?) {
mBreadcrumbAdapter?.apply {
setNode(group)
breadcrumbListView?.scrollToPosition(itemCount -1)
}
}
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) {
if (actionNodeMode == null)
addNodeButtonView?.hideOrShowButtonOnScrollListener(dy)
@@ -594,11 +653,14 @@ class GroupActivity : DatabaseLockActivity(),
val entryVersioned = node as Entry
EntrySelectionHelper.doSpecialAction(intent,
{
EntryActivity.launch(
this@GroupActivity,
database,
entryVersioned.nodeId
)
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
EntryActivity.launch(
this@GroupActivity,
database,
entryVersioned.nodeId,
resultLauncher
)
}
},
{
// Nothing here, a search is simply performed
@@ -788,23 +850,42 @@ class GroupActivity : DatabaseLockActivity(),
finishNodeAction()
when (node.type) {
Type.GROUP -> {
mOldGroupToUpdate = node as Group
GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo())
.show(
supportFragmentManager,
GroupEditDialogFragment.TAG_CREATE_GROUP
)
launchDialogForGroupUpdate(node as Group)
}
Type.ENTRY -> {
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
EntryEditActivity.launchToUpdate(
this@GroupActivity,
database,
(node as Entry).nodeId,
resultLauncher
)
}
}
Type.ENTRY -> EntryEditActivity.launchToUpdate(
this@GroupActivity,
database,
(node as Entry).nodeId
)
}
reloadGroupIfSearch()
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(
database: Database,
nodes: List<Node>
@@ -888,10 +969,15 @@ class GroupActivity : DatabaseLockActivity(),
inflater.inflate(R.menu.database, menu)
if (mDatabaseReadOnly) {
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) {
MenuUtil.defaultMenuInflater(inflater, menu)
} else {
menu.findItem(R.id.menu_merge_database)?.isVisible = false
menu.findItem(R.id.menu_reload_database)?.isVisible = false
}
@@ -984,7 +1070,7 @@ class GroupActivity : DatabaseLockActivity(),
if (!sortMenuEducationPerformed) {
// lockMenuEducationPerformed
val lockButtonView = findViewById<View>(R.id.lock_button_icon)
val lockButtonView = findViewById<View>(R.id.lock_button)
lockButtonView != null
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
lockButtonView,
@@ -1002,7 +1088,7 @@ class GroupActivity : DatabaseLockActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
// TODO change database
return true
}
R.id.menu_search ->
@@ -1012,6 +1098,10 @@ class GroupActivity : DatabaseLockActivity(),
saveDatabase()
return true
}
R.id.menu_merge_database -> {
mergeDatabase()
return true
}
R.id.menu_reload_database -> {
reloadDatabase()
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() {
intent.removeExtra(AUTO_SEARCH_KEY)
if (Intent.ACTION_SEARCH == intent.action) {
@@ -1292,8 +1351,9 @@ class GroupActivity : DatabaseLockActivity(),
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun launchForAutofillResult(activity: Activity,
fun launchForAutofillResult(activity: AppCompatActivity,
database: Database,
activityResultLaunch: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null,
autoSearch: Boolean = false) {
@@ -1303,6 +1363,7 @@ class GroupActivity : DatabaseLockActivity(),
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
activityResultLaunch,
autofillComponent,
searchInfo
)
@@ -1335,11 +1396,12 @@ class GroupActivity : DatabaseLockActivity(),
* Global Launch
* -------------------------
*/
fun launch(activity: Activity,
fun launch(activity: AppCompatActivity,
database: Database,
onValidateSpecialMode: () -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit) {
onLaunchActivitySpecialMode: () -> Unit,
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
EntrySelectionHelper.doSpecialAction(activity.intent,
{
GroupActivity.launch(
@@ -1451,6 +1513,7 @@ class GroupActivity : DatabaseLockActivity(),
// Here no search info found, disable auto search
GroupActivity.launchForAutofillResult(activity,
database,
autofillActivityResultLauncher,
autofillComponent,
searchInfo,
false)

View File

@@ -27,9 +27,12 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
@@ -82,6 +85,9 @@ class IconPickerActivity : DatabaseLockActivity() {
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
addCustomIcon(uri)
}
uploadButton = findViewById(R.id.icon_picker_upload)
@@ -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() {
setResult(Activity.RESULT_OK, Intent().apply {
putExtra(EXTRA_ICON, mIconImage)
@@ -331,30 +329,28 @@ class IconPickerActivity : DatabaseLockActivity() {
companion object {
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
private const val ICON_SELECTED_REQUEST = 15861
private const val EXTRA_ICON = "EXTRA_ICON"
private const val MAX_ICON_SIZE = 5242880
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) {
if (requestCode == ICON_SELECTED_REQUEST) {
if (resultCode == Activity.RESULT_OK) {
listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
fun registerIconSelectionForResult(context: FragmentActivity,
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
}
}
}
fun launch(context: Activity,
previousIcon: IconImage?) {
fun launch(context: FragmentActivity,
previousIcon: IconImage?,
resultLauncher: ActivityResultLauncher<Intent>) {
// Create an instance to return the picker icon
context.startActivityForResult(
Intent(context,
IconPickerActivity::class.java).apply {
resultLauncher.launch(
Intent(context, IconPickerActivity::class.java).apply {
if (previousIcon != null)
putExtra(EXTRA_ICON, previousIcon)
},
ICON_SELECTED_REQUEST)
}
)
}
}
}

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
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.InputMethodManager
import android.widget.*
import android.widget.TextView.OnEditorActionListener
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
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.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import java.io.FileNotFoundException
open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
// Views
private var toolbar: Toolbar? = null
@@ -89,7 +89,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
private lateinit var coordinatorLayout: CoordinatorLayout
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
private var mDefaultDatabase: Boolean = false
private var mDatabaseFileUri: Uri? = null
@@ -98,20 +99,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
private var mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null
private var mPermissionAsked = false
private var mReadOnly: 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?) {
super.onCreate(savedInstanceState)
@@ -133,7 +127,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
infoContainerView = findViewById(R.id.activity_password_info_container)
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
savedInstanceState.getBoolean(KEY_READ_ONLY)
} else {
@@ -142,6 +135,12 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) {
mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri)
}
}
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
passwordView?.setOnEditorActionListener(onEditorActionListener)
@@ -170,9 +169,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
}
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
}
// Init Biometric elements
advancedUnlockFragment = supportFragmentManager
@@ -188,21 +184,30 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
// Listen password checkbox to init advanced unlock and confirmation button
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
advancedUnlockFragment?.checkUnlockAvailability()
mAdvancedUnlockViewModel.checkUnlockAvailability()
enableOrNotTheConfirmationButton()
}
// Observe if default database
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
mDefaultDatabase = isDefaultDatabase
}
// Observe database file change
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
// Force read only if the file does not exists
mForceReadOnly = databaseFile?.let {
val databaseFileNotExists = databaseFile?.let {
!it.databaseFileExists
} ?: true
infoContainerView?.visibility = if (databaseFileNotExists) {
mReadOnly = true
View.VISIBLE
} else {
View.GONE
}
mForceReadOnly = databaseFileNotExists
invalidateOptionsMenu()
// 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
mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
false
else
mAllowAutoOpenBiometricPrompt
mDatabaseFileUri?.let { databaseFileUri ->
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
}
checkPermission()
mDatabaseFileUri?.let { databaseFileUri ->
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
}
mDatabase?.let { database ->
launchGroupActivityIfLoaded(database)
@@ -263,7 +266,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
// Recheck advanced unlock if error
advancedUnlockFragment?.initAdvancedUnlockMode()
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
if (result.isSuccess) {
launchGroupActivityIfLoaded(database)
@@ -311,7 +314,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible
if (mDefaultDatabase) {
databaseFileViewModel.removeDefaultDatabase()
mDatabaseFileViewModel.removeDefaultDatabase()
}
}
}
@@ -344,7 +347,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
}
mDatabaseFileUri?.let {
databaseFileViewModel.checkIfIsDefaultDatabase(it)
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
}
}
@@ -361,7 +364,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
database,
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }
{ onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher
)
}
}
@@ -435,8 +439,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
} else {
// Init Biometric elements
advancedUnlockFragment?.loadDatabase(databaseFileUri,
mAllowAutoOpenBiometricPrompt)
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
}
enableOrNotTheConfirmationButton()
@@ -496,18 +499,15 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
override fun onPause() {
// Reinit locking activity UI variable
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
mAllowAutoOpenBiometricPrompt = true
super.onPause()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
mDatabaseKeyFileUri?.let {
outState.putString(KEY_KEYFILE, it.toString())
}
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
super.onSaveInstanceState(outState)
}
@@ -606,35 +606,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
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
private var performedEductionInProgress = false
private fun launchEducation(menu: Menu) {
@@ -709,45 +680,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
return super.onOptionsItemSelected(item)
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mAllowAutoOpenBiometricPrompt = false
// To get device credential unlock result
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
// To get entry in result
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
var keyFileResult = false
mExternalFileHelper?.let {
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
if (uri != null) {
mDatabaseKeyFileUri = uri
populateKeyFileTextView(uri)
}
}
}
if (!keyFileResult) {
// this block if not a key file response
when (resultCode) {
DatabaseLockActivity.RESULT_EXIT_LOCK -> {
clearCredentialsViews()
closeDatabase()
}
Activity.RESULT_CANCELED -> {
clearCredentialsViews()
}
}
}
}
companion object {
private val TAG = PasswordActivity::class.java.name
@@ -761,10 +693,6 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
private const val KEY_PASSWORD = "password"
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?,
intentBuildLauncher: (Intent) -> Unit) {
@@ -855,15 +783,17 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
@RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class)
fun launchForAutofillResult(activity: Activity,
fun launchForAutofillResult(activity: AppCompatActivity,
databaseFile: Uri,
keyFile: Uri?,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
AutofillHelper.startActivityForAutofillResult(
activity,
intent,
activityResultLauncher,
autofillComponent,
searchInfo)
}
@@ -891,12 +821,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
* Global Launch
* -------------------------
*/
fun launch(activity: Activity,
fun launch(activity: AppCompatActivity,
databaseUri: Uri,
keyFile: Uri?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit) {
onLaunchActivitySpecialMode: () -> Unit,
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
try {
EntrySelectionHelper.doSpecialAction(activity.intent,
@@ -926,6 +857,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PasswordActivity.launchForAutofillResult(activity,
databaseUri, keyFile,
autofillActivityResultLauncher,
autofillComponent,
searchInfo)
onLaunchActivitySpecialMode()

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.Editable
@@ -133,6 +132,18 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
}
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
val dialog = builder.create()
@@ -208,7 +219,11 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
}
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
if ((mMasterPassword == null
|| mMasterPassword!!.isEmpty())
&& (keyFileCheckBox == null
|| !keyFileCheckBox!!.isChecked
|| keyFileSelectionView?.uri == null)) {
error = true
showEmptyPasswordConfirmationDialog()
}
@@ -282,23 +297,6 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri
if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog()
}
}
}
}
}
companion object {
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"

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?) {
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 {
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
const val KEY_ACTION_ID = "KEY_ACTION_ID"
const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
private const val KEY_ACTION_ID = "KEY_ACTION_ID"
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
val bundle = Bundle()

View File

@@ -309,7 +309,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
override fun afterTextChanged(s: Editable?) {
s?.toString()?.let { userString ->
try {
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH))
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
otpSecretContainer?.error = null
} catch (exception: Exception) {
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)

View File

@@ -109,6 +109,12 @@ class EntryEditFragment: DatabaseFragment() {
setOnIconClickListener {
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
}
setOnBackgroundColorClickListener {
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
}
setOnForegroundColorClickListener {
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
}
setOnCustomEditionActionClickListener { field ->
mEntryEditViewModel.requestCustomFieldEdition(field)
}
@@ -158,6 +164,14 @@ class EntryEditFragment: DatabaseFragment() {
templateView.setIcon(iconImage)
}
mEntryEditViewModel.onBackgroundColorSelected.observe(this) { color ->
templateView.setBackgroundColor(color)
}
mEntryEditViewModel.onForegroundColorSelected.observe(this) { color ->
templateView.setForegroundColor(color)
}
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
templateView.setPasswordField(passwordField)
}

View File

@@ -42,7 +42,6 @@ class EntryFragment: DatabaseFragment() {
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
private lateinit var uuidContainerView: View
private lateinit var uuidView: TextView
private lateinit var uuidReferenceView: TextView
private var mClipboardHelper: ClipboardHelper? = null
@@ -88,7 +87,6 @@ class EntryFragment: DatabaseFragment() {
uuidContainerView.apply {
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)
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
@@ -200,7 +198,6 @@ class EntryFragment: DatabaseFragment() {
}
private fun assignUUID(uuid: UUID?) {
uuidView.text = uuid?.toString()
uuidReferenceView.text = UuidUtil.toHexString(uuid)
}

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.activities.fragments
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
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.helpers.EntrySelectionHelper
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.Group
import com.kunzisoft.keepass.database.element.SortNodeEnum
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.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -50,10 +48,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var nodeClickListener: NodeClickListener? = null
private var onScrollListener: OnScrollListener? = null
private var groupRefreshed: GroupRefreshedListener? = null
private var mNodesRecyclerView: RecyclerView? = null
private var mLayoutManager: LinearLayoutManager? = null
private var mAdapter: NodeAdapter? = null
private var mAdapter: NodesAdapter? = null
private val mGroupViewModel: GroupViewModel by activityViewModels()
@@ -74,6 +73,19 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var mRecycleBinEnable: Boolean = false
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() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
@@ -89,12 +101,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
override fun onAttach(context: Context) {
super.onAttach(context)
// TODO Change to ViewModel
try {
nodeClickListener = context as NodeClickListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException(context.toString()
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
+ " must implement " + NodesAdapter.NodeClickCallback::class.java.name)
}
try {
@@ -102,14 +116,24 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} catch (e: ClassCastException) {
onScrollListener = null
// Context menu can be omit
Log.w(TAG, context.toString()
Log.w(
TAG, context.toString()
+ " 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() {
nodeClickListener = null
onScrollListener = null
groupRefreshed = null
super.onDetach()
}
@@ -125,8 +149,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
contextThemed?.let { context ->
database?.let { database ->
mAdapter = NodeAdapter(context, database).apply {
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
mAdapter = NodesAdapter(context, database).apply {
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
override fun onNodeClick(database: Database, node: Node) {
if (nodeActionSelectionMode) {
if (listActionNodes.contains(node)) {
@@ -182,7 +206,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
super.onCreateView(inflater, container, savedInstanceState)
// To apply theme
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?) {
@@ -247,6 +271,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
} else {
notFoundView?.visibility = View.GONE
}
groupRefreshed?.onGroupRefreshed()
}
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
@@ -279,15 +305,17 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
val sortDialogFragment: SortDialogFragment =
if (mRecycleBinEnable) {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context))
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
PreferencesUtil.getRecycleBinBottomSort(context)
)
} else {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context))
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context)
)
}
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
*/
@@ -455,6 +462,10 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
fun onScrolled(dy: Int)
}
interface GroupRefreshedListener {
fun onGroupRefreshed()
}
companion object {
private val TAG = GroupFragment::class.java.name
}

View File

@@ -20,14 +20,16 @@
package com.kunzisoft.keepass.activities.helpers
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import android.view.View
import androidx.annotation.RequiresApi
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
@@ -38,6 +40,10 @@ class ExternalFileHelper {
private var activity: FragmentActivity? = null
private var fragment: Fragment? = null
private var getContentResultLauncher: ActivityResultLauncher<String>? = null
private var openDocumentResultLauncher: ActivityResultLauncher<Array<String>>? = null
private var createDocumentResultLauncher: ActivityResultLauncher<String>? = null
constructor(context: FragmentActivity) {
this.activity = context
this.fragment = null
@@ -48,94 +54,81 @@ class ExternalFileHelper {
this.fragment = context
}
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
val resultCallback = ActivityResultCallback<Uri> { result ->
result?.let { uri ->
UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri)
}
}
getContentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
GetContent(),
resultCallback
)
} else {
activity?.registerForActivityResult(
GetContent(),
resultCallback
)
}
openDocumentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
OpenDocument(),
resultCallback
)
} else {
activity?.registerForActivityResult(
OpenDocument(),
resultCallback
)
}
}
fun buildCreateDocument(typeString: String = "application/octet-stream",
onFileCreated: (fileCreated: Uri?)->Unit) {
val resultCallback = ActivityResultCallback<Uri> { result ->
onFileCreated.invoke(result)
}
createDocumentResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
CreateDocument(typeString),
resultCallback
)
} else {
activity?.registerForActivityResult(
CreateDocument(typeString),
resultCallback
)
}
}
fun openDocument(getContent: Boolean = false,
typeString: String = "*/*") {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
if (getContent) {
openActivityWithActionGetContent(typeString)
} else {
openActivityWithActionOpenDocument(typeString)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to open document", e)
showFileManagerDialogFragment()
try {
if (getContent) {
getContentResultLauncher?.launch(typeString)
} else {
openDocumentResultLauncher?.launch(arrayOf(typeString))
}
} else {
} catch (e: Exception) {
Log.e(TAG, "Unable to open document", e)
showFileManagerDialogFragment()
}
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun openActivityWithActionOpenDocument(typeString: String) {
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
fun createDocument(titleString: String) {
try {
createDocumentResultLauncher?.launch(titleString)
} catch (e: Exception) {
Log.e(TAG, "Unable to create document", e)
showFileManagerDialogFragment()
}
if (fragment != null)
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
else
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun openActivityWithActionGetContent(typeString: String) {
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
else
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param onFileSelected Callback retrieve from data
* @return true if requestCode was captured, false elsewhere
*/
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
when (requestCode) {
FILE_BROWSE -> {
if (resultCode == RESULT_OK) {
val filename = data?.dataString
var keyUri: Uri? = null
if (filename != null) {
keyUri = UriUtil.parse(filename)
}
onFileSelected?.invoke(keyUri)
}
return true
}
GET_CONTENT, OPEN_DOC -> {
if (resultCode == RESULT_OK) {
if (data != null) {
val uri = data.data
if (uri != null) {
UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri)
}
}
}
return true
}
}
return false
}
/**
@@ -155,62 +148,50 @@ class ExternalFileHelper {
}
}
fun createDocument(titleString: String,
typeString: String = "application/octet-stream"): Int? {
val idCode = getUnusedCreateFileRequestCode()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
putExtra(Intent.EXTRA_TITLE, titleString)
class OpenDocument : ActivityResultContracts.OpenDocument() {
@SuppressLint("InlinedApi")
override fun createIntent(context: Context, input: Array<out String>): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
if (fragment != null)
fragment?.startActivityForResult(intent, idCode)
else
activity?.startActivityForResult(intent, idCode)
return idCode
} catch (e: Exception) {
Log.e(TAG, "Unable to create document", e)
showFileManagerDialogFragment()
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
} else {
showFileManagerDialogFragment()
}
return null
}
/**
* To use in onActivityResultCallback in Fragment or Activity
* @param onFileCreated Callback retrieve from data
* @return true if requestCode was captured, false elsewhere
*/
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
onFileCreated: (fileCreated: Uri?)->Unit) {
// Retrieve the created URI from the file manager
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) {
onFileCreated.invoke(data?.data)
fileRequestCodes.remove(requestCode)
class GetContent : ActivityResultContracts.GetContent() {
@SuppressLint("InlinedApi")
override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
}
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
}
}
}
companion object {
private const val TAG = "OpenFileHelper"
private const val GET_CONTENT = 25745
private const val OPEN_DOC = 25845
private const val FILE_BROWSE = 25645
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
private var fileRequestCodes = ArrayList<Int>()
private fun getUnusedCreateFileRequestCode(): Int {
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
fileRequestCodes.add(newCreateFileRequestCode)
return newCreateFileRequestCode
}
@SuppressLint("InlinedApi")
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
typeString: String = "application/octet-stream"): Boolean {
@@ -231,7 +212,7 @@ class ExternalFileHelper {
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
externalFileHelper?.let { fileHelper ->
setOnClickListener {
fileHelper.openDocument()
fileHelper.openDocument(false)
}
setOnLongClickListener {
fileHelper.openDocument(true)

View File

@@ -62,6 +62,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
private var mExitLock: Boolean = false
protected var mDatabaseReadOnly: Boolean = true
protected var mMergeDataAllowed: Boolean = false
private var mAutoSaveEnable: Boolean = true
protected var mIconDrawableFactory: IconDrawableFactory? = null
@@ -87,8 +88,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(save)
}
mDatabaseViewModel.mergeDatabase.observe(this) { fixDuplicateUuid ->
mDatabaseTaskProvider?.startDatabaseMerge(fixDuplicateUuid)
}
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
}
}
mDatabaseViewModel.saveName.observe(this) {
@@ -100,7 +107,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
mDatabaseViewModel.saveDefaultUsername.observe(this) {
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
}
mDatabaseViewModel.saveColor.observe(this) {
@@ -180,8 +187,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
closeDatabase(database)
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
// Add onActivityForResult response
setResult(RESULT_EXIT_LOCK)
mExitLock = true
closeOptionsMenu()
finish()
}
@@ -198,6 +204,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
}
mDatabaseReadOnly = database.isReadOnly
mMergeDataAllowed = database.isMergeDataAllowed()
mIconDrawableFactory = database.iconDrawableFactory
checkRegister()
@@ -213,6 +220,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
// Reload the current activity
if (result.isSuccess) {
@@ -255,8 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(true)
}
fun mergeDatabase() {
mDatabaseTaskProvider?.startDatabaseMerge(false)
}
fun reloadDatabase() {
mDatabaseTaskProvider?.startDatabaseReload(false)
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
mDatabaseTaskProvider?.startDatabaseReload(false)
}
}
fun createEntry(newEntry: Entry,
@@ -353,14 +367,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_EXIT_LOCK) {
mExitLock = true
lockAndExit()
}
}
private fun checkRegister() {
// If in ave or registration mode, don't allow read only
if ((mSpecialMode == SpecialMode.SAVE
@@ -440,8 +446,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
const val TAG = "LockingActivity"
const val RESULT_EXIT_LOCK = 1450
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
const val TIMEOUT_ENABLE_KEY_DEFAULT = true

View File

@@ -39,7 +39,11 @@ object Stylish {
*/
fun load(context: Context) {
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
themeString = PreferencesUtil.getStyle(context)
try {
themeString = PreferencesUtil.getStyle(context)
} catch (e: Exception) {
Log.e("Stylish", "Unable to get preference style", e)
}
}
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {

View File

@@ -28,7 +28,7 @@ import android.util.Log
import android.view.WindowManager
import androidx.annotation.StyleRes
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
@@ -89,8 +89,8 @@ abstract class StylishActivity : AppCompatActivity() {
super.onResume()
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|| DATABASE_APPEARANCE_PREFERENCE_CHANGED) {
DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
|| DATABASE_PREFERENCE_CHANGED) {
DATABASE_PREFERENCE_CHANGED = false
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
recreateActivity()
}

View File

@@ -23,12 +23,12 @@ import android.content.Context
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import androidx.annotation.StyleRes
import androidx.fragment.app.Fragment
import androidx.appcompat.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StyleRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.fragment.app.Fragment
abstract class StylishFragment : Fragment() {
@@ -42,7 +42,6 @@ abstract class StylishFragment : Fragment() {
contextThemed = ContextThemeWrapper(context, themeId)
}
@Suppress("DEPRECATION")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// To fix status bar color
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -58,6 +57,7 @@ abstract class StylishFragment : Fragment() {
try {
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
if (taWindowStatusLight?.getBoolean(0, false) == true) {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
taWindowStatusLight?.recycle()

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.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.ColorInt
@@ -34,6 +33,7 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
@@ -55,9 +55,9 @@ import java.util.*
* Create node list adapter with contextMenu or not
* @param context Context to use
*/
class NodeAdapter (private val context: Context,
private val database: Database)
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
class NodesAdapter (private val context: Context,
private val database: Database)
: RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
private val mNodeSortedListCallback: NodeSortedListCallback
@@ -79,6 +79,8 @@ class NodeAdapter (private val context: Context,
private var mShowOTP: Boolean = false
private var mShowUUID: Boolean = false
private var mEntryFilters = arrayOf<Group.ChildFilter>()
private var mOldVirtualGroup = false
private var mVirtualGroup = false
private var mActionNodesList = LinkedList<Node>()
private var mNodeClickCallback: NodeClickCallback? = null
@@ -87,9 +89,15 @@ class NodeAdapter (private val context: Context,
@ColorInt
private val mContentSelectionColor: Int
@ColorInt
private val mIconGroupColor: Int
private val mTextColorPrimary: Int
@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
@@ -110,12 +118,24 @@ class NodeAdapter (private val context: Context,
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
// Retrieve the color to tint the icon
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()
// In two times to fix bug compilation
// To get text color
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()
// 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() {
@@ -145,6 +165,8 @@ class NodeAdapter (private val context: Context,
* Rebuild the list by clear and build children from the group
*/
fun rebuildList(group: Group) {
mOldVirtualGroup = mVirtualGroup
mVirtualGroup = group.isVirtual
assignPreferences()
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
}
@@ -155,14 +177,19 @@ class NodeAdapter (private val context: Context,
}
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
if (mOldVirtualGroup != mVirtualGroup)
return false
var typeContentTheSame = true
if (oldItem is Entry && newItem is Entry) {
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
&& oldItem.username == newItem.username
&& oldItem.backgroundColor == newItem.backgroundColor
&& oldItem.foregroundColor == newItem.foregroundColor
&& oldItem.getOtpElement() == newItem.getOtpElement()
&& oldItem.containsAttachment() == newItem.containsAttachment()
} else if (oldItem is Group && newItem is Group) {
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
&& oldItem.notes == newItem.notes
}
return typeContentTheSame
&& oldItem.nodeId == newItem.nodeId
@@ -327,8 +354,8 @@ class NodeAdapter (private val context: Context,
val iconColor = if (holder.container.isSelected)
mContentSelectionColor
else when (subNode.type) {
Type.GROUP -> mIconGroupColor
Type.ENTRY -> mIconEntryColor
Type.GROUP -> mTextColorPrimary
Type.ENTRY -> mTextColor
}
holder.imageIdentifier?.setColorFilter(iconColor)
holder.icon.apply {
@@ -348,14 +375,24 @@ class NodeAdapter (private val context: Context,
}
// Add meta text to show UUID
holder.meta.apply {
if (mShowUUID) {
text = subNode.nodeId.toString()
val nodeId = subNode.nodeId?.toVisualString()
if (mShowUUID && nodeId != null) {
text = nodeId
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
visibility = View.VISIBLE
} else {
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
if (subNode.type == Type.ENTRY) {
@@ -398,6 +435,50 @@ class NodeAdapter (private val context: Context,
holder.attachmentIcon?.visibility =
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)
}
@@ -430,15 +511,16 @@ class NodeAdapter (private val context: Context,
OtpType.HOTP -> {
holder?.otpProgress?.apply {
max = 100
progress = 100
setProgressCompat(100, true)
}
}
OtpType.TOTP -> {
holder?.otpProgress?.apply {
max = otpElement.period
progress = otpElement.secondsRemaining
setProgressCompat(otpElement.secondsRemaining, true)
}
}
null -> {}
}
holder?.otpToken?.apply {
text = otpElement?.token
@@ -497,8 +579,9 @@ class NodeAdapter (private val context: Context,
var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView? = itemView.findViewById(R.id.node_subtext)
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 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 otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
@@ -506,6 +589,6 @@ class NodeAdapter (private val context: Context,
}
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.database.Cursor
import android.database.MatrixCursor
import android.graphics.Color
import android.provider.BaseColumns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.cursoradapter.widget.CursorAdapter
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.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.strikeOut
import java.util.*
class SearchEntryCursorAdapter(private val context: Context,
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(
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.textViewTitle = view.findViewById(R.id.entry_text)
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext)
viewHolder.textViewPath = view.findViewById(R.id.entry_path)
view.tag = viewHolder
return view
@@ -101,32 +104,16 @@ class SearchEntryCursorAdapter(private val context: Context,
visibility = if (text.isEmpty()) View.GONE else View.VISIBLE
strikeOut(currentEntry.isCurrentlyExpires)
}
viewHolder.textViewPath?.apply {
text = currentEntry.getPathString()
}
}
}
private fun getEntryFrom(cursor: Cursor): Entry? {
return database.createEntry()?.apply {
entryKDB?.let { entryKDB ->
(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)
}
)
}
}
val entryCursor = cursor as EntryCursor
return database.getEntryById(entryCursor.getNodeId())
}
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
@@ -134,14 +121,7 @@ class SearchEntryCursorAdapter(private val context: Context,
}
private fun searchEntries(context: Context, query: String): Cursor? {
var cursorKDB: EntryCursorKDB? = null
var cursorKDBX: EntryCursorKDBX? = null
if (database.type == DatabaseKDB.TYPE)
cursorKDB = EntryCursorKDB()
if (database.type == DatabaseKDBX.TYPE)
cursorKDBX = EntryCursorKDBX()
val cursor = EntryCursor()
val searchGroup = database.createVirtualGroupFromSearch(query,
mOmitBackup,
SearchHelper.MAX_SEARCH_ENTRY)
@@ -149,17 +129,11 @@ class SearchEntryCursorAdapter(private val context: Context,
// Search in hide entries but not meta-stream
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
database.startManageEntry(entry)
entry.entryKDB?.let {
cursorKDB?.addEntry(it)
}
entry.entryKDBX?.let {
cursorKDBX?.addEntry(it)
}
cursor.addEntry(entry)
database.stopManageEntry(entry)
}
}
return cursorKDB ?: cursorKDBX
return cursor
}
fun getEntryFromPosition(position: Int): Entry? {
@@ -176,5 +150,37 @@ class SearchEntryCursorAdapter(private val context: Context,
var imageViewIcon: ImageView? = null
var textViewTitle: 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.TextView
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.TemplateField
import com.kunzisoft.keepass.icons.IconDrawableFactory
class TemplatesSelectorAdapter(private val context: Context,
private val iconDrawableFactory: IconDrawableFactory?,
private var templates: List<Template>): BaseAdapter() {
class TemplatesSelectorAdapter(
context: Context,
private var templates: List<Template>): BaseAdapter() {
var iconDrawableFactory: IconDrawableFactory? = null
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var mIconColor = Color.BLACK
private var mTextColor = Color.BLACK
init {
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
mIconColor = taIconColor.getColor(0, Color.BLACK)
taIconColor.recycle()
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
mTextColor = taTextColor.getColor(0, Color.BLACK)
taTextColor.recycle()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
@@ -36,6 +36,7 @@ class TemplatesSelectorAdapter(private val context: Context,
if (templateView == null) {
holder = TemplateSelectorViewHolder()
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.name = templateView?.findViewById(R.id.template_name)
templateView?.tag = holder
@@ -43,10 +44,15 @@ class TemplatesSelectorAdapter(private val context: Context,
holder = templateView.tag as TemplateSelectorViewHolder
}
holder.background?.setBackgroundColor(template.backgroundColor ?: Color.TRANSPARENT)
val textColor = template.foregroundColor ?: mTextColor
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!!
}
@@ -64,6 +70,7 @@ class TemplatesSelectorAdapter(private val context: Context,
}
inner class TemplateSelectorViewHolder {
var background: View? = null
var icon: ImageView? = null
var name: TextView? = null
}

View File

@@ -4,4 +4,4 @@ import android.app.assist.AssistStructure
import android.view.inputmethod.InlineSuggestionsRequest
data class AutofillComponent(val assistStructure: AssistStructure,
val inlineSuggestionsRequest: InlineSuggestionsRequest?)
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)

View File

@@ -25,7 +25,6 @@ import android.app.PendingIntent
import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.graphics.BlendMode
import android.graphics.drawable.Icon
import android.os.Build
@@ -35,11 +34,13 @@ import android.service.autofill.InlinePresentation
import android.util.Log
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.RemoteViews
import android.widget.Toast
import android.widget.inline.InlinePresentationSpec
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat
@@ -49,21 +50,19 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.SpecialMode
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import kotlin.collections.ArrayList
import com.kunzisoft.keepass.utils.LOCK_ACTION
@RequiresApi(api = Build.VERSION_CODES.O)
object AutofillHelper {
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
@@ -112,7 +111,7 @@ object AutofillHelper {
database: Database,
entryInfo: EntryInfo,
struct: StructureParser.Result,
inlinePresentation: InlinePresentation?): Dataset? {
additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
val title = makeEntryTitle(entryInfo)
val views = newRemoteViews(context, database, title, entryInfo.icon)
val builder = Dataset.Builder(views)
@@ -201,11 +200,7 @@ object AutofillHelper {
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlinePresentation?.let {
builder.setInlinePresentation(it)
}
}
additionalBuild?.invoke(builder)
return try {
builder.build()
@@ -236,40 +231,51 @@ object AutofillHelper {
@SuppressLint("RestrictedApi")
private fun buildInlinePresentationForEntry(context: Context,
database: Database,
inlineSuggestionsRequest: InlineSuggestionsRequest,
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
positionItem: Int,
entryInfo: EntryInfo): InlinePresentation? {
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
if (positionItem <= maxSuggestion - 1
&& inlinePresentationSpecs.size > positionItem) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
if (positionItem <= maxSuggestion - 1
&& inlinePresentationSpecs.size > positionItem
) {
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null
// Make sure that the IME spec claims support for v1 UI template.
val imeStyle = inlinePresentationSpec.style
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
return null
// Build the content for IME UI
val pendingIntent = PendingIntent.getActivity(context,
// Build the content for IME UI
val pendingIntent = PendingIntent.getActivity(
context,
0,
Intent(context, AutofillSettingsActivity::class.java),
0)
return InlinePresentation(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
)
return InlinePresentation(
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
setTitle(entryInfo.title)
setSubtitle(entryInfo.username)
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
setStartIcon(
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST)
})
}
}.build().slice, inlinePresentationSpec, false)
}.build().slice, inlinePresentationSpec, false
)
}
}
return null
}
@@ -299,7 +305,7 @@ object AutofillHelper {
database: Database,
entriesInfo: List<EntryInfo>,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
val responseBuilder = FillResponse.Builder()
// Add Header
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -320,7 +326,7 @@ object AutofillHelper {
// Add inline suggestion for new IME and dataset
var numberInlineSuggestions = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
@@ -332,14 +338,19 @@ object AutofillHelper {
}
entriesInfo.forEachIndexed { _, entry ->
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let {
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry)
}
if (numberInlineSuggestions > 0
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& compatInlineSuggestionsRequest != null) {
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
buildInlinePresentationForEntry(context, database,
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
)?.let { inlinePresentation ->
builder.setInlinePresentation(inlinePresentation)
}
})
} else {
null
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
}
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
}
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
@@ -351,14 +362,14 @@ object AutofillHelper {
}
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
searchInfo, inlineSuggestionsRequest)
searchInfo, compatInlineSuggestionsRequest)
parseResult.allAutofillIds().let { autofillIds ->
autofillIds.forEach { id ->
val builder = Dataset.Builder(manualSelectionView)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.let {
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
inlinePresentation?.let {
@@ -403,11 +414,11 @@ object AutofillHelper {
StructureParser(structure).parse()?.let { result ->
// New Response
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
if (inlineSuggestionsRequest != null) {
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<CompatInlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
if (compatInlineSuggestionsRequest != null) {
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
}
buildResponse(activity, database, entriesInfo, result, inlineSuggestionsRequest)
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
} else {
buildResponse(activity, database, entriesInfo, result, null)
}
@@ -427,37 +438,44 @@ object AutofillHelper {
}
}
fun buildActivityResultLauncher(activity: AppCompatActivity,
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
// Utility method to loop and close each activity with return data
if (it.resultCode == Activity.RESULT_OK) {
activity.setResult(it.resultCode, it.data)
}
if (it.resultCode == Activity.RESULT_CANCELED) {
activity.setResult(Activity.RESULT_CANCELED)
}
activity.finish()
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
// Close the database
activity.sendBroadcast(Intent(LOCK_ACTION))
}
}
}
/**
* Utility method to start an activity with an Autofill for result
*/
fun startActivityForAutofillResult(activity: Activity,
fun startActivityForAutofillResult(activity: AppCompatActivity,
intent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
autofillComponent.inlineSuggestionsRequest?.let {
autofillComponent.compatInlineSuggestionsRequest?.let {
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
}
}
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
}
/**
* Utility method to loop and close each activity with return data
*/
fun onActivityResultSetResultAndFinish(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
activity.setResult(resultCode, data)
}
if (resultCode == Activity.RESULT_CANCELED) {
activity.setResult(Activity.RESULT_CANCELED)
}
activity.finish()
}
activityResultLauncher?.launch(intent)
}
}

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
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
request.inlineSuggestionsRequest
CompatInlineSuggestionsRequest(request)
} else {
null
}
@@ -127,7 +127,7 @@ class KeeAutofillService : AutofillService() {
private fun launchSelection(database: Database?,
searchInfo: SearchInfo,
parseResult: StructureParser.Result,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) {
SearchHelper.checkAutoSearchInfo(this,
database,
@@ -155,7 +155,7 @@ class KeeAutofillService : AutofillService() {
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
database: Database?,
searchInfo: SearchInfo,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) {
parseResult.allAutofillIds().let { autofillIds ->
if (autofillIds.isNotEmpty()) {
@@ -249,7 +249,7 @@ class KeeAutofillService : AutofillService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& autofillInlineSuggestionsEnabled) {
var inlinePresentation: InlinePresentation? = null
inlineSuggestionsRequest?.let {
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
if (inlineSuggestionsRequest.maxSuggestionCount > 0
&& inlinePresentationSpecs.size > 0) {
@@ -262,9 +262,13 @@ class KeeAutofillService : AutofillService() {
inlinePresentation = InlinePresentation(
InlineSuggestionUi.newContentBuilder(
PendingIntent.getActivity(this,
0,
Intent(this, AutofillSettingsActivity::class.java),
0)
0,
Intent(this, AutofillSettingsActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
})
).apply {
setContentDescription(getString(R.string.autofill_sign_in_prompt))
setTitle(getString(R.string.autofill_sign_in_prompt))
@@ -277,8 +281,9 @@ class KeeAutofillService : AutofillService() {
}
// Build response
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
} else {
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
}
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
callback.onSuccess(responseBuilder.build())
}
}

View File

@@ -272,12 +272,12 @@ class StructureParser(private val structure: AssistStructure) {
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
val nodHtml = node.htmlInfo
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
when (nodHtml?.tag?.lowercase(Locale.ENGLISH)) {
"input" -> {
nodHtml.attributes?.forEach { pairAttribute ->
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
when (pairAttribute.first.lowercase(Locale.ENGLISH)) {
"type" -> {
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
"tel", "email" -> {
result?.usernameId = autofillId
result?.usernameValue = node.autofillValue

View File

@@ -19,6 +19,7 @@
*/
package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -27,9 +28,11 @@ import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R
@@ -39,6 +42,7 @@ import com.kunzisoft.keepass.database.exception.IODatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -59,9 +63,12 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
/**
* Manage setting to auto open biometric prompt
*/
private var mAutoOpenPrompt: Boolean = false
private var mAutoOpenPrompt: Boolean
get() {
return field && mAutoOpenPromptEnabled
return mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt && mAutoOpenPromptEnabled
}
set(value) {
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = value
}
// Variable to check if the prompt can be open (if the right activity is currently shown)
@@ -72,6 +79,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
// Only to fix multiple fingerprint menu #332
private var mAllowAdvancedUnlockMenu = false
private var mAddBiometricMenuInProgress = false
@@ -79,6 +88,15 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
// Only keep connection when we request a device credential activity
private var keepConnection = false
private var mDeviceCredentialResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
// To wait resume
if (keepConnection) {
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = result.resultCode == Activity.RESULT_OK
}
keepConnection = false
}
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -97,10 +115,21 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
setHasOptionsMenu(true)
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
initAdvancedUnlockMode()
}
mAdvancedUnlockViewModel.onUnlockAvailabilityCheckRequested.observe(this) {
checkUnlockAvailability()
}
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
onDatabaseLoaded(it)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -114,17 +143,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
return rootView
}
private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
private var activityResult: ActivityResult? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// To wait resume
if (keepConnection) {
activityResult = ActivityResult(requestCode, resultCode, data)
}
keepConnection = false
}
override fun onResume() {
super.onResume()
context?.let {
@@ -154,32 +172,38 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
return super.onOptionsItemSelected(item)
}
fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) {
private fun onDatabaseLoaded(databaseUri: Uri?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// To get device credential unlock result, only if same database uri
if (databaseUri != null
&& mAdvancedUnlockEnabled) {
activityResult?.let {
val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded
deviceCredentialAuthSucceeded?.let {
if (databaseUri == databaseFileUri) {
advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode)
if (deviceCredentialAuthSucceeded == true) {
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
}
} else {
disconnect()
}
} ?: run {
this.mAutoOpenPrompt = autoOpenPrompt
connect(databaseUri)
if (databaseUri != databaseFileUri) {
connect(databaseUri)
}
}
} else {
disconnect()
}
activityResult = null
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
}
}
/**
* Check unlock availability and change the current mode depending of device's state
*/
fun checkUnlockAvailability() {
private fun checkUnlockAvailability() {
context?.let { context ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
allowOpenBiometricPrompt = true
@@ -317,7 +341,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
if (cryptoPrompt.isDeviceCredentialOperation)
keepConnection = true
try {
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt)
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
mDeviceCredentialResultLauncher)
} catch (e: Exception) {
Log.e(TAG, "Unable to open advanced unlock prompt", e)
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
@@ -369,8 +394,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} ?: throw Exception("AdvancedUnlockManager not initialized")
}
@Synchronized
fun initAdvancedUnlockMode() {
private fun initAdvancedUnlockMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mAllowAdvancedUnlockMenu = false
try {
@@ -444,6 +468,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
@RequiresApi(Build.VERSION_CODES.M)
fun deleteEncryptedDatabaseKey() {
mAllowAdvancedUnlockMenu = false
advancedUnlockManager?.closeBiometricPrompt()
databaseFileUri?.let { databaseUri ->
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
@@ -516,6 +541,11 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onUnrecoverableKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onInvalidKeyException(e: Exception) {
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)

View File

@@ -19,15 +19,18 @@
*/
package com.kunzisoft.keepass.biometric
import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.*
@@ -35,6 +38,7 @@ import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.settings.PreferencesUtil
import java.security.KeyStore
import java.security.UnrecoverableKeyException
@@ -136,18 +140,24 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
// and the constrains (purposes) in the constructor of the Builder
keyGenerator?.init(
KeyGenParameterSpec.Builder(
ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
ADVANCED_UNLOCK_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.apply {
// Require the user to authenticate with a fingerprint to authorize every use
// of the key, don't use it for device credential because it's the user authentication
.apply {
if (biometricUnlockEnable) {
setUserAuthenticationRequired(true)
}
if (biometricUnlockEnable) {
setUserAuthenticationRequired(true)
}
.build())
// To store in the security chip
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& retrieveContext().packageManager.hasSystemFeature(
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
setIsStrongBoxBacked(true)
}
}
.build())
keyGenerator?.generateKey()
}
} catch (e: Exception) {
@@ -164,8 +174,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
return null
}
fun initEncryptData(actionIfCypherInit
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
initEncryptData(actionIfCypherInit, true)
}
private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean) {
if (!isKeyManagerInitialized) {
return
}
@@ -185,10 +199,15 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initEncryptData(actionIfCypherInit, false)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize encrypt data", e)
advancedUnlockCallback?.onGenericException(e)
@@ -214,8 +233,14 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
}
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
fun initDecryptData(ivSpecValue: String,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
initDecryptData(ivSpecValue, actionIfCypherInit, true)
}
private fun initDecryptData(ivSpecValue: String,
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
firstLaunch: Boolean = true) {
if (!isKeyManagerInitialized) {
return
}
@@ -239,10 +264,20 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
deleteKeystoreKey()
if (firstLaunch) {
deleteKeystoreKey()
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
}
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
if (firstLaunch) {
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
} else {
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to initialize decrypt data", e)
advancedUnlockCallback?.onGenericException(e)
@@ -278,9 +313,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
}
@Suppress("DEPRECATION")
@Synchronized
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
) {
// Init advanced unlock prompt
if (biometricPrompt == null) {
biometricPrompt = BiometricPrompt(retrieveContext(),
@@ -311,20 +346,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
else if (cryptoPrompt.isDeviceCredentialOperation) {
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
retrieveContext().startActivityForResult(
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription),
REQUEST_DEVICE_CREDENTIAL)
}
}
@Synchronized
fun onActivityResult(requestCode: Int, resultCode: Int) {
if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
if (resultCode == Activity.RESULT_OK) {
advancedUnlockCallback?.onAuthenticationSucceeded()
} else {
advancedUnlockCallback?.onAuthenticationFailed()
}
@Suppress("DEPRECATION")
deviceCredentialResultLauncher.launch(
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
)
}
}
@@ -333,6 +358,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
}
interface AdvancedUnlockErrorCallback {
fun onUnrecoverableKeyException(e: Exception)
fun onInvalidKeyException(e: Exception)
fun onGenericException(e: Exception)
}
@@ -355,8 +381,6 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val REQUEST_DEVICE_CREDENTIAL = 556
@RequiresApi(api = Build.VERSION_CODES.M)
fun canAuthenticate(context: Context): Int {
return try {
@@ -449,6 +473,10 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
override fun handleDecryptedResult(decryptedValue: String) {}
override fun onUnrecoverableKeyException(e: Exception) {
advancedCallback.onUnrecoverableKeyException(e)
}
override fun onInvalidKeyException(e: Exception) {
advancedCallback.onInvalidKeyException(e)
}
@@ -460,6 +488,33 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
deleteKeystoreKey()
}
}
fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
deleteEntryKeyInKeystoreForBiometric(
activity,
object : AdvancedUnlockErrorCallback {
fun showException(e: Exception) {
Toast.makeText(activity,
activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
Toast.LENGTH_SHORT).show()
}
override fun onUnrecoverableKeyException(e: Exception) {
showException(e)
}
override fun onInvalidKeyException(e: Exception) {
showException(e)
}
override fun onGenericException(e: Exception) {
showException(e)
}
})
}
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
}
}
}

View File

@@ -43,7 +43,7 @@ open class AssignPasswordInDatabaseRunnable (
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream)
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
} catch (e: Exception) {
erase(mBackupKey)
setError(e)

View File

@@ -27,6 +27,7 @@ import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
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_NODES_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_RELOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
@@ -354,6 +356,13 @@ class DatabaseTaskProvider {
, 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) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
@@ -361,6 +370,19 @@ class DatabaseTaskProvider {
, 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,
mainCredential: MainCredential) {

View File

@@ -60,7 +60,6 @@ class LoadDatabaseRunnable(private val context: Context,
{ memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
LoadedKey.generateNewCipherKey(),
mFixDuplicateUUID,
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 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
@@ -35,23 +34,18 @@ class ReloadDatabaseRunnable(private val context: Context,
private val mLoadDatabaseResult: ((Result) -> Unit)?)
: ActionRunnable() {
private var tempCipherKey: LoadedKey? = null
override fun onStartRun() {
tempCipherKey = mDatabase.binaryCache.loadedCipherKey
// Clear before we load
mDatabase.clear(UriUtil.getBinaryDir(context))
mDatabase.clearIndexesAndBinaries(UriUtil.getBinaryDir(context))
mDatabase.wasReloaded = true
}
override fun onActionRun() {
try {
mDatabase.reloadData(context.contentResolver,
UriUtil.getBinaryDir(context),
{ memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
},
tempCipherKey ?: LoadedKey.generateNewCipherKey(),
progressTaskUpdater)
} catch (e: LoadDatabaseException) {
setError(e)
@@ -61,7 +55,6 @@ class ReloadDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context)
} else {
tempCipherKey = null
mDatabase.clearAndClose(context)
}
}

View File

@@ -34,6 +34,7 @@ open class SaveDatabaseRunnable(protected var context: Context,
override fun onStartRun() {}
override fun onActionRun() {
database.checkVersion()
if (saveDatabase && result.isSuccess) {
try {
database.saveData(context.contentResolver)

View File

@@ -36,7 +36,11 @@ abstract class ActionNodeDatabaseRunnable(
abstract fun nodeAction()
override fun onStartRun() {
nodeAction()
try {
nodeAction()
} catch (e: Exception) {
setError(e)
}
super.onStartRun()
}

View File

@@ -40,7 +40,7 @@ class DeleteNodesRunnable(context: Context,
foreachNode@ for(nodeToDelete in mNodesToDelete) {
mOldParent = nodeToDelete.parent
mOldParent?.touch(modified = false, touchParents = true)
nodeToDelete.touch(modified = true, touchParents = true)
when (nodeToDelete.type) {
Type.GROUP -> {
@@ -50,9 +50,9 @@ class DeleteNodesRunnable(context: Context,
// Remove Node from parent
mCanRecycle = database.canRecycle(groupToDelete)
if (mCanRecycle) {
groupToDelete.touch(modified = false, touchParents = true)
database.recycle(groupToDelete, context.resources)
groupToDelete.setPreviousParentGroup(mOldParent)
groupToDelete.touch(modified = true, touchParents = true)
} else {
database.deleteGroup(groupToDelete)
}
@@ -64,9 +64,9 @@ class DeleteNodesRunnable(context: Context,
// Remove Node from parent
mCanRecycle = database.canRecycle(entryToDelete)
if (mCanRecycle) {
entryToDelete.touch(modified = false, touchParents = true)
database.recycle(entryToDelete, context.resources)
entryToDelete.setPreviousParentGroup(mOldParent)
entryToDelete.touch(modified = true, touchParents = true)
} else {
database.deleteEntry(entryToDelete)
}

View File

@@ -43,6 +43,7 @@ class MoveNodesRunnable constructor(
foreachNode@ for(nodeToMove in mNodesToMove) {
// Move node in new parent
mOldParent = nodeToMove.parent
nodeToMove.touch(modified = true, touchParents = true)
when (nodeToMove.type) {
Type.GROUP -> {
@@ -52,9 +53,9 @@ class MoveNodesRunnable constructor(
// and if not in the current group
&& groupToMove != mNewParent
&& !mNewParent.isContainedIn(groupToMove)) {
groupToMove.touch(modified = true, touchParents = true)
database.moveGroupTo(groupToMove, mNewParent)
groupToMove.setPreviousParentGroup(mOldParent)
groupToMove.touch(modified = true, touchParents = true)
} else {
// Only finish thread
setError(MoveGroupDatabaseException())
@@ -67,9 +68,9 @@ class MoveNodesRunnable constructor(
if (mOldParent != mNewParent
// and root can contains entry
&& (mNewParent != database.rootGroup || database.rootCanContainsEntry())) {
entryToMove.touch(modified = true, touchParents = true)
database.moveEntryTo(entryToMove, mNewParent)
entryToMove.setPreviousParentGroup(mOldParent)
entryToMove.touch(modified = true, touchParents = true)
} else {
// Only finish thread
setError(MoveEntryDatabaseException())

View File

@@ -42,6 +42,9 @@ class UpdateGroupRunnable constructor(
// Update group with new values
mNewGroup.touch(modified = true, touchParents = true)
if (database.rootGroup == mOldGroup) {
database.rootGroup = mNewGroup
}
// Only change data in index
database.updateGroup(mNewGroup)
}
@@ -50,6 +53,9 @@ class UpdateGroupRunnable constructor(
override fun nodeFinish(): ActionNodesValues {
if (!result.isSuccess) {
// If we fail to save, back out changes to global structure
if (database.rootGroup == mNewGroup) {
database.rootGroup = 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.Context
import android.content.res.Resources
import android.graphics.Color
import android.net.Uri
import android.os.Build
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.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.BinaryCache
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.DatabaseKDB
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.output.DatabaseOutputKDB
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.SearchParameters
import com.kunzisoft.keepass.icons.IconDrawableFactory
@@ -94,6 +94,8 @@ class Database {
*/
var wasReloaded = false
var dataModifiedSinceLastLoading = false
var loadTimestamp: Long? = null
private set
@@ -112,7 +114,7 @@ class Database {
private val iconsManager: IconsManager
get() {
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager()
}
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
@@ -130,7 +132,7 @@ class Database {
return iconsManager.doForEachCustomIcon(action)
}
fun getCustomIcon(iconId: UUID): IconImageCustom {
fun getCustomIcon(iconId: UUID): IconImageCustom? {
return iconsManager.getIcon(iconId)
}
@@ -144,11 +146,12 @@ class Database {
fun removeCustomIcon(customIcon: IconImageCustom) {
iconDrawableFactory.clearFromCache(customIcon)
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
iconsManager.removeCustomIcon(customIcon.uuid, binaryCache)
mDatabaseKDBX?.addDeletedObject(customIcon.uuid)
}
fun updateCustomIcon(customIcon: IconImageCustom) {
iconsManager.getIcon(customIcon.uuid).updateWith(customIcon)
iconsManager.getIcon(customIcon.uuid)?.updateWith(customIcon)
}
fun getTemplates(templateCreation: Boolean): List<Template> {
@@ -212,6 +215,7 @@ class Database {
set(name) {
mDatabaseKDBX?.name = name
mDatabaseKDBX?.nameChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
val allowDescription: Boolean
@@ -224,33 +228,39 @@ class Database {
set(description) {
mDatabaseKDBX?.description = description
mDatabaseKDBX?.descriptionChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
val allowDefaultUsername: Boolean
get() = mDatabaseKDBX != null
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
var defaultUsername: String
get() {
return mDatabaseKDBX?.defaultUserName ?: "" // TODO mDatabaseKDB default username
return mDatabaseKDB?.defaultUserName ?: mDatabaseKDBX?.defaultUserName ?: ""
}
set(username) {
mDatabaseKDB?.defaultUserName = username
mDatabaseKDBX?.defaultUserName = username
mDatabaseKDBX?.defaultUserNameChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
val allowCustomColor: Boolean
get() = mDatabaseKDBX != null
// TODO get() = mDatabaseKDB != null || mDatabaseKDBX != null
// with format "#000000"
var customColor: String
var customColor: Int?
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) {
// TODO Check color string
mDatabaseKDBX?.color = value
mDatabaseKDB?.color = value
mDatabaseKDBX?.color = if (value == null) {
""
} else {
ChromaUtil.getFormattedColorString(value, false)
}
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
val allowOTP: Boolean
@@ -259,6 +269,12 @@ class Database {
val version: String
get() = mDatabaseKDB?.version ?: mDatabaseKDBX?.version ?: "-"
fun checkVersion() {
mDatabaseKDBX?.getMinKdbxVersion()?.let {
mDatabaseKDBX?.kdbxVersion = it
}
}
val type: Class<*>?
get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass
@@ -274,6 +290,8 @@ class Database {
value?.let {
mDatabaseKDBX?.compressionAlgorithm = it
}
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
fun compressionForNewEntry(): Boolean {
@@ -290,6 +308,7 @@ class Database {
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) {
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
dataModifiedSinceLastLoading = true
}
val allowNoMasterKey: Boolean
@@ -309,8 +328,6 @@ class Database {
set(algorithm) {
algorithm?.let {
mDatabaseKDBX?.encryptionAlgorithm = algorithm
mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine)
mDatabaseKDBX?.cipherUuid = algorithm.uuid
}
}
@@ -323,13 +340,10 @@ class Database {
var kdfEngine: KdfEngine?
get() = mDatabaseKDB?.kdfEngine ?: mDatabaseKDBX?.kdfEngine
set(kdfEngine) {
kdfEngine?.let {
if (mDatabaseKDBX?.kdfParameters?.uuid != kdfEngine.defaultParameters.uuid)
mDatabaseKDBX?.kdfParameters = kdfEngine.defaultParameters
numberKeyEncryptionRounds = kdfEngine.defaultKeyRounds
memoryUsage = kdfEngine.defaultMemoryUsage
parallelism = kdfEngine.defaultParallelism
}
mDatabaseKDB?.kdfEngine = kdfEngine
mDatabaseKDBX?.kdfEngine = kdfEngine
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
fun getKeyDerivationName(): String {
@@ -341,6 +355,8 @@ class Database {
set(numberRounds) {
mDatabaseKDB?.numberKeyEncryptionRounds = numberRounds
mDatabaseKDBX?.numberKeyEncryptionRounds = numberRounds
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
var memoryUsage: Long
@@ -349,12 +365,16 @@ class Database {
}
set(memory) {
mDatabaseKDBX?.memoryUsage = memory
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
var parallelism: Long
get() = mDatabaseKDBX?.parallelism ?: KdfEngine.UNKNOWN_VALUE
set(parallelism) {
mDatabaseKDBX?.parallelism = parallelism
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
var masterKey: ByteArray
@@ -362,9 +382,11 @@ class Database {
set(masterKey) {
mDatabaseKDB?.masterKey = masterKey
mDatabaseKDBX?.masterKey = masterKey
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
val rootGroup: Group?
var rootGroup: Group?
get() {
mDatabaseKDB?.rootGroup?.let {
return Group(it)
@@ -374,6 +396,25 @@ class Database {
}
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
@@ -393,6 +434,8 @@ class Database {
}
set(value) {
mDatabaseKDBX?.historyMaxItems = value
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
var historyMaxSize: Long
@@ -401,6 +444,8 @@ class Database {
}
set(value) {
mDatabaseKDBX?.historyMaxSize = value
mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
/**
@@ -421,15 +466,17 @@ class Database {
} else {
mDatabaseKDBX?.removeRecycleBin()
}
mDatabaseKDBX?.recycleBinChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
val recycleBin: Group?
get() {
mDatabaseKDB?.backupGroup?.let {
return Group(it)
return getGroupById(it.nodeId) ?: Group(it)
}
mDatabaseKDBX?.recycleBin?.let {
return Group(it)
return getGroupById(it.nodeId) ?: Group(it)
}
return null
}
@@ -439,8 +486,10 @@ class Database {
if (group != null) {
mDatabaseKDBX?.recycleBinUUID = group.nodeIdKDBX.id
} else {
mDatabaseKDBX?.removeTemplatesGroup()
mDatabaseKDBX?.removeRecycleBin()
}
mDatabaseKDBX?.recycleBinChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
/**
@@ -456,6 +505,8 @@ class Database {
fun enableTemplates(enable: Boolean, templatesGroupName: String) {
mDatabaseKDBX?.enableTemplatesGroup(enable, templatesGroupName)
mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
val templatesGroup: Group?
@@ -471,8 +522,10 @@ class Database {
if (group != null) {
mDatabaseKDBX?.entryTemplatesGroup = group.nodeIdKDBX.id
} else {
mDatabaseKDBX?.entryTemplatesGroup
mDatabaseKDBX?.removeTemplatesGroup()
}
mDatabaseKDBX?.entryTemplatesGroupChanged = DateInstant()
dataModifiedSinceLastLoading = true
}
val groupNamesNotAllowed: List<String>
@@ -499,6 +552,7 @@ class Database {
this.fileUri = databaseUri
// Set Database state
this.loaded = true
this.dataModifiedSinceLastLoading = false
}
@Throws(LoadDatabaseException::class)
@@ -555,7 +609,6 @@ class Database {
contentResolver: ContentResolver,
cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
tempCipherKey: LoadedKey,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
@@ -576,73 +629,156 @@ class Database {
// Read database stream for the first time
readDatabaseStream(contentResolver, uri,
{ databaseInputStream ->
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
.openDatabase(databaseInputStream,
mainCredential.masterPassword,
keyFileInputStream,
tempCipherKey,
progressTaskUpdater,
fixDuplicateUUID)
val databaseKDB = DatabaseKDB().apply {
binaryCache.cacheDirectory = cacheDirectory
changeDuplicateId = fixDuplicateUUID
}
DatabaseInputKDB(databaseKDB)
.openDatabase(databaseInputStream,
mainCredential.masterPassword,
keyFileInputStream,
progressTaskUpdater)
databaseKDB
},
{ databaseInputStream ->
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
.openDatabase(databaseInputStream,
mainCredential.masterPassword,
keyFileInputStream,
tempCipherKey,
progressTaskUpdater,
fixDuplicateUUID)
val databaseKDBX = DatabaseKDBX().apply {
binaryCache.cacheDirectory = cacheDirectory
changeDuplicateId = fixDuplicateUUID
}
DatabaseInputKDBX(databaseKDBX).apply {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream,
mainCredential.masterPassword,
keyFileInputStream,
progressTaskUpdater)
}
databaseKDBX
}
)
} catch (e: FileNotFoundException) {
Log.e(TAG, "Unable to load keyfile", e)
throw FileNotFoundDatabaseException()
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
} finally {
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)
fun reloadData(contentResolver: ContentResolver,
cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
tempCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?) {
// Retrieve the stream from the old database URI
try {
fileUri?.let { oldDatabaseUri ->
readDatabaseStream(contentResolver, oldDatabaseUri,
{ databaseInputStream ->
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
.openDatabase(databaseInputStream,
masterKey,
tempCipherKey,
progressTaskUpdater)
},
{ databaseInputStream ->
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
.openDatabase(databaseInputStream,
masterKey,
tempCipherKey,
progressTaskUpdater)
{ databaseInputStream ->
val databaseKDB = DatabaseKDB()
mDatabaseKDB?.let {
databaseKDB.binaryCache = it.binaryCache
}
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 {
Log.e(TAG, "Database URI is null, database cannot be reloaded")
throw IODatabaseException()
throw IODatabaseException("Database URI is null, database cannot be reloaded")
}
} catch (e: FileNotFoundException) {
Log.e(TAG, "Unable to load keyfile", e)
throw FileNotFoundDatabaseException()
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
} finally {
dataModifiedSinceLastLoading = false
}
}
@@ -682,7 +818,7 @@ class Database {
val attachmentPool: AttachmentPool
get() {
return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool(binaryCache)
return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool()
}
val allowMultipleAttachments: Boolean
@@ -695,8 +831,8 @@ class Database {
}
fun buildNewBinaryAttachment(): BinaryData? {
return mDatabaseKDB?.buildNewAttachment()
?: mDatabaseKDBX?.buildNewAttachment( false,
return mDatabaseKDB?.buildNewBinaryAttachment()
?: mDatabaseKDBX?.buildNewBinaryAttachment( false,
compressionForNewEntry(),
false)
}
@@ -710,6 +846,7 @@ class Database {
fun removeUnlinkedAttachments() {
// No check in database KDB because unique attachment by entry
mDatabaseKDBX?.removeUnlinkedAttachments(true)
dataModifiedSinceLastLoading = true
}
@Throws(DatabaseOutputException::class)
@@ -770,16 +907,25 @@ class Database {
}
}
this.fileUri = uri
this.dataModifiedSinceLastLoading = false
}
fun clear(filesDirectory: File? = null) {
binaryCache.clear()
iconsManager.clearCache()
fun clearIndexesAndBinaries(filesDirectory: File? = null) {
this.mDatabaseKDB?.clearIndexes()
this.mDatabaseKDBX?.clearIndexes()
this.mDatabaseKDB?.clearIconsCache()
this.mDatabaseKDBX?.clearIconsCache()
this.mDatabaseKDB?.clearAttachmentsCache()
this.mDatabaseKDBX?.clearAttachmentsCache()
this.mDatabaseKDB?.clearBinaries()
this.mDatabaseKDBX?.clearBinaries()
iconDrawableFactory.clearCache()
// Delete the cache of the database if present
mDatabaseKDB?.clearCache()
mDatabaseKDBX?.clearCache()
// In all cases, delete all the files in the temp dir
// delete all the files in the temp dir if allowed
try {
filesDirectory?.let { directory ->
cleanDirectory(directory)
@@ -790,7 +936,7 @@ class Database {
}
fun clearAndClose(context: Context? = null) {
clear(context?.let { UriUtil.getBinaryDir(context) })
clearIndexesAndBinaries(context?.let { UriUtil.getBinaryDir(context) })
this.mDatabaseKDB = null
this.mDatabaseKDBX = null
this.fileUri = null
@@ -817,9 +963,10 @@ class Database {
}
@Throws(IOException::class)
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.keyLastChanged = DateInstant()
}
fun rootCanContainsEntry(): Boolean {
@@ -827,6 +974,7 @@ class Database {
}
fun createEntry(): Entry? {
dataModifiedSinceLastLoading = true
mDatabaseKDB?.let { database ->
return Entry(database.createEntry()).apply {
nodeId = database.newEntryId()
@@ -842,6 +990,7 @@ class Database {
}
fun createGroup(): Group? {
dataModifiedSinceLastLoading = true
mDatabaseKDB?.let { database ->
return Group(database.createGroup()).apply {
setNodeId(database.newGroupId())
@@ -879,6 +1028,7 @@ class Database {
}
fun addEntryTo(entry: Entry, parent: Group) {
dataModifiedSinceLastLoading = true
entry.entryKDB?.let { entryKDB ->
mDatabaseKDB?.addEntryTo(entryKDB, parent.groupKDB)
}
@@ -889,6 +1039,7 @@ class Database {
}
fun updateEntry(entry: Entry) {
dataModifiedSinceLastLoading = true
entry.entryKDB?.let { entryKDB ->
mDatabaseKDB?.updateEntry(entryKDB)
}
@@ -898,6 +1049,7 @@ class Database {
}
fun removeEntryFrom(entry: Entry, parent: Group) {
dataModifiedSinceLastLoading = true
entry.entryKDB?.let { entryKDB ->
mDatabaseKDB?.removeEntryFrom(entryKDB, parent.groupKDB)
}
@@ -908,6 +1060,7 @@ class Database {
}
fun addGroupTo(group: Group, parent: Group) {
dataModifiedSinceLastLoading = true
group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
}
@@ -918,6 +1071,7 @@ class Database {
}
fun updateGroup(group: Group) {
dataModifiedSinceLastLoading = true
group.groupKDB?.let { entryKDB ->
mDatabaseKDB?.updateGroup(entryKDB)
}
@@ -927,6 +1081,7 @@ class Database {
}
fun removeGroupFrom(group: Group, parent: Group) {
dataModifiedSinceLastLoading = true
group.groupKDB?.let { groupKDB ->
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
}
@@ -965,12 +1120,17 @@ class Database {
}
fun deleteEntry(entry: Entry) {
dataModifiedSinceLastLoading = true
entry.entryKDBX?.id?.let { entryId ->
mDatabaseKDBX?.addDeletedObject(entryId)
}
entry.parent?.let {
removeEntryFrom(entry, it)
}
}
fun deleteGroup(group: Group) {
dataModifiedSinceLastLoading = true
group.doForEachChildAndForIt(
object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean {
@@ -980,6 +1140,9 @@ class Database {
},
object : NodeHandler<Group>() {
override fun operate(node: Group): Boolean {
node.groupKDBX?.id?.let { groupId ->
mDatabaseKDBX?.addDeletedObject(groupId)
}
node.parent?.let {
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) {
mDatabaseKDB?.ensureBackupExists()
mDatabaseKDBX?.ensureRecycleBinExists(resources)
@@ -1034,47 +1179,41 @@ class Database {
}
fun recycle(entry: Entry, resources: Resources) {
entry.entryKDB?.let {
mDatabaseKDB?.recycle(it)
ensureRecycleBinExists(resources)
entry.parent?.let { parent ->
removeEntryFrom(entry, parent)
}
entry.entryKDBX?.let {
mDatabaseKDBX?.recycle(it, resources)
recycleBin?.let {
addEntryTo(entry, it)
}
entry.afterAssignNewParent()
}
fun recycle(group: Group, resources: Resources) {
group.groupKDB?.let {
mDatabaseKDB?.recycle(it)
ensureRecycleBinExists(resources)
group.parent?.let { parent ->
removeGroupFrom(group, parent)
}
group.groupKDBX?.let {
mDatabaseKDBX?.recycle(it, resources)
recycleBin?.let {
addGroupTo(group, it)
}
group.afterAssignNewParent()
}
fun undoRecycle(entry: Entry, parent: Group) {
entry.entryKDB?.let { entryKDB ->
parent.groupKDB?.let { parentKDB ->
mDatabaseKDB?.undoRecycle(entryKDB, parentKDB)
}
}
entry.entryKDBX?.let { entryKDBX ->
parent.groupKDBX?.let { parentKDBX ->
mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
}
recycleBin?.let { it ->
removeEntryFrom(entry, it)
}
addEntryTo(entry, parent)
entry.afterAssignNewParent()
}
fun undoRecycle(group: Group, parent: Group) {
group.groupKDB?.let { groupKDB ->
parent.groupKDB?.let { parentKDB ->
mDatabaseKDB?.undoRecycle(groupKDB, parentKDB)
}
}
group.groupKDBX?.let { entryKDBX ->
parent.groupKDBX?.let { parentKDBX ->
mDatabaseKDBX?.undoRecycle(entryKDBX, parentKDBX)
}
recycleBin?.let {
removeGroupFrom(group, it)
}
addGroupTo(group, parent)
group.afterAssignNewParent()
}
fun startManageEntry(entry: Entry?) {

View File

@@ -28,29 +28,18 @@ import java.util.*
class DeletedObject : Parcelable {
var uuid: UUID = DatabaseVersioned.UUID_ZERO
private var mDeletionTime: DateInstant? = null
var deletionTime: DateInstant = DateInstant()
constructor()
constructor(uuid: UUID, deletionTime: DateInstant = DateInstant()) {
this.uuid = uuid
this.mDeletionTime = deletionTime
this.deletionTime = deletionTime
}
constructor(parcel: Parcel) {
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: DatabaseVersioned.UUID_ZERO
mDeletionTime = parcel.readParcelable(DateInstant::class.java.classLoader)
}
fun getDeletionTime(): DateInstant {
if (mDeletionTime == null) {
mDeletionTime = DateInstant(System.currentTimeMillis())
}
return mDeletionTime!!
}
fun setDeletionTime(deletionTime: DateInstant) {
this.mDeletionTime = deletionTime
deletionTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: deletionTime
}
override fun equals(other: Any?): Boolean {
@@ -69,7 +58,7 @@ class DeletedObject : Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(ParcelUuid(uuid), flags)
parcel.writeParcelable(mDeletionTime, flags)
parcel.writeParcelable(deletionTime, flags)
}
override fun describeContents(): Int {

View File

@@ -19,8 +19,10 @@
*/
package com.kunzisoft.keepass.database.element
import android.graphics.Color
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
@@ -238,6 +240,42 @@ class Entry : Node, EntryVersionedInterface<Group> {
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 {
return title == PMS_TAN_ENTRY && username.isNotEmpty()
}
@@ -420,6 +458,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryInfo.url = url
entryInfo.notes = notes
entryInfo.tags = tags
entryInfo.backgroundColor = backgroundColor
entryInfo.foregroundColor = foregroundColor
entryInfo.customFields = getExtraFields().toMutableList()
// Add otpElement to generate token
entryInfo.otpModel = getOtpElement()?.otpModel
@@ -455,6 +495,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
url = newEntryInfo.url
notes = newEntryInfo.notes
tags = newEntryInfo.tags
backgroundColor = newEntryInfo.backgroundColor
foregroundColor = newEntryInfo.foregroundColor
addExtraFields(newEntryInfo.customFields)
database?.attachmentPool?.let { binaryPool ->
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.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UuidUtil
import java.util.*
import kotlin.collections.ArrayList
@@ -308,8 +309,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
val withoutMetaStream = filters.contains(ChildFilter.META_STREAM)
val showExpiredEntries = !filters.contains(ChildFilter.EXPIRED)
// TODO Change KDB parser to remove meta entries
return groupKDB?.getChildEntries()?.filter {
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream))
(!withoutMetaStream || (withoutMetaStream && !it.isMetaStream()))
&& (!it.isCurrentlyExpires or showExpiredEntries)
}?.map {
Entry(it)
@@ -453,6 +455,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
fun getGroupInfo(): GroupInfo {
val groupInfo = GroupInfo()
groupInfo.id = groupKDBX?.nodeId?.id
groupInfo.title = title
groupInfo.icon = icon
groupInfo.creationTime = creationTime

View File

@@ -19,7 +19,7 @@
*/
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

View File

@@ -23,7 +23,7 @@ import android.util.Log
import java.io.IOException
import kotlin.math.abs
abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
abstract class BinaryPool<T> {
protected val pool = LinkedHashMap<T, BinaryData>()
@@ -225,9 +225,6 @@ abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
@Throws(IOException::class)
fun clear() {
doForEachBinary { _, binary ->
binary.clear(mBinaryCache)
}
pool.clear()
}

View File

@@ -4,19 +4,16 @@ import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import java.util.*
class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
class CustomIconPool : BinaryPool<UUID>() {
private val customIcons = HashMap<UUID, IconImageCustom>()
fun put(key: UUID? = null,
name: String,
lastModificationTime: DateInstant?,
smallSize: Boolean,
builder: (uniqueBinaryId: String) -> BinaryData,
result: (IconImageCustom, BinaryData?) -> Unit) {
val keyBinary = super.put(key) { uniqueBinaryId ->
// Create a byte array for better performance with small data
binaryCache.getBinaryData(uniqueBinaryId, smallSize)
}
val keyBinary = super.put(key, builder)
val uuid = keyBinary.keys.first()
val customIcon = IconImageCustom(uuid, name, lastModificationTime)
customIcons[uuid] = customIcon

View File

@@ -34,23 +34,41 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
import java.io.IOException
import java.io.InputStream
import java.util.*
import kotlin.collections.ArrayList
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
get() = "KeePass 1"
get() = "V1"
init {
kdfListV3.add(KdfFactory.aesKdf)
}
private fun getGroupById(groupId: Int): GroupKDB? {
if (groupId == -1)
return null
return getGroupById(NodeIdInt(groupId))
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
rootGroup = createGroup().apply {
icon.standard = getStandardIcon(IconImageStandard.DATABASE_ID)
}
}
val backupGroup: GroupKDB?
@@ -63,33 +81,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return listOf(BACKUP_FOLDER_TITLE)
}
override val kdfEngine: KdfEngine
get() = kdfListV3[0]
var defaultUserName: String = ""
override val kdfAvailableList: List<KdfEngine>
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
}
var color: Int? = null
/**
* Generates an unused random tree id
@@ -215,29 +209,7 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return true
}
fun recycle(group: GroupKDB) {
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 {
fun buildNewBinaryAttachment(): BinaryData {
// Generate an unique new file
return attachmentPool.put { uniqueBinaryId ->
binaryCache.getBinaryData(uniqueBinaryId, false)

View File

@@ -25,10 +25,9 @@ import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.R
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.VariantDictionary
import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
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.icon.IconImageCustom
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.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.template.Template
@@ -66,6 +67,7 @@ import javax.crypto.Mac
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import kotlin.collections.HashSet
import kotlin.math.min
@@ -73,27 +75,71 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var hmacKey: ByteArray? = null
private set
var cipherUuid = EncryptionAlgorithm.AESRijndael.uuid
private var dataEngine: CipherEngine = AesEngine()
var compressionAlgorithm = CompressionAlgorithm.GZip
override var encryptionAlgorithm: EncryptionAlgorithm = EncryptionAlgorithm.AESRijndael
fun setEncryptionAlgorithmFromUUID(uuid: UUID) {
encryptionAlgorithm = EncryptionAlgorithm.getFrom(uuid)
}
override val availableEncryptionAlgorithms: List<EncryptionAlgorithm> = listOf(
EncryptionAlgorithm.AESRijndael,
EncryptionAlgorithm.Twofish,
EncryptionAlgorithm.ChaCha20
)
var kdfParameters: KdfParameters? = null
private var kdfList: MutableList<KdfEngine> = ArrayList()
private var numKeyEncRounds: Long = 0
var publicCustomData = VariantDictionary()
override var kdfEngine: KdfEngine?
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 mTemplateEngine = TemplateEngineCompatible(this)
var kdbxVersion = UnsignedInt(0)
var name = ""
var nameChanged = DateInstant()
// TODO change setting date
var settingsChanged = DateInstant()
var description = ""
var descriptionChanged = DateInstant()
var defaultUserName = ""
var defaultUserNameChanged = DateInstant()
// TODO last change date
var settingsChanged = DateInstant()
var keyLastChanged = DateInstant()
var keyChangeRecDays: Long = -1
var keyChangeForceDays: Long = 1
@@ -115,17 +161,12 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
var lastSelectedGroupUUID = UUID_ZERO
var lastTopVisibleGroupUUID = UUID_ZERO
var memoryProtection = MemoryProtectionConfig()
val deletedObjects = ArrayList<DeletedObject>()
val deletedObjects = HashSet<DeletedObject>()
var publicCustomData = VariantDictionary()
val customData = CustomData()
var localizedAppName = "KeePassDX"
init {
kdfList.add(KdfFactory.aesKdf)
kdfList.add(KdfFactory.argon2dKdf)
kdfList.add(KdfFactory.argon2idKdf)
}
constructor()
/**
@@ -156,41 +197,74 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
FILE_VERSION_41 -> "4.1"
else -> "UNKNOWN"
}
return "KeePass 2 - KDBX$kdbxStringVersion"
return "V2 - KDBX$kdbxStringVersion"
}
override val kdfEngine: KdfEngine?
get() = try {
getEngineKDBX4(kdfParameters)
} catch (unknownKDF: UnknownKDF) {
Log.i(TAG, "Unable to retrieve KDF engine", unknownKDF)
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
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
}
throw unknownKDFException
}
val availableCompressionAlgorithms: List<CompressionAlgorithm>
get() {
val list = ArrayList<CompressionAlgorithm>()
list.add(CompressionAlgorithm.None)
list.add(CompressionAlgorithm.GZip)
return list
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)
}
}
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,
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
get() {
val kdfEngine = kdfEngine
var numKeyEncRounds: Long = 0
if (kdfEngine != null && kdfParameters != null)
numKeyEncRounds = kdfEngine.getKeyRounds(kdfParameters!!)
return numKeyEncRounds
@@ -265,7 +331,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
val kdfEngine = kdfEngine
if (kdfEngine != null && kdfParameters != null)
kdfEngine.setKeyRounds(kdfParameters!!, rounds)
numKeyEncRounds = rounds
}
var memoryUsage: Long
@@ -305,7 +370,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
// Retrieve recycle bin in index
val recycleBin: GroupKDBX?
get() = if (recycleBinUUID == UUID_ZERO) null else getGroupByUUID(recycleBinUUID)
get() = getGroupByUUID(recycleBinUUID)
val lastSelectedGroup: GroupKDBX?
get() = getGroupByUUID(lastSelectedGroupUUID)
@@ -313,17 +378,14 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
val lastTopVisibleGroup: GroupKDBX?
get() = getGroupByUUID(lastTopVisibleGroupUUID)
fun setDataEngine(dataEngine: CipherEngine) {
this.dataEngine = dataEngine
}
override fun getStandardIcon(iconId: Int): IconImageStandard {
return this.iconsManager.getIcon(iconId)
}
fun buildNewCustomIcon(customIconId: UUID? = null,
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,
@@ -331,14 +393,21 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
lastModificationTime: DateInstant?,
smallSize: Boolean,
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 {
return iconsManager.isCustomIconBinaryDuplicate(binary)
}
fun getCustomIcon(iconUuid: UUID): IconImageCustom {
fun getCustomIcon(iconUuid: UUID): IconImageCustom? {
return this.iconsManager.getIcon(iconUuid)
}
@@ -355,7 +424,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
val templatesGroup = firstGroupWithValidName
?: mTemplateEngine.createNewTemplatesGroup(templatesGroupName)
entryTemplatesGroup = templatesGroup.id
entryTemplatesGroupChanged = templatesGroup.lastModificationTime
} else {
removeTemplatesGroup()
}
@@ -363,7 +431,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
fun removeTemplatesGroup() {
entryTemplatesGroup = UUID_ZERO
entryTemplatesGroupChanged = DateInstant()
mTemplateEngine.clearCache()
}
@@ -414,37 +481,37 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
fun getEntryByTitle(title: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
return findEntry { entry ->
entry.decodeTitleKey(recursionLevel).equals(title, true)
}
}
fun getEntryByUsername(username: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
return findEntry { entry ->
entry.decodeUsernameKey(recursionLevel).equals(username, true)
}
}
fun getEntryByURL(url: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
return findEntry { entry ->
entry.decodeUrlKey(recursionLevel).equals(url, true)
}
}
fun getEntryByPassword(password: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
return findEntry { entry ->
entry.decodePasswordKey(recursionLevel).equals(password, true)
}
}
fun getEntryByNotes(notes: String, recursionLevel: Int): EntryKDBX? {
return this.entryIndexes.values.find { entry ->
return findEntry { entry ->
entry.decodeNotesKey(recursionLevel).equals(notes, true)
}
}
fun getEntryByCustomData(customDataValue: String): EntryKDBX? {
return entryIndexes.values.find { entry ->
return findEntry { entry ->
entry.customData.containsItemWithValue(customDataValue)
}
}
@@ -486,7 +553,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
val cmpKey = ByteArray(65)
System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
finalKey = resizeKey(cmpKey, dataEngine.keyLength())
finalKey = resizeKey(cmpKey, encryptionAlgorithm.cipherEngine.keyLength())
val messageDigest: MessageDigest
try {
@@ -724,14 +791,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
firstGroupWithValidName
}
recycleBinUUID = recycleBinGroup.id
recycleBinChanged = recycleBinGroup.lastModificationTime
recycleBinChanged = DateInstant()
}
}
fun removeRecycleBin() {
if (recycleBin != null) {
recycleBinUUID = UUID_ZERO
recycleBinChanged = DateInstant()
}
}
@@ -753,38 +819,18 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return false
}
fun recycle(group: GroupKDBX, resources: Resources) {
ensureRecycleBinExists(resources)
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 getDeletedObject(nodeId: NodeId<UUID>): DeletedObject? {
return deletedObjects.find { it.uuid == nodeId.id }
}
fun addDeletedObject(deletedObject: DeletedObject) {
this.deletedObjects.add(deletedObject)
}
fun addDeletedObject(objectId: UUID) {
addDeletedObject(DeletedObject(objectId))
}
override fun addEntryTo(newEntry: EntryKDBX, parent: GroupKDBX?) {
super.addEntryTo(newEntry, parent)
mFieldReferenceEngine.clear()
@@ -797,23 +843,17 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
override fun removeEntryFrom(entryToRemove: EntryKDBX, parent: GroupKDBX?) {
super.removeEntryFrom(entryToRemove, parent)
deletedObjects.add(DeletedObject(entryToRemove.id))
mFieldReferenceEngine.clear()
}
override fun undoDeleteEntryFrom(entry: EntryKDBX, origParent: GroupKDBX?) {
super.undoDeleteEntryFrom(entry, origParent)
deletedObjects.remove(DeletedObject(entry.id))
}
fun containsPublicCustomData(): Boolean {
return publicCustomData.size() > 0
}
fun buildNewAttachment(smallSize: Boolean,
compression: Boolean,
protection: Boolean,
binaryPoolId: Int? = null): BinaryData {
fun buildNewBinaryAttachment(smallSize: Boolean,
compression: Boolean,
protection: Boolean,
binaryPoolId: Int? = null): BinaryData {
return attachmentPool.put(binaryPoolId) { uniqueBinaryId ->
binaryCache.getBinaryData(uniqueBinaryId, smallSize, compression, protection)
}.binary
@@ -830,6 +870,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
}
private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
// TODO check in icon pool
// Build binaries to remove with all binaries known
val binariesToRemove = ArrayList<BinaryData>()
if (binaries.isEmpty()) {
@@ -866,11 +907,10 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return super.validatePasswordEncoding(password, containsKeyFile)
}
override fun clearCache() {
override fun clearIndexes() {
try {
super.clearCache()
super.clearIndexes()
mFieldReferenceEngine.clear()
attachmentPool.clear()
} catch (e: Exception) {
Log.e(TAG, "Unable to clear cache", e)
}

View File

@@ -19,8 +19,10 @@
*/
package com.kunzisoft.keepass.database.element.database
import android.util.Log
import com.kunzisoft.encrypt.HashManager
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.BinaryCache
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
@@ -44,51 +46,42 @@ abstract class DatabaseVersioned<
Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
> {
// 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 finalKey: ByteArray? = null
protected set
abstract val version: String
/**
* To manage binaries in faster way
* Cipher key generated when the database is loaded, and destroyed when the database is closed
* Can be used to temporarily store database elements
*/
var binaryCache = BinaryCache()
val iconsManager = IconsManager(binaryCache)
var attachmentPool = AttachmentPool(binaryCache)
var iconsManager = IconsManager()
var attachmentPool = AttachmentPool()
var changeDuplicateId = false
private var groupIndexes = LinkedHashMap<NodeId<GroupId>, Group>()
protected 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>
private var entryIndexes = LinkedHashMap<NodeId<EntryId>, Entry>()
var rootGroup: Group? = null
set(value) {
field = value
value?.let {
removeGroupIndex(it)
addGroupIndex(it)
}
}
@@ -124,25 +117,29 @@ abstract class DatabaseVersioned<
@Throws(IOException::class)
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
val keyData = keyInputStream.readBytes()
try {
val keyData = keyInputStream.readBytes()
// Check XML key file
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
if (xmlKeyByteArray != null) {
return xmlKeyByteArray
}
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
// Check XML key file
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
if (xmlKeyByteArray != null) {
return xmlKeyByteArray
}
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
}
}
// Hash file as binary data
return HashManager.hashSha256(keyData)
} catch (outOfMemoryError: OutOfMemoryError) {
throw IOException("Keyfile data is too large", outOfMemoryError)
}
// Hash file as binary data
return HashManager.hashSha256(keyData)
}
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
@@ -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
*
@@ -215,14 +206,7 @@ abstract class DatabaseVersioned<
return groupIndexes.values
}
fun setGroupIndexes(groupList: List<Group>) {
this.groupIndexes.clear()
for (currentGroup in groupList) {
this.groupIndexes[currentGroup.nodeId] = currentGroup
}
}
fun getGroupById(id: NodeId<GroupId>): Group? {
open fun getGroupById(id: NodeId<GroupId>): Group? {
return this.groupIndexes[id]
}
@@ -246,16 +230,6 @@ abstract class DatabaseVersioned<
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 {
return entryIndexes.containsKey(id)
}
@@ -268,6 +242,10 @@ abstract class DatabaseVersioned<
return this.entryIndexes[id]
}
fun findEntry(predicate: (Entry) -> Boolean): Entry? {
return this.entryIndexes.values.find(predicate)
}
fun addEntryIndex(entry: Entry) {
val entryId = entry.nodeId
if (entryIndexes.containsKey(entryId)) {
@@ -288,11 +266,7 @@ abstract class DatabaseVersioned<
this.entryIndexes.remove(entry.nodeId)
}
fun numberOfEntries(): Int {
return entryIndexes.size
}
open fun clearCache() {
open fun clearIndexes() {
this.groupIndexes.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
parent?.removeChildGroup(groupToRemove)
removeGroupIndex(groupToRemove)
@@ -349,15 +323,6 @@ abstract class DatabaseVersioned<
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
fun isGroupSearchable(group: Group?, omitBackup: Boolean): Boolean {
@@ -368,6 +333,39 @@ abstract class DatabaseVersioned<
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 {
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.BinaryData
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.node.NodeId
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
// Determine if this is a MetaStream entry
val isMetaStream: Boolean
get() {
if (notes.isEmpty()) return false
if (binaryDescription != PMS_ID_BINDESC) return false
if (title.isEmpty()) return false
if (title != PMS_ID_TITLE) return false
if (username.isEmpty()) return false
if (username != PMS_ID_USER) return false
if (url.isEmpty()) return false
if (url != PMS_ID_URL) return false
return icon.standard.id == KEY_ID
}
fun isMetaStream(): Boolean {
if (notes.isEmpty()) return false
if (binaryDescription != PMS_ID_BINDESC) return false
if (title.isEmpty()) return false
if (title != PMS_ID_TITLE) return false
if (username.isEmpty()) return false
if (username != PMS_ID_USER) return false
if (url.isEmpty()) return false
if (url != PMS_ID_URL) return false
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> {
return NodeIdUUID()
@@ -113,8 +139,9 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
dest.writeInt(binaryDataId ?: -1)
}
fun updateWith(source: EntryKDB) {
super.updateWith(source)
fun updateWith(source: EntryKDB,
updateParents: Boolean = true) {
super.updateWith(source, updateParents)
title = source.title
username = source.username
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_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
val CREATOR: Parcelable.Creator<EntryKDB> = object : Parcelable.Creator<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
* @param source
*/
fun updateWith(source: EntryKDBX, copyHistory: Boolean = true) {
super.updateWith(source)
fun updateWith(source: EntryKDBX,
copyHistory: Boolean = true,
updateParents: Boolean = true) {
super.updateWith(source, updateParents)
usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged)
customData = CustomData(source.customData)

View File

@@ -53,8 +53,9 @@ class GroupKDB : GroupVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
dest.writeInt(groupFlags)
}
fun updateWith(source: GroupKDB) {
super.updateWith(source)
fun updateWith(source: GroupKDB,
updateParents: Boolean = true) {
super.updateWith(source, updateParents)
groupFlags = source.groupFlags
}

View File

@@ -102,8 +102,9 @@ class GroupKDBX : GroupVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
dest.writeParcelable(ParcelUuid(previousParentGroup), flags)
}
fun updateWith(source: GroupKDBX) {
super.updateWith(source)
fun updateWith(source: GroupKDBX,
updateParents: Boolean = true) {
super.updateWith(source, updateParents)
usageCount = source.usageCount
locationChanged = DateInstant(source.locationChanged)
// Add all custom elements in map

View File

@@ -51,12 +51,15 @@ abstract class GroupVersioned
dest.writeString(titleGroup)
}
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>) {
super.updateWith(source)
protected fun updateWith(source: GroupVersioned<GroupId, EntryId, Group, Entry>,
updateParents: Boolean = true) {
super.updateWith(source, updateParents)
titleGroup = source.titleGroup
removeChildren()
childGroups.addAll(source.childGroups)
childEntries.addAll(source.childEntries)
if (updateParents) {
removeChildren()
childGroups.addAll(source.childGroups)
childEntries.addAll(source.childEntries)
}
}
override var title: String

View File

@@ -81,6 +81,7 @@ class IconImageStandard : IconImageDraw {
const val CREDIT_CARD_ID = 37
const val TRASH_ID = 43
const val FOLDER_ID = 48
const val DATABASE_ID = 50
const val LIST_ID = 57
const val BUILD_ID = 59
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 java.util.*
class IconsManager(binaryCache: BinaryCache) {
class IconsManager {
private val standardCache = List(NB_ICONS) {
IconImageStandard(it)
}
private val customCache = CustomIconPool(binaryCache)
private val customCache = CustomIconPool()
fun getIcon(iconId: Int): IconImageStandard {
val searchIconId = if (IconImageStandard.isCorrectIconId(iconId)) iconId else KEY_ID
@@ -50,29 +50,23 @@ class IconsManager(binaryCache: BinaryCache) {
* 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,
name: String,
lastModificationTime: DateInstant?,
smallSize: Boolean,
builder: (uniqueBinaryId: String) -> BinaryData,
result: (IconImageCustom, BinaryData?) -> Unit) {
customCache.put(key, name, lastModificationTime, smallSize, result)
customCache.put(key, name, lastModificationTime, builder, result)
}
fun getIcon(iconUuid: UUID): IconImageCustom {
return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid)
fun getIcon(iconUuid: UUID): IconImageCustom? {
return customCache.getCustomIcon(iconUuid)
}
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
return customCache.isBinaryDuplicate(binaryData)
}
fun removeCustomIcon(binaryCache: BinaryCache, iconUuid: UUID) {
fun removeCustomIcon(iconUuid: UUID, binaryCache: BinaryCache) {
val binary = customCache[iconUuid]
customCache.remove(iconUuid)
try {
@@ -99,12 +93,8 @@ class IconsManager(binaryCache: BinaryCache) {
/**
* Clear the cache of icons
*/
fun clearCache() {
try {
customCache.clear()
} catch(e: Exception) {
Log.e(TAG, "Unable to clear cache", e)
}
fun clear() {
customCache.clear()
}
companion object {

View File

@@ -32,6 +32,19 @@ interface Node: NodeVersionedInterface<Group> {
fun removeParent() {
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 {
return id?.hashCode() ?: 0
}
abstract fun toVisualString(): String?
}

View File

@@ -63,6 +63,10 @@ class NodeIdInt : NodeId<Int> {
return id.toString()
}
override fun toVisualString(): String? {
return null
}
companion object {
@JvmField
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()
}
override fun toVisualString(): String {
return toString()
}
companion object {
@JvmField
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
}
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.parent = source.parent
if (updateParents) {
this.parent = source.parent
}
this.icon = source.icon
this.creationTime = DateInstant(source.creationTime)
this.lastModificationTime = DateInstant(source.lastModificationTime)

View File

@@ -23,10 +23,8 @@ import android.os.ParcelUuid
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashMap
class Template : Parcelable {
@@ -34,6 +32,8 @@ class Template : Parcelable {
var uuid: UUID = DatabaseVersioned.UUID_ZERO
var title = ""
var icon = IconImage()
var backgroundColor: Int? = null
var foregroundColor: Int? = null
var sections: MutableList<TemplateSection> = ArrayList()
private set
@@ -41,7 +41,8 @@ class Template : Parcelable {
title: String,
icon: IconImage,
section: TemplateSection,
version: Int = 1): this(uuid, title, icon, ArrayList<TemplateSection>().apply {
version: Int = 1)
: this(uuid, title, icon, ArrayList<TemplateSection>().apply {
add(section)
}, version)
@@ -49,11 +50,22 @@ class Template : Parcelable {
title: String,
icon: IconImage,
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) {
this.version = version
this.uuid = uuid
this.title = title
this.icon = icon
this.backgroundColor = backgroundColor
this.foregroundColor = foregroundColor
this.sections.clear()
this.sections.addAll(sections)
}
@@ -63,6 +75,8 @@ class Template : Parcelable {
this.uuid = template.uuid
this.title = template.title
this.icon = template.icon
this.backgroundColor = template.backgroundColor
this.foregroundColor = template.foregroundColor
this.sections.clear()
this.sections.addAll(template.sections)
}
@@ -72,6 +86,8 @@ class Template : Parcelable {
uuid = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: uuid
title = parcel.readString() ?: title
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
backgroundColor = parcel.readInt()
foregroundColor = parcel.readInt()
parcel.readList(sections, TemplateSection::class.java.classLoader)
}
@@ -80,6 +96,8 @@ class Template : Parcelable {
parcel.writeParcelable(ParcelUuid(uuid), flags)
parcel.writeString(title)
parcel.writeParcelable(icon, flags)
parcel.writeInt(backgroundColor ?: -1)
parcel.writeInt(foregroundColor ?: -1)
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
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
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
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import java.util.*
import kotlin.collections.LinkedHashMap
class TemplateBuilder {

View File

@@ -1,6 +1,24 @@
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.graphics.Color
import android.util.Log
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Field
@@ -26,7 +44,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
if (templateGroup != null) {
templates.add(Template.STANDARD)
templateGroup.getChildEntries().forEach { templateEntry ->
getTemplateFromTemplateEntry(templateEntry)?.let {
getTemplateFromTemplateEntry(templateEntry).let {
mCacheTemplates[templateEntry.id] = it
templates.add(it)
}
@@ -70,7 +88,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
return mCacheTemplates[uuid]
else {
mDatabase.getEntryById(uuid)?.let { templateEntry ->
getTemplateFromTemplateEntry(templateEntry)?.let { newTemplate ->
getTemplateFromTemplateEntry(templateEntry).let { newTemplate ->
mCacheTemplates[uuid] = newTemplate
return newTemplate
}
@@ -134,7 +152,7 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
return TemplateSection(sectionAttributes)
}
private fun getTemplateFromTemplateEntry(templateEntry: EntryKDBX): Template? {
private fun getTemplateFromTemplateEntry(templateEntry: EntryKDBX): Template {
val templateEntryDecoded = decodeTemplateEntry(templateEntry)
val templateSections = mutableListOf<TemplateSection>()
@@ -149,7 +167,28 @@ abstract class TemplateEngine(private val mDatabase: DatabaseKDBX) {
}
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 {

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
import android.util.Log
@@ -208,16 +226,8 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
when (attribute.type) {
TemplateAttributeType.TEXT -> {
try {
when (attribute.options.getNumberLines()) {
1 -> {
// If one line, default attribute option is number of chars
attribute.options.setNumberChars(defaultOption.toInt())
}
else -> {
// else it's number of lines
attribute.options.setNumberLines(defaultOption.toInt())
}
}
// It's always a number of lines...
attribute.options.setNumberLines(defaultOption.toInt())
} catch (e: Exception) {
Log.e(TAG, "Unable to transform default text option", e)
}
@@ -265,6 +275,9 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
entryCopy.putField(field)
}
}
// Add colors
entryCopy.foregroundColor = templateEntry.foregroundColor
entryCopy.backgroundColor = templateEntry.backgroundColor
return entryCopy
}
@@ -367,6 +380,9 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
}
}
}
// Add colors
entryCopy.foregroundColor = templateEntry.foregroundColor
entryCopy.backgroundColor = templateEntry.backgroundColor
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
import android.content.Context
@@ -27,7 +45,7 @@ object TemplateField {
const val LABEL_DATE_OF_ISSUE = "Date of issue"
const val LABEL_EMAIL = "Email"
const val LABEL_EMAIL_ADDRESS = "Email address"
const val LABEL_WIRELESS = "Wifi"
const val LABEL_WIRELESS = "Wi-Fi"
const val LABEL_SSID = "SSID"
const val LABEL_TYPE = "Type"
const val LABEL_CRYPTOCURRENCY = "Cryptocurrency wallet"

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
import android.os.Parcel

View File

@@ -46,6 +46,7 @@ open class LoadDatabaseException : DatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database
constructor() : super()
constructor(string: String) : super(string)
constructor(throwable: Throwable) : super(throwable)
}
@@ -53,6 +54,7 @@ class FileNotFoundDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.file_not_found_content
constructor() : super()
constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception)
}
@@ -76,6 +78,7 @@ class IODatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database
constructor() : super()
constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception)
}

View File

@@ -19,8 +19,6 @@
*/
package com.kunzisoft.keepass.database.file
import com.kunzisoft.keepass.utils.UnsignedInt
abstract class DatabaseHeader {
/**
@@ -33,8 +31,4 @@ abstract class DatabaseHeader {
*/
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 signature1 = UnsignedInt(0) // = PWM_DBSIG_1
var signature1 = UnsignedInt(0) // = DBSIG_1
var signature2 = UnsignedInt(0) // = DBSIG_2
var flags= UnsignedInt(0)
var version= UnsignedInt(0)
@@ -84,9 +84,9 @@ class DatabaseHeaderKDB : DatabaseHeader() {
companion object {
// DB sig from KeePass 1.03
val DBSIG_2 = UnsignedInt(-0x4ab4049b)
// DB sig from KeePass 1.03
val DBVER_DW = UnsignedInt(0x00030003)
val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
val DBSIG_2 = UnsignedInt(-0x4ab4049b) // 0xB54BFB65
val DBVER_DW = UnsignedInt(0x00030004)
val FLAG_SHA2 = UnsignedInt(1)
val FLAG_RIJNDAEL = UnsignedInt(2)
@@ -97,7 +97,7 @@ class DatabaseHeaderKDB : DatabaseHeader() {
const val BUF_SIZE = 124
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 {

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.database.file
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.VariantDictionary
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.element.database.CompressionAlgorithm
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.stream.CopyInputStream
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)
init {
this.version = getMinKdbxVersion(databaseV4) // Only for writing
this.version = databaseV4.getMinKdbxVersion()
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
* @param inputStream
* @throws IOException
@@ -256,8 +191,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
if (pbId == null || pbId.size != 16) {
throw IOException("Invalid cipher ID.")
}
databaseV4.cipherUuid = bytes16ToUuid(pbId)
databaseV4.setEncryptionAlgorithmFromUUID(bytes16ToUuid(pbId))
}
private fun setTransformRound(roundsByte: ByteArray) {
@@ -311,8 +245,9 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
companion object {
val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a)
val DBSIG_2 = UnsignedInt(-0x4ab40499)
val DBSIG_1 = UnsignedInt(-0x655d26fd) // 0x9AA2D903
val DBSIG_PRE2 = UnsignedInt(-0x4ab4049a) // 0xB54BFB66
val DBSIG_2 = UnsignedInt(-0x4ab40499) // 0xB54BFB67
private val FILE_VERSION_CRITICAL_MASK = UnsignedInt(-0x10000)
val FILE_VERSION_31 = UnsignedInt(0x00030001)
@@ -335,7 +270,7 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
}
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 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.exception.LoadDatabaseException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.File
import java.io.InputStream
abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>>
(protected val cacheDirectory: File,
protected val isRAMSufficient: (memoryWanted: Long) -> Boolean) {
abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var mDatabase: D) {
private var startTimeKey = System.currentTimeMillis()
private var startTimeContent = System.currentTimeMillis()
@@ -49,17 +45,13 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>>
abstract fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): D
progressTaskUpdater: ProgressTaskUpdater?): D
@Throws(LoadDatabaseException::class)
abstract fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean = false): D
progressTaskUpdater: ProgressTaskUpdater?): D
protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) {
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)

View File

@@ -20,17 +20,16 @@
package com.kunzisoft.keepass.database.file.input
import android.graphics.Color
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
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.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.*
@@ -46,21 +45,15 @@ import kotlin.collections.HashMap
/**
* Load a KDB database file.
*/
class DatabaseInputKDB(cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean)
: DatabaseInput<DatabaseKDB>(cacheDirectory, isRAMSufficient) {
private lateinit var mDatabase: DatabaseKDB
class DatabaseInputKDB(database: DatabaseKDB)
: DatabaseInput<DatabaseKDB>(database) {
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@@ -68,11 +61,8 @@ class DatabaseInputKDB(cacheDirectory: File,
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.masterKey = masterKey
}
}
@@ -80,7 +70,6 @@ class DatabaseInputKDB(cacheDirectory: File,
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
try {
@@ -98,7 +87,7 @@ class DatabaseInputKDB(cacheDirectory: File,
if (fileSize != (contentSize + DatabaseHeaderKDB.BUF_SIZE))
throw IOException("Header corrupted")
if (header.signature1 != DatabaseHeader.PWM_DBSIG_1
if (header.signature1 != DatabaseHeaderKDB.DBSIG_1
|| header.signature2 != DatabaseHeaderKDB.DBSIG_2) {
throw SignatureDatabaseException()
}
@@ -107,10 +96,6 @@ class DatabaseInputKDB(cacheDirectory: File,
throw VersionDatabaseException()
}
mDatabase = DatabaseKDB()
mDatabase.binaryCache.cacheDirectory = cacheDirectory
mDatabase.changeDuplicateId = fixDuplicateUUID
assignMasterKey?.invoke()
// 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
val groupLevelList = HashMap<GroupKDB, Int>()
var newGroup: GroupKDB? = null
@@ -285,7 +266,7 @@ class DatabaseInputKDB(cacheDirectory: File,
0x000E -> {
newEntry?.let { entry ->
if (fieldSize > 0) {
val binaryData = mDatabase.buildNewAttachment()
val binaryData = mDatabase.buildNewBinaryAttachment()
entry.putBinary(binaryData, mDatabase.attachmentPool)
BufferedOutputStream(binaryData.getOutputDataStream(mDatabase.binaryCache)).use { outputStream ->
cipherInputStream.readBytes(fieldSize) { buffer ->
@@ -303,7 +284,34 @@ class DatabaseInputKDB(cacheDirectory: File,
newGroup = null
}
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++
newEntry = null
}
@@ -323,16 +331,16 @@ class DatabaseInputKDB(cacheDirectory: File,
stopContentTimer()
} catch (e: LoadDatabaseException) {
mDatabase.clearCache()
mDatabase.clearAll()
throw e
} catch (e: IOException) {
mDatabase.clearCache()
mDatabase.clearAll()
throw IODatabaseException(e)
} catch (e: OutOfMemoryError) {
mDatabase.clearCache()
mDatabase.clearAll()
throw NoMemoryDatabaseException(e)
} catch (e: Exception) {
mDatabase.clearCache()
mDatabase.clearAll()
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.element.*
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.DatabaseKDBX
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.entry.EntryKDBX
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.NodeKDBXInterface
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.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
@@ -63,12 +62,10 @@ import javax.crypto.CipherInputStream
import javax.crypto.Mac
import kotlin.math.min
class DatabaseInputKDBX(cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean)
: DatabaseInput<DatabaseKDBX>(cacheDirectory, isRAMSufficient) {
class DatabaseInputKDBX(database: DatabaseKDBX)
: DatabaseInput<DatabaseKDBX>(database) {
private var randomStream: StreamCipher? = null
private lateinit var mDatabase: DatabaseKDBX
private var hashOfHeader: ByteArray? = null
@@ -97,15 +94,18 @@ class DatabaseInputKDBX(cacheDirectory: File,
private var entryCustomDataKey: 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)
override fun openDatabase(databaseInputStream: InputStream,
password: String?,
keyfileInputStream: InputStream?,
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.retrieveMasterKey(password, keyfileInputStream)
}
}
@@ -113,11 +113,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
@Throws(LoadDatabaseException::class)
override fun openDatabase(databaseInputStream: InputStream,
masterKey: ByteArray,
loadedCipherKey: LoadedKey,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) {
mDatabase.binaryCache.loadedCipherKey = loadedCipherKey
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
return openDatabase(databaseInputStream, progressTaskUpdater) {
mDatabase.masterKey = masterKey
}
}
@@ -125,14 +122,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
@Throws(LoadDatabaseException::class)
private fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?,
fixDuplicateUUID: Boolean,
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
try {
startKeyTimer(progressTaskUpdater)
mDatabase = DatabaseKDBX()
mDatabase.binaryCache.cacheDirectory = cacheDirectory
mDatabase.changeDuplicateId = fixDuplicateUUID
val header = DatabaseHeaderKDBX(mDatabase)
@@ -148,13 +140,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
stopKeyTimer()
startContentTimer(progressTaskUpdater)
val engine: CipherEngine
val cipher: Cipher
try {
engine = EncryptionAlgorithm.getFrom(mDatabase.cipherUuid).cipherEngine
val engine: CipherEngine = mDatabase.encryptionAlgorithm.cipherEngine
engine.forcePaddingCompatibility = true
mDatabase.setDataEngine(engine)
mDatabase.encryptionAlgorithm = engine.getEncryptionAlgorithm()
cipher = engine.getCipher(Cipher.DECRYPT_MODE, mDatabase.finalKey!!, header.encryptionIV)
engine.forcePaddingCompatibility = false
} catch (e: Exception) {
@@ -288,7 +277,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
val byteLength = size - 1
// No compression at this level
val protectedBinary = mDatabase.buildNewAttachment(
val protectedBinary = mDatabase.buildNewBinaryAttachment(
isRAMSufficient.invoke(byteLength.toLong()), false, protectedFlag)
protectedBinary.getOutputDataStream(mDatabase.binaryCache).use { outputStream ->
dataInputStream.readBytes(byteLength) { buffer ->
@@ -524,7 +513,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxGroup?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} 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)) {
ctxGroup?.tags = readTags(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemPreviousParentGroup, ignoreCase = true)) {
@@ -583,7 +573,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
} else if (name.equals(DatabaseKDBXXML.ElemIcon, ignoreCase = true)) {
ctxEntry?.icon?.standard = mDatabase.getStandardIcon(readUInt(xpp, UnsignedInt(0)).toKotlinInt())
} 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)) {
ctxEntry?.foregroundColor = readString(xpp)
} 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)) {
ctxDeletedObject?.uuid = readUuid(xpp)
} else if (name.equals(DatabaseKDBXXML.ElemDeletionTime, ignoreCase = true)) {
ctxDeletedObject?.setDeletionTime(readDateInstant(xpp))
ctxDeletedObject?.deletionTime = readDateInstant(xpp)
} else {
readUnknown(xpp)
}
@@ -1009,7 +1000,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
var binaryRetrieve = mDatabase.attachmentPool[id]
// Create empty binary if not retrieved in pool
if (binaryRetrieve == null) {
binaryRetrieve = mDatabase.buildNewAttachment(
binaryRetrieve = mDatabase.buildNewBinaryAttachment(
smallSize = false,
compression = false,
protection = false,
@@ -1049,7 +1040,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
return null
// Build the new binary and compress
val binaryAttachment = mDatabase.buildNewAttachment(
val binaryAttachment = mDatabase.buildNewBinaryAttachment(
isRAMSufficient.invoke(base64.length.toLong()), compressed, protected, binaryId)
try {
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.element.database.DatabaseKDBX
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.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.stream.MacOutputStream
@@ -68,11 +67,11 @@ constructor(private val databaseKDBX: DatabaseKDBX,
@Throws(IOException::class)
fun output() {
mos.write4BytesUInt(DatabaseHeader.PWM_DBSIG_1)
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_1)
mos.write4BytesUInt(DatabaseHeaderKDBX.DBSIG_2)
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.MasterSeed, header.masterSeed)
@@ -130,6 +129,6 @@ constructor(private val databaseKDBX: DatabaseKDBX,
}
companion object {
private val EndHeaderValue = byteArrayOf('\r'.toByte(), '\n'.toByte(), '\r'.toByte(), '\n'.toByte())
private val EndHeaderValue = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte(), '\r'.code.toByte(), '\n'.code.toByte())
}
}

View File

@@ -19,9 +19,11 @@
*/
package com.kunzisoft.keepass.database.file.output
import android.graphics.Color
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
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.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeader
@@ -34,7 +36,6 @@ import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.OutputStream
import java.security.*
import java.util.*
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
@@ -44,6 +45,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
private var headerHashBlock: ByteArray? = null
private var mGroupList = mutableListOf<GroupKDB>()
private var mEntryList = mutableListOf<EntryKDB>()
@Throws(DatabaseOutputException::class)
fun getFinalKey(header: DatabaseHeader): ByteArray? {
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
// also remove the virtual root not present in kdb
val rootGroup = mDatabaseKDB.rootGroup
sortGroupsForOutput()
sortNodesForOutput()
val header = outputHeader(mOutputStream)
@@ -91,6 +95,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} finally {
// Add again the virtual root group for better management
mDatabaseKDB.rootGroup = rootGroup
clearParser()
}
}
@@ -105,7 +110,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB {
// Build header
val header = DatabaseHeaderKDB()
header.signature1 = DatabaseHeader.PWM_DBSIG_1
header.signature1 = DatabaseHeaderKDB.DBSIG_1
header.signature2 = DatabaseHeaderKDB.DBSIG_2
header.flags = DatabaseHeaderKDB.FLAG_SHA2
@@ -120,8 +125,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
}
header.version = DatabaseHeaderKDB.DBVER_DW
header.numGroups = UnsignedInt(mDatabaseKDB.numberOfGroups())
header.numEntries = UnsignedInt(mDatabaseKDB.numberOfEntries())
// To remove root
header.numGroups = UnsignedInt(mGroupList.size)
header.numEntries = UnsignedInt(mEntryList.size)
header.numKeyEncRounds = UnsignedInt.fromKotlinLong(mDatabaseKDB.numberKeyEncryptionRounds)
setIVs(header)
@@ -194,31 +200,89 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
}
// Groups
mDatabaseKDB.doForEachGroupInIndex { group ->
GroupOutputKDB(group, outputStream).output()
mGroupList.forEach { group ->
if (group != mDatabaseKDB.rootGroup) {
GroupOutputKDB(group, outputStream).output()
}
}
// Entries
mDatabaseKDB.doForEachEntryInIndex { entry ->
mEntryList.forEach { entry ->
EntryOutputKDB(mDatabaseKDB, entry, outputStream).output()
}
}
private fun sortGroupsForOutput() {
val groupList = ArrayList<GroupKDB>()
// Rebuild list according to sorting order removing any orphaned groups
for (rootGroup in mDatabaseKDB.rootGroups) {
sortGroup(rootGroup, groupList)
}
mDatabaseKDB.setGroupIndexes(groupList)
private fun clearParser() {
mGroupList.clear()
mEntryList.clear()
}
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
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
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 com.kunzisoft.encrypt.StreamCipher
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.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.element.*
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.HmacBlockOutputStream
import com.kunzisoft.keepass.utils.*
import org.joda.time.DateTime
import org.xmlpull.v1.XmlSerializer
import java.io.IOException
import java.io.OutputStream
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.util.*
import java.util.zip.GZIPOutputStream
@@ -70,18 +66,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private var header: DatabaseHeaderKDBX? = null
private var hashOfHeader: ByteArray? = null
private var headerHmac: ByteArray? = null
private var engine: CipherEngine? = null
@Throws(DatabaseOutputException::class)
override fun output() {
try {
try {
engine = EncryptionAlgorithm.getFrom(mDatabaseKDBX.cipherUuid).cipherEngine
} catch (e: NoSuchAlgorithmException) {
throw DatabaseOutputException("No such cipher", e)
}
header = outputHeader(mOutputStream)
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)))
}
writeDateInstant(DatabaseKDBXXML.ElemSettingsChanged, mDatabaseKDBX.settingsChanged)
writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true)
writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged)
writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true)
@@ -280,7 +270,10 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun attachStreamEncryptor(header: DatabaseHeaderKDBX, os: OutputStream): CipherOutputStream {
val cipher: Cipher
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) {
throw DatabaseOutputException("Invalid algorithm.", e)
}
@@ -293,7 +286,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
val random = super.setIVs(header)
random.nextBytes(header.masterSeed)
val ivLength = engine!!.ivLength()
val ivLength = mDatabaseKDBX.encryptionAlgorithm.cipherEngine.ivLength()
if (ivLength != header.encryptionIV.size) {
header.encryptionIV = ByteArray(ivLength)
}
@@ -592,7 +585,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)
writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.getDeletionTime())
writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime)
xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
}
@@ -618,7 +611,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeDeletedObjects(value: List<DeletedObject>) {
private fun writeDeletedObjects(value: Collection<DeletedObject>) {
xml.startTag(null, DatabaseKDBXXML.ElemDeletedObjects)
for (pdo in value) {
@@ -765,7 +758,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
var character: Char
for (element in text) {
character = element
val hexChar = character.toInt()
val hexChar = character.code
if (
hexChar in 0x20..0xD7FF ||
hexChar == 0x9 ||

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())
.outerCircleAlpha(getCircleAlpha())
.textColorInt(getTextColor())
.tintTarget(true)
.tintTarget(false)
.cancelable(true),
object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) {

View File

@@ -38,9 +38,9 @@ class PasswordActivityEducation(activity: Activity)
activity.getString(R.string.education_unlock_summary))
.outerCircleColorInt(getCircleColor())
.outerCircleAlpha(getCircleAlpha())
.icon(ContextCompat.getDrawable(activity, R.mipmap.ic_launcher_round))
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_lock_open_white_24dp))
.textColorInt(getTextColor())
.tintTarget(false)
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
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/>.
*
*/
@file:Suppress("DEPRECATION")
package com.kunzisoft.keepass.magikeyboard
import android.content.Context
import android.content.Intent
import android.inputmethodservice.InputMethodService
import android.inputmethodservice.Keyboard
import android.inputmethodservice.KeyboardView
import android.media.AudioManager
import android.os.Build
import android.util.Log
@@ -99,7 +96,6 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
popupCustomKeys = PopupWindow(context).apply {
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
inputMethodMode = PopupWindow.INPUT_METHOD_NEEDED
contentView = popupFieldsView
}
@@ -130,8 +126,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
removeEntryInfo()
}
assignKeyboardView()
keyboardView?.setOnKeyboardActionListener(this)
keyboardView?.isPreviewEnabled = false
keyboardView?.onKeyboardActionListener = this
return rootKeyboardView
}
@@ -206,6 +201,7 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
switchToPreviousInputMethod()
} else {
@Suppress("DEPRECATION")
window.window?.let { window ->
imeManager?.switchToLastInputMethod(window.attributes.token)
}

View File

@@ -38,6 +38,8 @@ class EntryInfo : NodeInfo {
var url: String = ""
var notes: String = ""
var tags: Tags = Tags()
var backgroundColor: Int? = null
var foregroundColor: Int? = null
var customFields: MutableList<Field> = mutableListOf()
var attachments: MutableList<Attachment> = mutableListOf()
var otpModel: OtpModel? = null
@@ -52,6 +54,10 @@ class EntryInfo : NodeInfo {
url = parcel.readString() ?: url
notes = parcel.readString() ?: notes
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(attachments, Attachment::class.java.classLoader)
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
@@ -70,6 +76,8 @@ class EntryInfo : NodeInfo {
parcel.writeString(url)
parcel.writeString(notes)
parcel.writeParcelable(tags, flags)
parcel.writeInt(backgroundColor ?: -1)
parcel.writeInt(foregroundColor ?: -1)
parcel.writeList(customFields)
parcel.writeList(attachments)
parcel.writeParcelable(otpModel, flags)
@@ -197,6 +205,8 @@ class EntryInfo : NodeInfo {
if (url != other.url) return false
if (notes != other.notes) 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 (attachments != other.attachments) return false
if (otpModel != other.otpModel) return false
@@ -213,6 +223,8 @@ class EntryInfo : NodeInfo {
result = 31 * result + url.hashCode()
result = 31 * result + notes.hashCode()
result = 31 * result + tags.hashCode()
result = 31 * result + backgroundColor.hashCode()
result = 31 * result + foregroundColor.hashCode()
result = 31 * result + customFields.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + (otpModel?.hashCode() ?: 0)

View File

@@ -1,12 +1,15 @@
package com.kunzisoft.keepass.model
import android.os.Parcel
import android.os.ParcelUuid
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.FOLDER_ID
import java.util.*
class GroupInfo : NodeInfo {
var id: UUID? = null
var notes: String? = null
init {
@@ -16,11 +19,14 @@ class GroupInfo : NodeInfo {
constructor(): super()
constructor(parcel: Parcel): super(parcel) {
id = parcel.readParcelable<ParcelUuid>(ParcelUuid::class.java.classLoader)?.uuid ?: id
notes = parcel.readString()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
super.writeToParcel(parcel, flags)
val uuid = if (id != null) ParcelUuid(id) else null
parcel.writeParcelable(uuid, flags)
parcel.writeString(notes)
}
@@ -29,6 +35,7 @@ class GroupInfo : NodeInfo {
if (other !is GroupInfo) return false
if (!super.equals(other)) return false
if (id != other.id) return false
if (notes != other.notes) return false
return true
@@ -36,6 +43,7 @@ class GroupInfo : NodeInfo {
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (id?.hashCode() ?: 0)
result = 31 * result + (notes?.hashCode() ?: 0)
return result
}

Some files were not shown because too many files have changed in this diff Show More