diff --git a/CHANGELOG b/CHANGELOG index d7cec880d..7d88f9a23 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ KeePassDX(2.9.9) - * + * Detect file changes and reload database #794 + * Inline suggestions autofill with compatible keyboard (Android R) #827 + * Add Keyfile XML version 2 #844 + * Fix binaries of 64 bytes #835 KeePassDX(2.9.8) * Fix specific attachments with kdbx3.1 databases #828 diff --git a/app/build.gradle b/app/build.gradle index 368d42cbb..d4b530ecc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,7 +5,7 @@ apply plugin: 'kotlin-kapt' android { compileSdkVersion 30 - buildToolsVersion '30.0.2' + buildToolsVersion '30.0.3' ndkVersion '21.3.6528147' defaultConfig { @@ -110,6 +110,8 @@ dependencies { // Database implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" + // Autofill + implementation "androidx.autofill:autofill:1.1.0-rc01" // Crypto implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01' // Time diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt index bfa581810..dd1433aed 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt @@ -26,6 +26,7 @@ import android.content.Intent import android.content.IntentSender import android.os.Build import android.os.Bundle +import android.view.inputmethod.InlineSuggestionsRequest import android.widget.Toast import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity @@ -33,6 +34,7 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.autofill.AutofillHelper +import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST import com.kunzisoft.keepass.autofill.KeeAutofillService import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.search.SearchHelper @@ -40,7 +42,6 @@ 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 -import com.kunzisoft.keepass.utils.UriUtil @RequiresApi(api = Build.VERSION_CODES.O) class AutofillLauncherActivity : AppCompatActivity() { @@ -84,9 +85,9 @@ class AutofillLauncherActivity : AppCompatActivity() { private fun launchSelection(searchInfo: SearchInfo) { // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) - val assistStructure = AutofillHelper.retrieveAssistStructure(intent) + val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent) - if (assistStructure == null) { + if (autofillComponent == null) { setResult(Activity.RESULT_CANCELED) finish() } else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId, @@ -105,21 +106,21 @@ class AutofillLauncherActivity : AppCompatActivity() { searchInfo, { items -> // Items found - AutofillHelper.buildResponse(this, items) + AutofillHelper.buildResponseAndSetResult(this, items) finish() }, { // Show the database UI to select the entry GroupActivity.launchForAutofillResult(this, readOnly, - assistStructure, + autofillComponent, searchInfo, false) }, { // If database not open FileDatabaseSelectActivity.launchForAutofillResult(this, - assistStructure, + autofillComponent, searchInfo) } ) @@ -196,7 +197,8 @@ class AutofillLauncherActivity : AppCompatActivity() { private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" fun getAuthIntentSenderForSelection(context: Context, - searchInfo: SearchInfo? = null): IntentSender { + searchInfo: SearchInfo? = null, + inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender { return PendingIntent.getActivity(context, 0, // Doesn't work with Parcelable (don't know why?) Intent(context, AutofillLauncherActivity::class.java).apply { @@ -205,6 +207,11 @@ class AutofillLauncherActivity : AppCompatActivity() { putExtra(KEY_SEARCH_DOMAIN, it.webDomain) putExtra(KEY_SEARCH_SCHEME, it.webScheme) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + inlineSuggestionsRequest?.let { + putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it) + } + } }, PendingIntent.FLAG_CANCEL_CURRENT).intentSender } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index ef526b953..10005f94a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -39,6 +39,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.appbar.CollapsingToolbarLayout import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper +import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.database.element.Attachment @@ -53,6 +54,7 @@ import com.kunzisoft.keepass.model.StreamDirection import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager @@ -151,6 +153,10 @@ class EntryActivity : LockingActivity() { if (result.isSuccess) finish() } + ACTION_DATABASE_RELOAD_TASK -> { + // Close the current activity + finish() + } } coordinatorLayout?.showActionError(result) } @@ -408,6 +414,9 @@ class EntryActivity : LockingActivity() { menu.findItem(R.id.menu_save_database)?.isVisible = false menu.findItem(R.id.menu_edit)?.isVisible = false } + if (mSpecialMode != SpecialMode.DEFAULT) { + menu.findItem(R.id.menu_reload_database)?.isVisible = false + } val gotoUrl = menu.findItem(R.id.menu_goto_url) gotoUrl?.apply { @@ -501,6 +510,9 @@ class EntryActivity : LockingActivity() { R.id.menu_save_database -> { mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly) } + R.id.menu_reload_database -> { + mProgressDatabaseTaskProvider?.startDatabaseReload(false) + } android.R.id.home -> finish() // close this activity and return to preview activity (if there is any) } return super.onOptionsItemSelected(item) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index 5134f1ffb..90909200d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -21,7 +21,6 @@ package com.kunzisoft.keepass.activities import android.app.Activity import android.app.DatePickerDialog import android.app.TimePickerDialog -import android.app.assist.AssistStructure import android.content.Context import android.content.Intent import android.net.Uri @@ -49,6 +48,7 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.SelectFileHelper import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged +import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.icon.IconImage @@ -61,6 +61,7 @@ import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService import com.kunzisoft.keepass.otp.OtpElement @@ -335,6 +336,10 @@ class EntryEditActivity : LockingActivity(), Log.e(TAG, "Unable to retrieve entry after database action", e) } } + ACTION_DATABASE_RELOAD_TASK -> { + // Close the current activity + finish() + } } coordinatorLayout?.showActionError(result) } @@ -361,7 +366,7 @@ class EntryEditActivity : LockingActivity(), // Build Autofill response with the entry selected if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mDatabase?.let { database -> - AutofillHelper.buildResponse(this@EntryEditActivity, + AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity, entry.getEntryInfo(database)) } } @@ -610,13 +615,7 @@ class EntryEditActivity : LockingActivity(), override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) - - val inflater = menuInflater - inflater.inflate(R.menu.database, menu) - // Save database not needed here - menu.findItem(R.id.menu_save_database)?.isVisible = false - MenuUtil.contributionMenuInflater(inflater, menu) - + MenuUtil.contributionMenuInflater(menuInflater, menu) return true } @@ -673,9 +672,6 @@ class EntryEditActivity : LockingActivity(), override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_save_database -> { - mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly) - } R.id.menu_contribute -> { MenuUtil.onContributionItemSelected(this) return true @@ -909,7 +905,7 @@ class EntryEditActivity : LockingActivity(), */ @RequiresApi(api = Build.VERSION_CODES.O) fun launchForAutofillResult(activity: Activity, - assistStructure: AssistStructure, + autofillComponent: AutofillComponent, group: Group, searchInfo: SearchInfo? = null) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { @@ -917,7 +913,7 @@ class EntryEditActivity : LockingActivity(), intent.putExtra(KEY_PARENT, group.nodeId) AutofillHelper.startActivityForAutofillResult(activity, intent, - assistStructure, + autofillComponent, searchInfo) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index 17bcea82d..a4d313091 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.activities import android.app.Activity -import android.app.assist.AssistStructure import android.content.Context import android.content.Intent import android.net.Uri @@ -48,6 +47,7 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.selection.SpecialModeActivity import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction +import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database @@ -501,11 +501,11 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), @RequiresApi(api = Build.VERSION_CODES.O) fun launchForAutofillResult(activity: Activity, - assistStructure: AssistStructure, + autofillComponent: AutofillComponent, searchInfo: SearchInfo? = null) { AutofillHelper.startActivityForAutofillResult(activity, Intent(activity, FileDatabaseSelectActivity::class.java), - assistStructure, + autofillComponent, searchInfo) } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index 28fa61cb8..2ea93ef09 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.activities import android.app.Activity import android.app.SearchManager -import android.app.assist.AssistStructure import android.content.ComponentName import android.content.Context import android.content.Intent @@ -52,6 +51,7 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter +import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Entry @@ -70,6 +70,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY @@ -228,10 +229,10 @@ class GroupActivity : LockingActivity(), currentGroup, searchInfo) onLaunchActivitySpecialMode() }, - { searchInfo, assistStructure -> + { searchInfo, autofillComponent -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { EntryEditActivity.launchForAutofillResult(this@GroupActivity, - assistStructure, + autofillComponent, currentGroup, searchInfo) onLaunchActivitySpecialMode() } else { @@ -342,6 +343,12 @@ class GroupActivity : LockingActivity(), } } } + ACTION_DATABASE_RELOAD_TASK -> { + // Reload the current activity + startActivity(intent) + finish() + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + } } coordinatorLayout?.showActionError(result) @@ -665,7 +672,7 @@ class GroupActivity : LockingActivity(), // Build response with the entry selected if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) { mDatabase?.let { database -> - AutofillHelper.buildResponse(this, + AutofillHelper.buildResponseAndSetResult(this, entry.getEntryInfo(database)) } } @@ -878,6 +885,8 @@ class GroupActivity : LockingActivity(), } if (mSpecialMode == SpecialMode.DEFAULT) { MenuUtil.defaultMenuInflater(inflater, menu) + } else { + menu.findItem(R.id.menu_reload_database)?.isVisible = false } // Menu for recycle bin @@ -1003,6 +1012,10 @@ class GroupActivity : LockingActivity(), mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly) return true } + R.id.menu_reload_database -> { + mProgressDatabaseTaskProvider?.startDatabaseReload(false) + return true + } R.id.menu_empty_recycle_bin -> { mCurrentGroup?.getChildren()?.let { listChildren -> // Automatically delete all elements @@ -1310,14 +1323,14 @@ class GroupActivity : LockingActivity(), @RequiresApi(api = Build.VERSION_CODES.O) fun launchForAutofillResult(activity: Activity, readOnly: Boolean, - assistStructure: AssistStructure, + autofillComponent: AutofillComponent, searchInfo: SearchInfo? = null, autoSearch: Boolean = false) { checkTimeAndBuildIntent(activity, null, readOnly) { intent -> intent.putExtra(AUTO_SEARCH_KEY, autoSearch) AutofillHelper.startActivityForAutofillResult(activity, intent, - assistStructure, + autofillComponent, searchInfo) } } @@ -1434,21 +1447,21 @@ class GroupActivity : LockingActivity(), } ) }, - { searchInfo, assistStructure -> + { searchInfo, autofillComponent -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { SearchHelper.checkAutoSearchInfo(activity, Database.getInstance(), searchInfo, { items -> // Response is build - AutofillHelper.buildResponse(activity, items) + AutofillHelper.buildResponseAndSetResult(activity, items) onValidateSpecialMode() }, { // Here no search info found, disable auto search GroupActivity.launchForAutofillResult(activity, readOnly, - assistStructure, + autofillComponent, searchInfo, false) onLaunchActivitySpecialMode() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt index b2b491638..fd514a1a7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.activities import android.app.Activity -import android.app.assist.AssistStructure import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -49,6 +48,7 @@ import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.selection.SpecialModeActivity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity +import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider @@ -720,7 +720,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil when (resultCode) { LockingActivity.RESULT_EXIT_LOCK -> { clearCredentialsViews() - Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this)) + Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this)) } Activity.RESULT_CANCELED -> { clearCredentialsViews() @@ -838,13 +838,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil fun launchForAutofillResult(activity: Activity, databaseFile: Uri, keyFile: Uri?, - assistStructure: AssistStructure, + autofillComponent: AutofillComponent, searchInfo: SearchInfo?) { buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> AutofillHelper.startActivityForAutofillResult( activity, intent, - assistStructure, + autofillComponent, searchInfo) } } @@ -902,11 +902,11 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil searchInfo) onLaunchActivitySpecialMode() }, - { searchInfo, assistStructure -> // Autofill Selection Action + { searchInfo, autofillComponent -> // Autofill Selection Action if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { PasswordActivity.launchForAutofillResult(activity, databaseUri, keyFile, - assistStructure, + autofillComponent, searchInfo) onLaunchActivitySpecialMode() } else { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt new file mode 100644 index 000000000..6a83f0df7 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2020 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 . + * + */ +package com.kunzisoft.keepass.activities.dialogs + +import android.app.Dialog +import android.os.Bundle +import android.text.SpannableStringBuilder +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.model.SnapFileDatabaseInfo + + +class DatabaseChangedDialogFragment : DialogFragment() { + + var actionDatabaseListener: ActionDatabaseChangedListener? = null + + override fun onPause() { + super.onPause() + actionDatabaseListener = null + this.dismiss() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + activity?.let { activity -> + + val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(OLD_FILE_DATABASE_INFO) + val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(NEW_FILE_DATABASE_INFO) + + if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) { + // Use the Builder class for convenient dialog construction + val builder = AlertDialog.Builder(activity) + + val stringBuilder = SpannableStringBuilder() + if (newSnapFileDatabaseInfo.exists) { + stringBuilder.append(getString(R.string.warning_database_info_changed)) + stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity) + + "\n→\n" + + newSnapFileDatabaseInfo.toString(activity) + "\n\n") + stringBuilder.append(getString(R.string.warning_database_info_changed_options)) + } else { + stringBuilder.append(getString(R.string.warning_database_revoked)) + } + builder.setMessage(stringBuilder) + builder.setPositiveButton(android.R.string.ok) { _, _ -> + actionDatabaseListener?.validateDatabaseChanged() + } + return builder.create() + } + } + return super.onCreateDialog(savedInstanceState) + } + + interface ActionDatabaseChangedListener { + fun validateDatabaseChanged() + } + + companion object { + + const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment" + private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO" + private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO" + + fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo, + newSnapFileDatabaseInfo: SnapFileDatabaseInfo) + : DatabaseChangedDialogFragment { + val fragment = DatabaseChangedDialogFragment() + fragment.arguments = Bundle().apply { + putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo) + putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo) + } + return fragment + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt index 23d9b3d51..c9ef7e7e7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt @@ -19,10 +19,10 @@ */ package com.kunzisoft.keepass.activities.helpers -import android.app.assist.AssistStructure import android.content.Context import android.content.Intent import android.os.Build +import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo @@ -106,7 +106,7 @@ object EntrySelectionHelper { fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (AutofillHelper.retrieveAssistStructure(intent) != null) + if (AutofillHelper.retrieveAutofillComponent(intent) != null) return SpecialMode.SELECTION } return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode? @@ -119,7 +119,7 @@ object EntrySelectionHelper { fun retrieveTypeModeFromIntent(intent: Intent): TypeMode { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (AutofillHelper.retrieveAssistStructure(intent) != null) + if (AutofillHelper.retrieveAutofillComponent(intent) != null) return TypeMode.AUTOFILL } return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT @@ -136,7 +136,7 @@ object EntrySelectionHelper { saveAction: (searchInfo: SearchInfo) -> Unit, keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit, autofillSelectionAction: (searchInfo: SearchInfo?, - assistStructure: AssistStructure) -> Unit, + autofillComponent: AutofillComponent) -> Unit, autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) { when (retrieveSpecialModeFromIntent(intent)) { @@ -167,14 +167,14 @@ object EntrySelectionHelper { } SpecialMode.SELECTION -> { val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent) - var assistStructureInit = false + var autofillComponentInit = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure -> - autofillSelectionAction.invoke(searchInfo, assistStructure) - assistStructureInit = true + AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent -> + autofillSelectionAction.invoke(searchInfo, autofillComponent) + autofillComponentInit = true } } - if (!assistStructureInit) { + if (!autofillComponentInit) { if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) { when (retrieveTypeModeFromIntent(intent)) { TypeMode.DEFAULT -> { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/selection/SpecialModeActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/selection/SpecialModeActivity.kt index 91c8e5b2f..bc9ce9365 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/selection/SpecialModeActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/selection/SpecialModeActivity.kt @@ -18,7 +18,7 @@ import com.kunzisoft.keepass.view.SpecialModeView abstract class SpecialModeActivity : StylishActivity() { protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT - protected var mTypeMode: TypeMode = TypeMode.DEFAULT + private var mTypeMode: TypeMode = TypeMode.DEFAULT private var mSpecialModeView: SpecialModeView? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/app/App.kt b/app/src/main/java/com/kunzisoft/keepass/app/App.kt index 8094648b6..5207b1e51 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/App.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/App.kt @@ -34,7 +34,7 @@ class App : MultiDexApplication() { } override fun onTerminate() { - Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this)) + Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this)) super.onTerminate() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt index 6b8c22781..9c7242fb9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/FileDatabaseHistoryAction.kt @@ -47,7 +47,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""), fileDatabaseInfo.exists, - fileDatabaseInfo.getModificationString(), + fileDatabaseInfo.getLastModificationString(), fileDatabaseInfo.getSizeString() ) }, @@ -90,7 +90,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { UriUtil.decode(fileDatabaseHistoryEntity.databaseUri), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias), fileDatabaseInfo.exists, - fileDatabaseInfo.getModificationString(), + fileDatabaseInfo.getLastModificationString(), fileDatabaseInfo.getSizeString() ) ) @@ -152,7 +152,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) { UriUtil.decode(fileDatabaseHistory.databaseUri), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias), fileDatabaseInfo.exists, - fileDatabaseInfo.getModificationString(), + fileDatabaseInfo.getLastModificationString(), fileDatabaseInfo.getSizeString() ) } diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt new file mode 100644 index 000000000..2043b9705 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt @@ -0,0 +1,7 @@ +package com.kunzisoft.keepass.autofill + +import android.app.assist.AssistStructure +import android.view.inputmethod.InlineSuggestionsRequest + +data class AutofillComponent(val assistStructure: AssistStructure, + val inlineSuggestionsRequest: InlineSuggestionsRequest?) \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt index 5a1a64d1d..f169c2655 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt @@ -19,18 +19,27 @@ */ package com.kunzisoft.keepass.autofill +import android.annotation.SuppressLint import android.app.Activity +import android.app.PendingIntent import android.app.assist.AssistStructure import android.content.Context import android.content.Intent +import android.graphics.BlendMode +import android.graphics.drawable.Icon import android.os.Build import android.service.autofill.Dataset import android.service.autofill.FillResponse +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 androidx.annotation.RequiresApi +import androidx.autofill.inline.UiVersions +import androidx.autofill.inline.v1.InlineSuggestionUi import androidx.core.content.ContextCompat import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper @@ -38,8 +47,11 @@ 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.icons.assignDatabaseIcon +import com.kunzisoft.keepass.icons.createIconFromDatabaseIcon import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.SearchInfo +import com.kunzisoft.keepass.settings.AutofillSettingsActivity +import com.kunzisoft.keepass.settings.PreferencesUtil @RequiresApi(api = Build.VERSION_CODES.O) @@ -47,11 +59,13 @@ object AutofillHelper { private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165 - private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE + private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE + const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST" - fun retrieveAssistStructure(intent: Intent?): AssistStructure? { - intent?.let { - return it.getParcelableExtra(ASSIST_STRUCTURE) + fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? { + intent?.getParcelableExtra(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure -> + return AutofillComponent(assistStructure, + intent.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST)) } return null } @@ -68,26 +82,10 @@ object AutofillHelper { return "" } - internal fun addHeader(responseBuilder: FillResponse.Builder, - packageName: String, - webDomain: String?, - applicationId: String?) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - if (webDomain != null) { - responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply { - setTextViewText(R.id.autofill_web_domain_text, webDomain) - }) - } else if (applicationId != null) { - responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply { - setTextViewText(R.id.autofill_app_id_text, applicationId) - }) - } - } - } - - internal fun buildDataset(context: Context, + private fun buildDataset(context: Context, entryInfo: EntryInfo, - struct: StructureParser.Result): Dataset? { + struct: StructureParser.Result, + inlinePresentation: InlinePresentation?): Dataset? { val title = makeEntryTitle(entryInfo) val views = newRemoteViews(context, title, entryInfo.icon) val builder = Dataset.Builder(views) @@ -100,6 +98,12 @@ object AutofillHelper { builder.setValue(password, AutofillValue.forText(entryInfo.password)) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + inlinePresentation?.let { + builder.setInlinePresentation(it) + } + } + return try { builder.build() } catch (e: IllegalArgumentException) { @@ -108,44 +112,116 @@ object AutofillHelper { } } + @RequiresApi(Build.VERSION_CODES.R) + @SuppressLint("RestrictedApi") + private fun buildInlinePresentationForEntry(context: Context, + inlineSuggestionsRequest: InlineSuggestionsRequest, + positionItem: Int, + entryInfo: EntryInfo): InlinePresentation? { + val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs + val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount + + 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 + + // Build the content for IME UI + val pendingIntent = PendingIntent.getActivity(context, + 0, + Intent(context, AutofillSettingsActivity::class.java), + 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) + }) + buildIconFromEntry(context, entryInfo)?.let { icon -> + setEndIcon(icon.apply { + setTintBlendMode(BlendMode.DST) + }) + } + }.build().slice, inlinePresentationSpec, false) + } + return null + } + + fun buildResponse(context: Context, + entriesInfo: List, + parseResult: StructureParser.Result, + inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse { + val responseBuilder = FillResponse.Builder() + // Add Header + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val packageName = context.packageName + parseResult.webDomain?.let { webDomain -> + responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply { + setTextViewText(R.id.autofill_web_domain_text, webDomain) + }) + } ?: kotlin.run { + parseResult.applicationId?.let { applicationId -> + responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply { + setTextViewText(R.id.autofill_app_id_text, applicationId) + }) + } + } + } + // Add inline suggestion for new IME and dataset + entriesInfo.forEachIndexed { index, entryInfo -> + val inlinePresentation = inlineSuggestionsRequest?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + buildInlinePresentationForEntry(context, inlineSuggestionsRequest, index, entryInfo) + } else { + null + } + } + responseBuilder.addDataset(buildDataset(context, entryInfo, parseResult, inlinePresentation)) + } + return responseBuilder.build() + } + /** * Build the Autofill response for one entry */ - fun buildResponse(activity: Activity, entryInfo: EntryInfo) { - buildResponse(activity, ArrayList().apply { add(entryInfo) }) + fun buildResponseAndSetResult(activity: Activity, entryInfo: EntryInfo) { + buildResponseAndSetResult(activity, ArrayList().apply { add(entryInfo) }) } /** * Build the Autofill response for many entry */ - fun buildResponse(activity: Activity, entriesInfo: List) { + fun buildResponseAndSetResult(activity: Activity, entriesInfo: List) { if (entriesInfo.isEmpty()) { activity.setResult(Activity.RESULT_CANCELED) } else { var setResultOk = false - activity.intent?.extras?.let { extras -> - if (extras.containsKey(ASSIST_STRUCTURE)) { - activity.intent?.getParcelableExtra(ASSIST_STRUCTURE)?.let { structure -> - StructureParser(structure).parse()?.let { result -> - // New Response - val responseBuilder = FillResponse.Builder() - entriesInfo.forEach { - responseBuilder.addDataset(buildDataset(activity, it, result)) - } - val mReplyIntent = Intent() - Log.d(activity.javaClass.name, "Successed Autofill auth.") - mReplyIntent.putExtra( - AutofillManager.EXTRA_AUTHENTICATION_RESULT, - responseBuilder.build()) - setResultOk = true - activity.setResult(Activity.RESULT_OK, mReplyIntent) - } + activity.intent?.getParcelableExtra(EXTRA_ASSIST_STRUCTURE)?.let { structure -> + StructureParser(structure).parse()?.let { result -> + // New Response + val inlineSuggestionsRequest = activity.intent?.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST) + val response = buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest) + if (inlineSuggestionsRequest != null) { + Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show() } + val mReplyIntent = Intent() + Log.d(activity.javaClass.name, "Successed Autofill auth.") + mReplyIntent.putExtra( + AutofillManager.EXTRA_AUTHENTICATION_RESULT, + response) + setResultOk = true + activity.setResult(Activity.RESULT_OK, mReplyIntent) } - if (!setResultOk) { - Log.w(activity.javaClass.name, "Failed Autofill auth.") - activity.setResult(Activity.RESULT_CANCELED) - } + } + if (!setResultOk) { + Log.w(activity.javaClass.name, "Failed Autofill auth.") + activity.setResult(Activity.RESULT_CANCELED) } } } @@ -155,10 +231,16 @@ object AutofillHelper { */ fun startActivityForAutofillResult(activity: Activity, intent: Intent, - assistStructure: AssistStructure, + autofillComponent: AutofillComponent, searchInfo: SearchInfo?) { EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION) - intent.putExtra(ASSIST_STRUCTURE, assistStructure) + intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) { + autofillComponent.inlineSuggestionsRequest?.let { + intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it) + } + } EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo) activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE) } @@ -192,4 +274,11 @@ object AutofillHelper { } return presentation } + + private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? { + return createIconFromDatabaseIcon(context, + Database.getInstance().drawFactory, + entryInfo.icon, + ContextCompat.getColor(context, R.color.green)) + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt index 70801c75c..cccbe277e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt @@ -19,37 +19,50 @@ */ package com.kunzisoft.keepass.autofill +import android.app.PendingIntent +import android.content.Intent +import android.graphics.BlendMode +import android.graphics.drawable.Icon import android.os.Build import android.os.CancellationSignal import android.service.autofill.* import android.util.Log import android.view.autofill.AutofillId +import android.view.inputmethod.InlineSuggestionsRequest import android.widget.RemoteViews import androidx.annotation.RequiresApi +import androidx.autofill.inline.UiVersions +import androidx.autofill.inline.v1.InlineSuggestionUi import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.AutofillLauncherActivity 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.AutofillSettingsActivity import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.utils.UriUtil import java.util.concurrent.atomic.AtomicBoolean + @RequiresApi(api = Build.VERSION_CODES.O) class KeeAutofillService : AutofillService() { var applicationIdBlocklist: Set? = null var webDomainBlocklist: Set? = null var askToSaveData: Boolean = false + var autofillInlineSuggestionsEnabled: Boolean = false private var mLock = AtomicBoolean() override fun onCreate() { super.onCreate() + getPreferences() + } + private fun getPreferences() { applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this) webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this) - askToSaveData = PreferencesUtil.askToSaveAutofillData(this) // TODO apply when changed + askToSaveData = PreferencesUtil.askToSaveAutofillData(this) + autofillInlineSuggestionsEnabled = PreferencesUtil.isAutofillInlineSuggestionsEnable(this) } override fun onFillRequest(request: FillRequest, @@ -75,7 +88,16 @@ class KeeAutofillService : AutofillService() { } SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain -> searchInfo.webDomain = webDomainWithoutSubDomain - launchSelection(searchInfo, parseResult, callback) + val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && autofillInlineSuggestionsEnabled) { + request.inlineSuggestionsRequest + } else { + null + } + launchSelection(searchInfo, + parseResult, + inlineSuggestionsRequest, + callback) } } } @@ -84,39 +106,40 @@ class KeeAutofillService : AutofillService() { private fun launchSelection(searchInfo: SearchInfo, parseResult: StructureParser.Result, + inlineSuggestionsRequest: InlineSuggestionsRequest?, callback: FillCallback) { SearchHelper.checkAutoSearchInfo(this, Database.getInstance(), searchInfo, { items -> - val responseBuilder = FillResponse.Builder() - AutofillHelper.addHeader(responseBuilder, packageName, - parseResult.webDomain, parseResult.applicationId) - items.forEach { - responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult)) - } - callback.onSuccess(responseBuilder.build()) + callback.onSuccess( + AutofillHelper.buildResponse(this, + items, parseResult, inlineSuggestionsRequest) + ) }, { // Show UI if no search result - showUIForEntrySelection(parseResult, searchInfo, callback) + showUIForEntrySelection(parseResult, + searchInfo, inlineSuggestionsRequest, callback) }, { // Show UI if database not open - showUIForEntrySelection(parseResult, searchInfo, callback) + showUIForEntrySelection(parseResult, + searchInfo, inlineSuggestionsRequest, callback) } ) } private fun showUIForEntrySelection(parseResult: StructureParser.Result, searchInfo: SearchInfo, + inlineSuggestionsRequest: InlineSuggestionsRequest?, callback: FillCallback) { parseResult.allAutofillIds().let { autofillIds -> if (autofillIds.isNotEmpty()) { // If the entire Autofill Response is authenticated, AuthActivity is used // to generate Response. val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this, - searchInfo) + searchInfo, inlineSuggestionsRequest) val responseBuilder = FillResponse.Builder() val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) { RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply { @@ -149,8 +172,45 @@ class KeeAutofillService : AutofillService() { ) } } + + // Build inline presentation + var inlinePresentation: InlinePresentation? = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && autofillInlineSuggestionsEnabled) { + inlineSuggestionsRequest?.let { + val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs + if (inlineSuggestionsRequest.maxSuggestionCount > 0 + && inlinePresentationSpecs.size > 0) { + val inlinePresentationSpec = inlinePresentationSpecs[0] + + // 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)) { + // Build the content for IME UI + inlinePresentation = InlinePresentation( + InlineSuggestionUi.newContentBuilder( + PendingIntent.getActivity(this, + 0, + Intent(this, AutofillSettingsActivity::class.java), + 0) + ).apply { + setContentDescription(getString(R.string.autofill_sign_in_prompt)) + setTitle(getString(R.string.autofill_sign_in_prompt)) + setStartIcon(Icon.createWithResource(this@KeeAutofillService, R.mipmap.ic_launcher_round).apply { + setTintBlendMode(BlendMode.DST) + }) + }.build().slice, inlinePresentationSpec, false) + } + } + } + } + // Build response - responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation) + } else { + responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock) + } callback.onSuccess(responseBuilder.build()) } } @@ -190,6 +250,7 @@ class KeeAutofillService : AutofillService() { override fun onConnected() { Log.d(TAG, "onConnected") + getPreferences() } override fun onDisconnected() { diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt index dd6ee4cb4..c93cb4a36 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt @@ -33,7 +33,7 @@ import java.util.* * Parse AssistStructure and guess username and password fields. */ @RequiresApi(api = Build.VERSION_CODES.O) -internal class StructureParser(private val structure: AssistStructure) { +class StructureParser(private val structure: AssistStructure) { private var result: Result? = null private var usernameNeeded = true @@ -274,7 +274,7 @@ internal class StructureParser(private val structure: AssistStructure) { } @RequiresApi(api = Build.VERSION_CODES.O) - internal class Result { + class Result { var applicationId: String? = null var webDomain: String? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt index 7b7f90d0b..f2763f30a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/CreateDatabaseRunnable.kt @@ -26,7 +26,6 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.UriUtil -import com.kunzisoft.keepass.utils.closeDatabase class CreateDatabaseRunnable(context: Context, private val mDatabase: Database, @@ -47,7 +46,7 @@ class CreateDatabaseRunnable(context: Context, createData(mDatabaseUri, databaseName, rootName) } } catch (e: Exception) { - mDatabase.closeAndClear(UriUtil.getBinaryDir(context)) + mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) setError(e) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt index 5f9d0ce04..f732f3779 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/LoadDatabaseRunnable.kt @@ -31,7 +31,6 @@ import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.utils.UriUtil -import com.kunzisoft.keepass.utils.closeDatabase class LoadDatabaseRunnable(private val context: Context, private val mDatabase: Database, @@ -47,7 +46,7 @@ class LoadDatabaseRunnable(private val context: Context, override fun onStartRun() { // Clear before we load - mDatabase.closeAndClear(UriUtil.getBinaryDir(context)) + mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) } override fun onActionRun() { @@ -59,9 +58,6 @@ class LoadDatabaseRunnable(private val context: Context, mFixDuplicateUUID, progressTaskUpdater) } - catch (e: DuplicateUuidDatabaseException) { - setError(e) - } catch (e: LoadDatabaseException) { setError(e) } @@ -83,7 +79,7 @@ class LoadDatabaseRunnable(private val context: Context, // Register the current time to init the lock timer PreferencesUtil.saveCurrentTime(context) } else { - mDatabase.closeAndClear(UriUtil.getBinaryDir(context)) + mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt index 279b40e27..534067403 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ProgressDatabaseTaskProvider.kt @@ -26,6 +26,8 @@ import android.net.Uri import android.os.Bundle import android.os.IBinder import androidx.fragment.app.FragmentActivity +import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment +import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.database.element.Entry @@ -35,6 +37,7 @@ import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm +import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK @@ -44,6 +47,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY @@ -84,6 +88,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { private var serviceConnection: ServiceConnection? = null private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null + private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener { override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) { @@ -101,6 +106,28 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { } } + private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener { + override fun validateDatabaseChanged() { + mBinder?.getService()?.saveDatabaseInfo() + } + } + + private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener { + override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo, + newDatabaseInfo: SnapFileDatabaseInfo) { + if (databaseChangedDialogFragment == null) { + databaseChangedDialogFragment = activity.supportFragmentManager + .findFragmentByTag(DATABASE_CHANGED_DIALOG_TAG) as DatabaseChangedDialogFragment? + databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener + } + if (progressTaskDialogFragment == null) { + databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(previousDatabaseInfo, newDatabaseInfo) + databaseChangedDialogFragment?.actionDatabaseListener = mActionDatabaseListener + databaseChangedDialogFragment?.show(activity.supportFragmentManager, DATABASE_CHANGED_DIALOG_TAG) + } + } + } + private fun startDialog(titleId: Int? = null, messageId: Int? = null, warningId: Int? = null) { @@ -140,11 +167,14 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply { addActionTaskListener(actionTaskListener) + addDatabaseFileInfoListener(databaseInfoListener) getService().checkAction() + getService().checkDatabaseInfo() } } override fun onServiceDisconnected(name: ComponentName?) { + mBinder?.removeDatabaseFileInfoListener(databaseInfoListener) mBinder?.removeActionTaskListener(actionTaskListener) mBinder = null } @@ -206,6 +236,7 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { fun unregisterProgressTask() { stopDialog() + mBinder?.removeDatabaseFileInfoListener(databaseInfoListener) mBinder?.removeActionTaskListener(actionTaskListener) mBinder = null @@ -264,6 +295,13 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) { , ACTION_DATABASE_LOAD_TASK) } + fun startDatabaseReload(fixDuplicateUuid: Boolean) { + start(Bundle().apply { + putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) + } + , ACTION_DATABASE_RELOAD_TASK) + } + fun startDatabaseAssignPassword(databaseUri: Uri, masterPasswordChecked: Boolean, masterPassword: String?, diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt new file mode 100644 index 000000000..8951d3615 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt @@ -0,0 +1,63 @@ +/* + * 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 . + * + */ +package com.kunzisoft.keepass.database.action + +import android.content.Context +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException +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 ReloadDatabaseRunnable(private val context: Context, + private val mDatabase: Database, + private val progressTaskUpdater: ProgressTaskUpdater?, + private val mLoadDatabaseResult: ((Result) -> Unit)?) + : ActionRunnable() { + + override fun onStartRun() { + // Clear before we load + mDatabase.clear(UriUtil.getBinaryDir(context)) + } + + override fun onActionRun() { + try { + mDatabase.reloadData(context.contentResolver, + UriUtil.getBinaryDir(context), + progressTaskUpdater) + } + catch (e: LoadDatabaseException) { + setError(e) + } + + if (result.isSuccess) { + // Register the current time to init the lock timer + PreferencesUtil.saveCurrentTime(context) + } else { + mDatabase.clearAndClose(UriUtil.getBinaryDir(context)) + } + } + + override fun onFinishRun() { + mLoadDatabaseResult?.invoke(result) + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index 444b86ba6..cb4c3c977 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -31,10 +31,7 @@ import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm -import com.kunzisoft.keepass.database.exception.DatabaseOutputException -import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException -import com.kunzisoft.keepass.database.exception.LoadDatabaseException -import com.kunzisoft.keepass.database.exception.SignatureDatabaseException +import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB @@ -330,29 +327,11 @@ class Database { } @Throws(LoadDatabaseException::class) - fun loadData(uri: Uri, password: String?, keyfile: Uri?, - readOnly: Boolean, - contentResolver: ContentResolver, - cacheDirectory: File, - fixDuplicateUUID: Boolean, - progressTaskUpdater: ProgressTaskUpdater?) { - - this.fileUri = uri - isReadOnly = readOnly - if (uri.scheme == "file") { - val file = File(uri.path!!) - isReadOnly = !file.canWrite() - } - - // Pass KeyFile Uri as InputStreams + private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri, + openDatabaseKDB: (InputStream) -> DatabaseKDB, + openDatabaseKDBX: (InputStream) -> DatabaseKDBX) { var databaseInputStream: InputStream? = null - var keyFileInputStream: InputStream? = null try { - // Get keyFile inputStream - keyfile?.let { - keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile) - } - // Load Data, pass Uris as InputStreams val databaseStream = UriUtil.getUriInputStream(contentResolver, uri) ?: throw IOException("Database input stream cannot be retrieve") @@ -374,22 +353,10 @@ class Database { when { // Header of database KDB - DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(DatabaseInputKDB( - cacheDirectory, - fixDuplicateUUID) - .openDatabase(databaseInputStream, - password, - keyFileInputStream, - progressTaskUpdater)) + DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(openDatabaseKDB(databaseInputStream)) // Header of database KDBX - DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(DatabaseInputKDBX( - cacheDirectory, - fixDuplicateUUID) - .openDatabase(databaseInputStream, - password, - keyFileInputStream, - progressTaskUpdater)) + DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(openDatabaseKDBX(databaseInputStream)) // Header not recognized else -> throw SignatureDatabaseException() @@ -397,14 +364,90 @@ class Database { this.mSearchHelper = SearchHelper() loaded = true + } catch (e: LoadDatabaseException) { + throw e + } finally { + databaseInputStream?.close() + } + } + @Throws(LoadDatabaseException::class) + fun loadData(uri: Uri, password: String?, keyfile: Uri?, + readOnly: Boolean, + contentResolver: ContentResolver, + cacheDirectory: File, + fixDuplicateUUID: Boolean, + progressTaskUpdater: ProgressTaskUpdater?) { + + // Save database URI + this.fileUri = uri + + // Check if the file is writable + this.isReadOnly = readOnly + + // Pass KeyFile Uri as InputStreams + var keyFileInputStream: InputStream? = null + try { + // Get keyFile inputStream + keyfile?.let { + keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile) + } + + // Read database stream for the first time + readDatabaseStream(contentResolver, uri, + { databaseInputStream -> + DatabaseInputKDB(cacheDirectory) + .openDatabase(databaseInputStream, + password, + keyFileInputStream, + progressTaskUpdater, + fixDuplicateUUID) + }, + { databaseInputStream -> + DatabaseInputKDBX(cacheDirectory) + .openDatabase(databaseInputStream, + password, + keyFileInputStream, + progressTaskUpdater, + fixDuplicateUUID) + } + ) + } catch (e: FileNotFoundException) { + Log.e(TAG, "Unable to load keyfile", e) + throw FileNotFoundDatabaseException() } catch (e: LoadDatabaseException) { throw e } catch (e: Exception) { - throw FileNotFoundDatabaseException() + throw LoadDatabaseException(e) } finally { keyFileInputStream?.close() - databaseInputStream?.close() + } + } + + @Throws(LoadDatabaseException::class) + fun reloadData(contentResolver: ContentResolver, + cacheDirectory: File, + progressTaskUpdater: ProgressTaskUpdater?) { + + // Retrieve the stream from the old database URI + fileUri?.let { oldDatabaseUri -> + readDatabaseStream(contentResolver, oldDatabaseUri, + { databaseInputStream -> + DatabaseInputKDB(cacheDirectory) + .openDatabase(databaseInputStream, + masterKey, + progressTaskUpdater) + }, + { databaseInputStream -> + DatabaseInputKDBX(cacheDirectory) + .openDatabase(databaseInputStream, + masterKey, + progressTaskUpdater) + } + ) + } ?: run { + Log.e(TAG, "Database URI is null, database cannot be reloaded") + throw IODatabaseException() } } @@ -531,7 +574,7 @@ class Database { this.fileUri = uri } - fun closeAndClear(filesDirectory: File? = null) { + fun clear(filesDirectory: File? = null) { drawFactory.clearCache() // Delete the cache of the database if present mDatabaseKDB?.clearCache() @@ -544,7 +587,10 @@ class Database { } catch (e: Exception) { Log.e(TAG, "Unable to clear the directory cache.", e) } + } + fun clearAndClose(filesDirectory: File? = null) { + clear(filesDirectory) this.mDatabaseKDB = null this.mDatabaseKDBX = null this.fileUri = null diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt index 3f1461d48..8678e84ce 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDB.kt @@ -163,10 +163,6 @@ class DatabaseKDB : DatabaseVersioned() { finalKey = messageDigest.digest() } - override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { - return null - } - override fun createGroup(): GroupKDB { return GroupKDB() } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt index d960aa316..a5efbbd36 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt @@ -43,10 +43,12 @@ import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4 +import com.kunzisoft.keepass.utils.StringUtil.hexStringToByteArray +import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars +import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.VariantDictionary import org.w3c.dom.Node -import org.w3c.dom.Text import java.io.File import java.io.IOException import java.io.InputStream @@ -180,7 +182,8 @@ class DatabaseKDBX : DatabaseVersioned { when (oldCompression) { CompressionAlgorithm.None -> { when (newCompression) { - CompressionAlgorithm.None -> {} + CompressionAlgorithm.None -> { + } CompressionAlgorithm.GZip -> { // Only in databaseV3.1, in databaseV4 the header is zipped during the save if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) { @@ -198,7 +201,8 @@ class DatabaseKDBX : DatabaseVersioned { CompressionAlgorithm.None -> { decompressAllBinaries() } - CompressionAlgorithm.GZip -> {} + CompressionAlgorithm.GZip -> { + } } } } @@ -384,30 +388,73 @@ class DatabaseKDBX : DatabaseVersioned { val documentBuilder = documentBuilderFactory.newDocumentBuilder() val doc = documentBuilder.parse(keyInputStream) + var xmlKeyFileVersion = 1F + val docElement = doc.documentElement - if (docElement == null || !docElement.nodeName.equals(RootElementName, ignoreCase = true)) { + val keyFileChildNodes = docElement.childNodes + // Root node + if (docElement == null + || !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) { return null } - - val children = docElement.childNodes - if (children.length < 2) { + if (keyFileChildNodes.length < 2) return null - } - - for (i in 0 until children.length) { - val child = children.item(i) - - if (child.nodeName.equals(KeyElementName, ignoreCase = true)) { - val keyChildren = child.childNodes - for (j in 0 until keyChildren.length) { - val keyChild = keyChildren.item(j) - if (keyChild.nodeName.equals(KeyDataElementName, ignoreCase = true)) { - val children2 = keyChild.childNodes - for (k in 0 until children2.length) { - val text = children2.item(k) - if (text.nodeType == Node.TEXT_NODE) { - val txt = text as Text - return Base64.decode(txt.nodeValue, BASE_64_FLAG) + for (keyFileChildPosition in 0 until keyFileChildNodes.length) { + val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition) + // + if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) { + val metaChildNodes = keyFileChildNode.childNodes + for (metaChildPosition in 0 until metaChildNodes.length) { + val metaChildNode = metaChildNodes.item(metaChildPosition) + // + if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) { + val versionChildNodes = metaChildNode.childNodes + for (versionChildPosition in 0 until versionChildNodes.length) { + val versionChildNode = versionChildNodes.item(versionChildPosition) + if (versionChildNode.nodeType == Node.TEXT_NODE) { + val versionText = versionChildNode.textContent.removeSpaceChars() + try { + xmlKeyFileVersion = versionText.toFloat() + Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion") + } catch (e: Exception) { + Log.e(TAG, "XML Keyfile version cannot be read : $versionText") + } + } + } + } + } + } + // + if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) { + val keyChildNodes = keyFileChildNode.childNodes + for (keyChildPosition in 0 until keyChildNodes.length) { + val keyChildNode = keyChildNodes.item(keyChildPosition) + // + if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) { + var hashString : String? = null + if (keyChildNode.hasAttributes()) { + val dataNodeAttributes = keyChildNode.attributes + hashString = dataNodeAttributes + .getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue + } + val dataChildNodes = keyChildNode.childNodes + for (dataChildPosition in 0 until dataChildNodes.length) { + val dataChildNode = dataChildNodes.item(dataChildPosition) + if (dataChildNode.nodeType == Node.TEXT_NODE) { + val dataString = dataChildNode.textContent.removeSpaceChars() + when (xmlKeyFileVersion) { + 1F -> { + // No hash in KeyFile XML version 1 + } + 2F -> { + if (hashString != null + && checkKeyFileHash(dataString, hashString)) + Log.i(TAG, "Successful key file hash check.") + else + Log.e(TAG, "Unable to check the hash of the key file.") + } + } + return Base64.decode(dataString, BASE_64_FLAG) } } } @@ -417,10 +464,26 @@ class DatabaseKDBX : DatabaseVersioned { } catch (e: Exception) { return null } - return null } + private fun checkKeyFileHash(data: String, hash: String): Boolean { + val digest: MessageDigest? + var success = false + try { + digest = MessageDigest.getInstance("SHA-256") + digest?.reset() + // hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key. + val dataDigest = digest.digest(data.hexStringToByteArray()) + .copyOfRange(0, 4) + .toHexString() + success = dataDigest == hash + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + } + return success + } + override fun newGroupId(): NodeIdUUID { var newId: NodeIdUUID do { @@ -634,11 +697,12 @@ class DatabaseKDBX : DatabaseVersioned { private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited - private const val RootElementName = "KeyFile" - //private const val MetaElementName = "Meta"; - //private const val VersionElementName = "Version"; - private const val KeyElementName = "Key" - private const val KeyDataElementName = "Data" + private const val XML_NODE_ROOT_NAME = "KeyFile" + private const val XML_NODE_META_NAME = "Meta"; + private const val XML_NODE_VERSION_NAME = "Version"; + private const val XML_NODE_KEY_NAME = "Key" + private const val XML_NODE_DATA_NAME = "Data" + private const val XML_ATTRIBUTE_DATA_HASH = "Hash" const val BASE_64_FLAG = Base64.NO_WRAP diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt index dd2a0ea35..c2035f53d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseVersioned.kt @@ -27,7 +27,10 @@ import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException -import java.io.* +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import java.io.UnsupportedEncodingException import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.util.* @@ -124,42 +127,30 @@ abstract class DatabaseVersioned< @Throws(IOException::class) protected fun getFileKey(keyInputStream: InputStream): ByteArray { - val keyByteArrayOutputStream = ByteArrayOutputStream() - keyInputStream.copyTo(keyByteArrayOutputStream) - val keyData = keyByteArrayOutputStream.toByteArray() + val keyData = keyInputStream.readBytes() - val keyByteArrayInputStream = ByteArrayInputStream(keyData) - val key = loadXmlKeyFile(keyByteArrayInputStream) - if (key != null) { - return key + // Check 32 bits key file + if (keyData.size == 32) { + return keyData } - when (keyData.size.toLong()) { - 32L -> return keyData - 64L -> try { - return hexStringToByteArray(String(keyData)) - } catch (e: IndexOutOfBoundsException) { - // Key is not base 64, treat it as binary data - } + // Check XML key file + val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData)) + if (xmlKeyByteArray != null) { + return xmlKeyByteArray } - val messageDigest: MessageDigest + // Hash file as binary data try { - messageDigest = MessageDigest.getInstance("SHA-256") + return MessageDigest.getInstance("SHA-256").digest(keyData) } catch (e: NoSuchAlgorithmException) { throw IOException("SHA-256 not supported") } - - try { - messageDigest.update(keyData) - } catch (e: Exception) { - println(e.toString()) - } - - return messageDigest.digest() } - protected abstract fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? + protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { + return null + } open fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean { if (password == null && !containsKeyFile) @@ -391,16 +382,5 @@ abstract class DatabaseVersioned< private const val TAG = "DatabaseVersioned" val UUID_ZERO = UUID(0, 0) - - fun hexStringToByteArray(s: String): ByteArray { - val len = s.length - val data = ByteArray(len / 2) - var i = 0 - while (i < len) { - data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte() - i += 2 - } - return data - } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt index 8da334872..ad819cde5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInput.kt @@ -41,6 +41,13 @@ abstract class DatabaseInput> abstract fun openDatabase(databaseInputStream: InputStream, password: String?, keyInputStream: InputStream?, - progressTaskUpdater: ProgressTaskUpdater?): PwDb + progressTaskUpdater: ProgressTaskUpdater?, + fixDuplicateUUID: Boolean = false): PwDb + + @Throws(LoadDatabaseException::class) + abstract fun openDatabase(databaseInputStream: InputStream, + masterKey: ByteArray, + progressTaskUpdater: ProgressTaskUpdater?, + fixDuplicateUUID: Boolean = false): PwDb } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt index dd5ac5b6b..f2b67e788 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDB.kt @@ -45,8 +45,7 @@ import javax.crypto.spec.SecretKeySpec /** * Load a KDB database file. */ -class DatabaseInputKDB(cacheDirectory: File, - private val fixDuplicateUUID: Boolean = false) +class DatabaseInputKDB(cacheDirectory: File) : DatabaseInput(cacheDirectory) { private lateinit var mDatabaseToOpen: DatabaseKDB @@ -55,7 +54,28 @@ class DatabaseInputKDB(cacheDirectory: File, override fun openDatabase(databaseInputStream: InputStream, password: String?, keyInputStream: InputStream?, - progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB { + progressTaskUpdater: ProgressTaskUpdater?, + fixDuplicateUUID: Boolean): DatabaseKDB { + return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { + mDatabaseToOpen.retrieveMasterKey(password, keyInputStream) + } + } + + @Throws(LoadDatabaseException::class) + override fun openDatabase(databaseInputStream: InputStream, + masterKey: ByteArray, + progressTaskUpdater: ProgressTaskUpdater?, + fixDuplicateUUID: Boolean): DatabaseKDB { + return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { + mDatabaseToOpen.masterKey = masterKey + } + } + + @Throws(LoadDatabaseException::class) + private fun openDatabase(databaseInputStream: InputStream, + progressTaskUpdater: ProgressTaskUpdater?, + fixDuplicateUUID: Boolean, + assignMasterKey: (() -> Unit)? = null): DatabaseKDB { try { // Load entire file, most of it's encrypted. @@ -84,7 +104,7 @@ class DatabaseInputKDB(cacheDirectory: File, mDatabaseToOpen = DatabaseKDB() mDatabaseToOpen.changeDuplicateId = fixDuplicateUUID - mDatabaseToOpen.retrieveMasterKey(password, keyInputStream) + assignMasterKey?.invoke() // Select algorithm when { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt index 85b237aec..14276b2a9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/input/DatabaseInputKDBX.kt @@ -63,8 +63,7 @@ import javax.crypto.Cipher import javax.crypto.CipherInputStream import kotlin.math.min -class DatabaseInputKDBX(cacheDirectory: File, - private val fixDuplicateUUID: Boolean = false) +class DatabaseInputKDBX(cacheDirectory: File) : DatabaseInput(cacheDirectory) { private var randomStream: StreamCipher? = null @@ -98,12 +97,30 @@ class DatabaseInputKDBX(cacheDirectory: File, override fun openDatabase(databaseInputStream: InputStream, password: String?, keyInputStream: InputStream?, - progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX { + progressTaskUpdater: ProgressTaskUpdater?, + fixDuplicateUUID: Boolean): DatabaseKDBX { + return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { + mDatabase.retrieveMasterKey(password, keyInputStream) + } + } + @Throws(LoadDatabaseException::class) + override fun openDatabase(databaseInputStream: InputStream, + masterKey: ByteArray, + progressTaskUpdater: ProgressTaskUpdater?, + fixDuplicateUUID: Boolean): DatabaseKDBX { + return openDatabase(databaseInputStream, progressTaskUpdater, fixDuplicateUUID) { + mDatabase.masterKey = masterKey + } + } + + @Throws(LoadDatabaseException::class) + private fun openDatabase(databaseInputStream: InputStream, + progressTaskUpdater: ProgressTaskUpdater?, + fixDuplicateUUID: Boolean, + assignMasterKey: (() -> Unit)? = null): DatabaseKDBX { try { - // TODO performance progressTaskUpdater?.updateMessage(R.string.retrieving_db_key) - mDatabase = DatabaseKDBX() mDatabase.changeDuplicateId = fixDuplicateUUID @@ -116,9 +133,8 @@ class DatabaseInputKDBX(cacheDirectory: File, hashOfHeader = headerAndHash.hash val pbHeader = headerAndHash.header - mDatabase.retrieveMasterKey(password, keyInputStream) + assignMasterKey?.invoke() mDatabase.makeFinalKey(header.masterSeed) - // TODO performance progressTaskUpdater?.updateMessage(R.string.decrypting_db) val engine: CipherEngine @@ -436,8 +452,6 @@ class DatabaseInputKDBX(cacheDirectory: File, val strData = readString(xpp) if (strData.isNotEmpty()) { customIconData = Base64.decode(strData, BASE_64_FLAG) - } else { - assert(false) } } else { readUnknown(xpp) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutput.kt deleted file mode 100644 index 4cb13458d..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutput.kt +++ /dev/null @@ -1,25 +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 . - * - */ -package com.kunzisoft.keepass.database.file.output - -open class DatabaseHeaderOutput { - var hashOfHeader: ByteArray? = null - protected set -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt index bd18d8207..de76b662d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseHeaderOutputKDBX.kt @@ -40,13 +40,16 @@ import javax.crypto.spec.SecretKeySpec class DatabaseHeaderOutputKDBX @Throws(DatabaseOutputException::class) constructor(private val databaseKDBX: DatabaseKDBX, private val header: DatabaseHeaderKDBX, - outputStream: OutputStream) : DatabaseHeaderOutput() { + outputStream: OutputStream) { private val los: LittleEndianDataOutputStream private val mos: MacOutputStream private val dos: DigestOutputStream lateinit var headerHmac: ByteArray + var hashOfHeader: ByteArray? = null + private set + init { val md: MessageDigest diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseInnerHeaderOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseInnerHeaderOutputKDBX.kt deleted file mode 100644 index fb3666e90..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseInnerHeaderOutputKDBX.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePassDX. - * - * KeePassDroid 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. - * - * KeePassDroid 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 KeePassDroid. If not, see . - * - */ -package com.kunzisoft.keepass.database.file.output - -import com.kunzisoft.keepass.database.element.database.DatabaseKDBX -import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BUFFER_SIZE_BYTES -import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX -import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream -import com.kunzisoft.keepass.stream.readBytes -import com.kunzisoft.keepass.utils.UnsignedInt -import java.io.IOException -import java.io.OutputStream -import kotlin.experimental.or - -class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX, - private val header: DatabaseHeaderKDBX, - outputStream: OutputStream) { - - private val dataOutputStream: LittleEndianDataOutputStream = LittleEndianDataOutputStream(outputStream) - - @Throws(IOException::class) - fun output() { - dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID) - dataOutputStream.writeInt(4) - if (header.innerRandomStream == null) - throw IOException("Can't write innerRandomStream") - dataOutputStream.writeUInt(header.innerRandomStream!!.id) - - val streamKeySize = header.innerRandomStreamKey.size - dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey) - dataOutputStream.writeInt(streamKeySize) - dataOutputStream.write(header.innerRandomStreamKey) - - database.binaryPool.doForEachOrderedBinary { _, keyBinary -> - val protectedBinary = keyBinary.binary - // Force decompression to add binary in header - protectedBinary.decompress() - // Write type binary - dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) - // Write size - dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1)) - // Write protected flag - var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None - if (protectedBinary.isProtected) { - flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected - } - dataOutputStream.writeByte(flag) - - protectedBinary.getInputDataStream().use { inputStream -> - inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> - dataOutputStream.write(buffer) - } - } - } - - dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader) - dataOutputStream.writeInt(0) - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt index 4c0951393..4d456e538 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutput.kt @@ -26,7 +26,7 @@ import java.io.OutputStream import java.security.NoSuchAlgorithmException import java.security.SecureRandom -abstract class DatabaseOutput
protected constructor(protected var mOS: OutputStream) { +abstract class DatabaseOutput
protected constructor(protected var mOutputStream: OutputStream) { @Throws(DatabaseOutputException::class) protected open fun setIVs(header: Header): SecureRandom { diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt index f71345bf8..417865c21 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDB.kt @@ -63,7 +63,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, // and remove any orphaned nodes that are no longer part of the tree hierarchy sortGroupsForOutput() - val header = outputHeader(mOS) + val header = outputHeader(mOutputStream) val finalKey = getFinalKey(header) @@ -85,7 +85,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(finalKey, "AES"), IvParameterSpec(header.encryptionIV)) - val cos = CipherOutputStream(mOS, cipher) + val cos = CipherOutputStream(mOutputStream, cipher) val bos = BufferedOutputStream(cos) outputPlanGroupAndEntries(bos) bos.flush() diff --git a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt index 965d55a19..9fb0f0d37 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/file/output/DatabaseOutputKDBX.kt @@ -46,6 +46,7 @@ import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.DatabaseKDBXXML import com.kunzisoft.keepass.database.file.DateKDBXUtil import com.kunzisoft.keepass.stream.* +import com.kunzisoft.keepass.utils.UnsignedInt import org.bouncycastle.crypto.StreamCipher import org.joda.time.DateTime import org.xmlpull.v1.XmlSerializer @@ -57,6 +58,7 @@ import java.util.* import java.util.zip.GZIPOutputStream import javax.crypto.Cipher import javax.crypto.CipherOutputStream +import kotlin.experimental.or class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, @@ -80,20 +82,19 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, throw DatabaseOutputException("No such cipher", e) } - header = outputHeader(mOS) + header = outputHeader(mOutputStream) val osPlain: OutputStream osPlain = if (header!!.version.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) { - val cos = attachStreamEncryptor(header!!, mOS) + val cos = attachStreamEncryptor(header!!, mOutputStream) cos.write(header!!.streamStartBytes) HashedBlockOutputStream(cos) } else { - mOS.write(hashOfHeader!!) - mOS.write(headerHmac!!) + mOutputStream.write(hashOfHeader!!) + mOutputStream.write(headerHmac!!) - - attachStreamEncryptor(header!!, HmacBlockOutputStream(mOS, mDatabaseKDBX.hmacKey!!)) + attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!)) } val osXml: OutputStream @@ -104,8 +105,7 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, } if (header!!.version.toKotlinLong() >= DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()) { - val ihOut = DatabaseInnerHeaderOutputKDBX(mDatabaseKDBX, header!!, osXml) - ihOut.output() + outputInnerHeader(mDatabaseKDBX, header!!, osXml) } outputDatabase(osXml) @@ -121,6 +121,49 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, } } + @Throws(IOException::class) + private fun outputInnerHeader(database: DatabaseKDBX, + header: DatabaseHeaderKDBX, + outputStream: OutputStream) { + val dataOutputStream = LittleEndianDataOutputStream(outputStream) + + dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID) + dataOutputStream.writeInt(4) + if (header.innerRandomStream == null) + throw IOException("Can't write innerRandomStream") + dataOutputStream.writeUInt(header.innerRandomStream!!.id) + + val streamKeySize = header.innerRandomStreamKey.size + dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey) + dataOutputStream.writeInt(streamKeySize) + dataOutputStream.write(header.innerRandomStreamKey) + + database.binaryPool.doForEachOrderedBinary { _, keyBinary -> + val protectedBinary = keyBinary.binary + // Force decompression to add binary in header + protectedBinary.decompress() + // Write type binary + dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) + // Write size + dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1)) + // Write protected flag + var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None + if (protectedBinary.isProtected) { + flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected + } + dataOutputStream.writeByte(flag) + + protectedBinary.getInputDataStream().use { inputStream -> + inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> + dataOutputStream.write(buffer) + } + } + } + + dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader) + dataOutputStream.writeInt(0) + } + @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) private fun outputDatabase(outputStream: OutputStream) { diff --git a/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt index 691273b25..add39ee52 100644 --- a/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt +++ b/app/src/main/java/com/kunzisoft/keepass/icons/IconDrawableFactory.kt @@ -26,9 +26,12 @@ import android.graphics.* import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.os.Build import android.util.Log import android.widget.ImageView import android.widget.RemoteViews +import androidx.annotation.RequiresApi import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.toBitmap import androidx.core.widget.ImageViewCompat @@ -87,6 +90,22 @@ class IconDrawableFactory { remoteViews.setImageViewBitmap(imageId, bitmap) } + /** + * Utility method to assign a drawable to a icon and tint it + */ + @RequiresApi(Build.VERSION_CODES.M) + fun assignDrawableToIcon(superDrawable: SuperDrawable, + tintColor: Int = Color.BLACK): Icon { + val bitmap = superDrawable.drawable.toBitmap() + // Tint bitmap if it's not a custom icon + if (superDrawable.tintable && bitmap.isMutable) { + Canvas(bitmap).drawBitmap(bitmap, 0.0F, 0.0F, Paint().apply { + colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN) + }) + } + return Icon.createWithBitmap(bitmap) + } + /** * Get the [SuperDrawable] [icon] (from cache, or build it and add it to the cache if not exists yet), then [tint] it with [tintColor] if needed */ @@ -309,3 +328,22 @@ fun RemoteViews.assignDatabaseIcon(context: Context, Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e) } } + +@RequiresApi(Build.VERSION_CODES.M) +fun createIconFromDatabaseIcon(context: Context, + iconFactory: IconDrawableFactory, + icon: IconImage, + tintColor: Int = Color.BLACK): Icon? { + try { + return iconFactory.assignDrawableToIcon( + iconFactory.getIconSuperDrawable(context, + icon, + 24, + true, + tintColor), + tintColor) + } catch (e: Exception) { + Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e) + } + return null +} diff --git a/app/src/main/java/com/kunzisoft/keepass/model/SnapFileDatabaseInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/SnapFileDatabaseInfo.kt new file mode 100644 index 000000000..4ef9acb3e --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/model/SnapFileDatabaseInfo.kt @@ -0,0 +1,62 @@ +package com.kunzisoft.keepass.model + +import android.content.Context +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import android.text.format.Formatter +import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo +import java.text.DateFormat +import java.util.* + +/** + * Utility data class to get FileDatabaseInfo at a `t` time + */ +data class SnapFileDatabaseInfo(var fileUri: Uri?, + var exists: Boolean, + var lastModification: Long?, + var size: Long?): Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readParcelable(Uri::class.java.classLoader), + parcel.readByte() != 0.toByte(), + parcel.readValue(Long::class.java.classLoader) as? Long, + parcel.readValue(Long::class.java.classLoader) as? Long) { + } + + fun toString(context: Context): String { + val lastModificationString = DateFormat.getDateTimeInstance() + .format(Date(lastModification ?: 0)) + return "$lastModificationString, " + + Formatter.formatFileSize(context, size ?: 0) + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(fileUri, flags) + parcel.writeByte(if (exists) 1 else 0) + parcel.writeValue(lastModification) + parcel.writeValue(size) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SnapFileDatabaseInfo { + return SnapFileDatabaseInfo(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + + fun fromFileDatabaseInfo(fileDatabaseInfo: FileDatabaseInfo): SnapFileDatabaseInfo { + return SnapFileDatabaseInfo( + fileDatabaseInfo.fileUri, + fileDatabaseInfo.exists, + fileDatabaseInfo.getLastModification(), + fileDatabaseInfo.getSize()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt index bbc0e9bf1..3535fba62 100644 --- a/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/notifications/DatabaseTaskNotificationService.kt @@ -22,9 +22,8 @@ package com.kunzisoft.keepass.notifications import android.app.PendingIntent import android.content.Intent import android.net.Uri -import android.os.Binder -import android.os.Bundle -import android.os.IBinder +import android.os.* +import android.util.Log import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper @@ -40,6 +39,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm 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.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.timeout.TimeoutHelper @@ -47,6 +47,7 @@ import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.utils.closeDatabase +import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo import kotlinx.coroutines.* import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -65,6 +66,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress private var mAllowFinishAction = AtomicBoolean() private var mActionRunning = false + private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null + private var mDatabaseInfoListeners = LinkedList() + private var mIconId: Int = R.drawable.notification_ic_database_load private var mTitleId: Int = R.string.database_opened private var mMessageId: Int? = null @@ -93,6 +97,14 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress mAllowFinishAction.set(false) } } + + fun addDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) { + mDatabaseInfoListeners.add(databaseInfoListener) + } + + fun removeDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) { + mDatabaseInfoListeners.remove(databaseInfoListener) + } } interface ActionTaskListener { @@ -101,6 +113,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress fun onStopAction(actionTask: String, result: ActionRunnable.Result) } + interface DatabaseInfoListener { + fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo, + newDatabaseInfo: SnapFileDatabaseInfo) + } + /** * Force to call [ActionTaskListener.onStartAction] if the action is still running */ @@ -112,6 +129,31 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } } + fun checkDatabaseInfo() { + mDatabase.fileUri?.let { + val previousDatabaseInfo = mSnapFileDatabaseInfo + val lastFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo( + FileDatabaseInfo(applicationContext, it)) + if (previousDatabaseInfo != null) { + if (previousDatabaseInfo != lastFileDatabaseInfo) { + Log.i(TAG, "Database file modified " + + "$previousDatabaseInfo != $lastFileDatabaseInfo ") + // Call listener to indicate a change in database info + mDatabaseInfoListeners.forEach { listener -> + listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo) + } + } + } + } + } + + fun saveDatabaseInfo() { + mDatabase.fileUri?.let { + mSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo( + FileDatabaseInfo(applicationContext, it)) + } + } + override fun onBind(intent: Intent): IBinder? { super.onBind(intent) return mActionTaskBinder @@ -138,6 +180,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress val actionRunnable: ActionRunnable? = when (intentAction) { ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent) ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent) + ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask() ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent) ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent) ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent) @@ -193,6 +236,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } } finally { removeIntentData(intent) + // Save the database info after performing action + saveDatabaseInfo() TimeoutHelper.releaseTemporarilyDisableTimeout() if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) { if (!mDatabase.loaded) { @@ -214,7 +259,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } return when (intentAction) { - ACTION_DATABASE_LOAD_TASK, null -> { + ACTION_DATABASE_LOAD_TASK, + ACTION_DATABASE_RELOAD_TASK, + null -> { START_STICKY } else -> { @@ -248,7 +295,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress else -> { when (intentAction) { ACTION_DATABASE_CREATE_TASK -> R.string.creating_database - ACTION_DATABASE_LOAD_TASK -> R.string.loading_database + ACTION_DATABASE_LOAD_TASK, + ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database ACTION_DATABASE_SAVE -> R.string.saving_database else -> { R.string.command_execution @@ -258,13 +306,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } mMessageId = when (intentAction) { - ACTION_DATABASE_LOAD_TASK -> null + ACTION_DATABASE_LOAD_TASK, + ACTION_DATABASE_RELOAD_TASK -> null else -> null } mWarningId = if (!saveAction - || intentAction == ACTION_DATABASE_LOAD_TASK) + || intentAction == ACTION_DATABASE_LOAD_TASK + || intentAction == ACTION_DATABASE_RELOAD_TASK) null else R.string.do_not_kill_app @@ -342,9 +392,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress * Execute action with a coroutine */ private suspend fun executeAction(progressTaskUpdater: ProgressTaskUpdater, - onPreExecute: () -> Unit, - onExecute: (ProgressTaskUpdater?) -> ActionRunnable?, - onPostExecute: (result: ActionRunnable.Result) -> Unit) { + onPreExecute: () -> Unit, + onExecute: (ProgressTaskUpdater?) -> ActionRunnable?, + onPostExecute: (result: ActionRunnable.Result) -> Unit) { mAllowFinishAction.set(false) TimeoutHelper.temporarilyDisableTimeout() @@ -465,6 +515,17 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress } } + private fun buildDatabaseReloadActionTask(): ActionRunnable { + return ReloadDatabaseRunnable( + this, + mDatabase, + this + ) { result -> + // No need to add each info to reload database + result.data = Bundle() + } + } + private fun buildDatabaseAssignPasswordActionTask(intent: Intent): ActionRunnable? { return if (intent.hasExtra(DATABASE_URI_KEY) && intent.hasExtra(MASTER_PASSWORD_CHECKED_KEY) @@ -770,6 +831,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK" const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK" + const val ACTION_DATABASE_RELOAD_TASK = "ACTION_DATABASE_RELOAD_TASK" const val ACTION_DATABASE_ASSIGN_PASSWORD_TASK = "ACTION_DATABASE_ASSIGN_PASSWORD_TASK" const val ACTION_DATABASE_CREATE_GROUP_TASK = "ACTION_DATABASE_CREATE_GROUP_TASK" const val ACTION_DATABASE_UPDATE_GROUP_TASK = "ACTION_DATABASE_UPDATE_GROUP_TASK" diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt index 682f7889e..bd61b3325 100644 --- a/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt +++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt @@ -20,6 +20,7 @@ package com.kunzisoft.keepass.otp import com.kunzisoft.keepass.model.OtpModel +import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars import org.apache.commons.codec.binary.Base32 import org.apache.commons.codec.binary.Base64 import org.apache.commons.codec.binary.Hex @@ -216,17 +217,9 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) { return secret.isNotEmpty() && checkBase64Secret(secret) } - fun removeLineChars(parameter: String): String { - return parameter.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "") - } - - fun removeSpaceChars(parameter: String): String { - return parameter.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "") - } - fun replaceBase32Chars(parameter: String): String { // Add 'A' at end if not Base32 length - var parameterNewSize = removeSpaceChars(parameter.toUpperCase(Locale.ENGLISH)) + var parameterNewSize = parameter.toUpperCase(Locale.ENGLISH).removeSpaceChars() while (parameterNewSize.length % 8 != 0) { parameterNewSize += 'A' } diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt b/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt index 38106feca..33977d6a8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt +++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.kt @@ -24,9 +24,9 @@ import android.net.Uri import android.util.Log import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.model.Field -import com.kunzisoft.keepass.otp.OtpElement.Companion.removeLineChars -import com.kunzisoft.keepass.otp.OtpElement.Companion.removeSpaceChars import com.kunzisoft.keepass.otp.TokenCalculator.* +import com.kunzisoft.keepass.utils.StringUtil.removeLineChars +import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars import java.util.* import java.util.regex.Pattern @@ -126,7 +126,7 @@ object OtpEntryFields { private fun parseOTPUri(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { val otpPlainText = getField(OTP_FIELD) if (otpPlainText != null && otpPlainText.isNotEmpty() && isOTPUri(otpPlainText)) { - val uri = Uri.parse(removeSpaceChars(otpPlainText)) + val uri = Uri.parse(otpPlainText.removeSpaceChars()) if (uri.scheme == null || OTP_SCHEME != uri.scheme!!.toLowerCase(Locale.ENGLISH)) { Log.e(TAG, "Invalid or missing scheme in uri") @@ -159,16 +159,16 @@ object OtpEntryFields { if (nameParam != null && nameParam.isNotEmpty()) { val userIdArray = nameParam.split(":", "%3A") if (userIdArray.size > 1) { - otpElement.issuer = removeLineChars(userIdArray[0]) - otpElement.name = removeLineChars(userIdArray[1]) + otpElement.issuer = userIdArray[0].removeLineChars() + otpElement.name = userIdArray[1].removeLineChars() } else { - otpElement.name = removeLineChars(nameParam) + otpElement.name = nameParam.removeLineChars() } } val issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM) if (issuerParam != null && issuerParam.isNotEmpty()) - otpElement.issuer = removeLineChars(issuerParam) + otpElement.issuer = issuerParam.removeLineChars() val secretParam = uri.getQueryParameter(SECRET_URL_PARAM) if (secretParam != null && secretParam.isNotEmpty()) { @@ -262,7 +262,7 @@ object OtpEntryFields { } private fun encodeParameter(parameter: String): String { - return Uri.encode(OtpElement.removeLineChars(parameter)) + return Uri.encode(parameter.removeLineChars()) } private fun parseTOTPKeyValues(getField: (id: String) -> String?, otpElement: OtpElement): Boolean { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt index cba9c231d..f3462752b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/AutofillSettingsFragment.kt @@ -19,10 +19,12 @@ */ package com.kunzisoft.keepass.settings +import android.os.Build import android.os.Bundle import androidx.fragment.app.DialogFragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreference import com.kunzisoft.keepass.R import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat @@ -32,6 +34,11 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { // Load the preferences from an XML resource setPreferencesFromResource(R.xml.preferences_autofill, rootKey) + + val autofillInlineSuggestionsPreference: SwitchPreference? = findPreference(getString(R.string.autofill_inline_suggestions_key)) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + autofillInlineSuggestionsPreference?.isVisible = false + } } override fun onDisplayPreferenceDialog(preference: Preference?) { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt index d03cd3411..b6b0acb7f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/MainPreferenceFragment.kt @@ -103,6 +103,6 @@ class MainPreferenceFragment : PreferenceFragmentCompat() { } interface Callback { - fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) + fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean = false) } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt index de0bbadda..7fedcd403 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt @@ -552,6 +552,10 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() { settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseSave(!mDatabaseReadOnly) true } + R.id.menu_reload_database -> { + settingActivity?.mProgressDatabaseTaskProvider?.startDatabaseReload(false) + return true + } else -> { // Check the time lock before launching settings diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt index f9c2f415d..f2c166d9e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt @@ -436,13 +436,18 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.autofill_close_database_default)) } - fun isAutofillAutoSearchEnable(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.autofill_auto_search_key), context.resources.getBoolean(R.bool.autofill_auto_search_default)) } + fun isAutofillInlineSuggestionsEnable(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.autofill_inline_suggestions_key), + context.resources.getBoolean(R.bool.autofill_inline_suggestions_default)) + } + fun isAutofillSaveSearchInfoEnable(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.autofill_save_search_info_key), diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt index 4418e86f7..5d4f1b007 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft. + * Copyright 2020 Jeremy Jamet / Kunzisoft. * * This file is part of KeePassDX. * @@ -21,7 +21,6 @@ package com.kunzisoft.keepass.settings import android.app.Activity import android.app.backup.BackupManager -import android.content.DialogInterface import android.content.Intent import android.net.Uri import android.os.Bundle @@ -37,6 +36,7 @@ import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.view.showActionError @@ -95,12 +95,28 @@ open class SettingsActivity backupManager = BackupManager(this) mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result -> - // Call result in fragment - (supportFragmentManager - .findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?) - ?.onProgressDialogThreadResult(actionTask, result) + when (actionTask) { + DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> { + // Reload the current activity + startActivity(intent) + finish() + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + } + else -> { + // Call result in fragment + (supportFragmentManager + .findFragmentByTag(TAG_NESTED) as NestedSettingsFragment?) + ?.onProgressDialogThreadResult(actionTask, result) + coordinatorLayout?.showActionError(result) + } + } + } - coordinatorLayout?.showActionError(result) + // To reload the current screen + if (intent.extras?.containsKey(FRAGMENT_ARG) == true) { + intent.extras?.getString(FRAGMENT_ARG)?.let { fragmentScreenName -> + onNestedPreferenceSelected(NestedSettingsFragment.Screen.valueOf(fragmentScreenName), true) + } } } @@ -193,25 +209,33 @@ open class SettingsActivity hideOrShowLockButton(NestedSettingsFragment.Screen.APPLICATION) } - private fun replaceFragment(key: NestedSettingsFragment.Screen) { - supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, + private fun replaceFragment(key: NestedSettingsFragment.Screen, reload: Boolean) { + supportFragmentManager.beginTransaction().apply { + if (reload) { + setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out, R.anim.slide_in_left, R.anim.slide_out_right) - .replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key, mReadOnly), TAG_NESTED) - .addToBackStack(TAG_NESTED) - .commit() + } else { + setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, + R.anim.slide_in_left, R.anim.slide_out_right) + } + replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key, mReadOnly), TAG_NESTED) + addToBackStack(TAG_NESTED) + commit() + } toolbar?.title = NestedSettingsFragment.retrieveTitle(resources, key) + // To reload the current screen + intent.putExtra(FRAGMENT_ARG, key.name) hideOrShowLockButton(key) } - override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen) { + override fun onNestedPreferenceSelected(key: NestedSettingsFragment.Screen, reload: Boolean) { if (mTimeoutEnable) TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) { - replaceFragment(key) + replaceFragment(key, reload) } else - replaceFragment(key) + replaceFragment(key, reload) } override fun onSaveInstanceState(outState: Bundle) { @@ -226,6 +250,7 @@ open class SettingsActivity private const val SHOW_LOCK = "SHOW_LOCK" private const val TITLE_KEY = "TITLE_KEY" private const val TAG_NESTED = "TAG_NESTED" + private const val FRAGMENT_ARG = "FRAGMENT_ARG" fun launch(activity: Activity, readOnly: Boolean, timeoutEnable: Boolean) { val intent = Intent(activity, SettingsActivity::class.java) diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt index 59c97ac7c..3dcfcf7b4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt @@ -138,5 +138,5 @@ fun Context.closeDatabase() { cancelAll() } // Clear data - Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this)) + Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this)) } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt new file mode 100644 index 000000000..e9ed8ebaa --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/utils/StringUtil.kt @@ -0,0 +1,26 @@ +package com.kunzisoft.keepass.utils + +object StringUtil { + + fun String.removeLineChars(): String { + return this.replace("[\\r|\\n|\\t|\\u00A0]+".toRegex(), "") + } + + fun String.removeSpaceChars(): String { + return this.replace("[\\r|\\n|\\t|\\s|\\u00A0]+".toRegex(), "") + } + + fun String.hexStringToByteArray(): ByteArray { + val len = this.length + val data = ByteArray(len / 2) + var i = 0 + while (i < len) { + data[i / 2] = ((Character.digit(this[i], 16) shl 4) + + Character.digit(this[i + 1], 16)).toByte() + i += 2 + } + return data + } + + fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/FileDatabaseInfo.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/FileDatabaseInfo.kt index 65b8c9e5b..6d9df989c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/FileDatabaseInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/FileDatabaseInfo.kt @@ -23,7 +23,6 @@ import android.content.Context import android.net.Uri import android.text.format.Formatter import androidx.documentfile.provider.DocumentFile -import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.UriUtil import java.io.Serializable import java.text.DateFormat @@ -58,7 +57,11 @@ class FileDatabaseInfo : Serializable { } private set - fun getModificationString(): String? { + fun getLastModification(): Long? { + return documentFile?.lastModified() + } + + fun getLastModificationString(): String? { return documentFile?.lastModified()?.let { if (it != 0L) { DateFormat.getDateTimeInstance() @@ -69,6 +72,10 @@ class FileDatabaseInfo : Serializable { } } + fun getSize(): Long? { + return documentFile?.length() + } + fun getSizeString(): String? { return documentFile?.let { Formatter.formatFileSize(context, it.length()) diff --git a/app/src/main/res/drawable/ic_reload_white_24dp.xml b/app/src/main/res/drawable/ic_reload_white_24dp.xml new file mode 100644 index 000000000..020472b89 --- /dev/null +++ b/app/src/main/res/drawable/ic_reload_white_24dp.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/menu/contribution.xml b/app/src/main/res/menu/contribution.xml index b76875ff9..5c1671456 100644 --- a/app/src/main/res/menu/contribution.xml +++ b/app/src/main/res/menu/contribution.xml @@ -22,6 +22,6 @@ \ No newline at end of file diff --git a/app/src/main/res/menu/database.xml b/app/src/main/res/menu/database.xml index d94f86817..a7a182b8a 100644 --- a/app/src/main/res/menu/database.xml +++ b/app/src/main/res/menu/database.xml @@ -24,4 +24,9 @@ android:title="@string/menu_save_database" android:orderInCategory="95" app:showAsAction="ifRoom" /> + \ No newline at end of file diff --git a/app/src/main/res/values-v30/donottranslate.xml b/app/src/main/res/values-v30/donottranslate.xml new file mode 100644 index 000000000..fc50571b3 --- /dev/null +++ b/app/src/main/res/values-v30/donottranslate.xml @@ -0,0 +1,22 @@ + + + + true + diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 757f9f374..fd145926f 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -151,6 +151,8 @@ false autofill_auto_search_key true + autofill_inline_suggestions_key + false autofill_save_search_info_key true autofill_ask_to_save_data_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index db22f7442..9dd03860d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -185,6 +185,7 @@ Hide password Lock database Save database + Reload database Open Search Show password @@ -272,6 +273,9 @@ Remove this data anyway? It is not recommended to add an empty keyfile. The content of the keyfile should never be changed, and in the best case, should contain randomly generated data. + The information contained in your database file has been modified outside the app. + Overwrite the external modifications by saving the database or reload it with the latest changes. + Access to the file revoked by the file manager, close the database and reopen it from its location. Version %1$s Build %1$s No biometric or device credential is enrolled. @@ -428,6 +432,8 @@ Close the database after an autofill selection Auto search Automatically suggest search results from the web domain or application ID + Inline suggestions + Attempt to display autofill suggestions directly from a compatible keyboard Save search info Try to save search information when making a manual entry selection Ask to save data @@ -439,6 +445,7 @@ Block autofill Restart the app containing the form to activate the blocking. Data save is not allowed for a database opened as read-only. + Autofill suggestions added. Allow no master key Allows tapping the \"Open\" button if no credentials are selected Delete password diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 76406e20a..ca871ad06 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -465,6 +465,7 @@ @android:color/transparent @null true + true true false diff --git a/app/src/main/res/xml/dataset_service.xml b/app/src/main/res/xml/dataset_service.xml index ea2597ec7..6d595b7b5 100644 --- a/app/src/main/res/xml/dataset_service.xml +++ b/app/src/main/res/xml/dataset_service.xml @@ -23,7 +23,9 @@ Settings Activity. This is pointed to in the service's meta-data in the applicat + android:settingsActivity="com.kunzisoft.keepass.settings.AutofillSettingsActivity" + android:supportsInlineSuggestions="true" + tools:ignore="UnusedAttribute"> diff --git a/app/src/main/res/xml/preferences_autofill.xml b/app/src/main/res/xml/preferences_autofill.xml index 53f00db66..5ab891b4c 100644 --- a/app/src/main/res/xml/preferences_autofill.xml +++ b/app/src/main/res/xml/preferences_autofill.xml @@ -30,6 +30,11 @@ android:title="@string/autofill_auto_search_title" android:summary="@string/autofill_auto_search_summary" android:defaultValue="@bool/autofill_auto_search_default"/> + diff --git a/fastlane/metadata/android/en-US/changelogs/53.txt b/fastlane/metadata/android/en-US/changelogs/53.txt index 42780ecb1..4c35cf140 100644 --- a/fastlane/metadata/android/en-US/changelogs/53.txt +++ b/fastlane/metadata/android/en-US/changelogs/53.txt @@ -1 +1,4 @@ - * \ No newline at end of file + * Detect file changes and reload database #794 + * Inline suggestions autofill with compatible keyboard (Android R) #827 + * Add Keyfile XML version 2 #844 + * Fix binaries of 64 bytes #835 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/53.txt b/fastlane/metadata/android/fr-FR/changelogs/53.txt index 42780ecb1..f14609bfc 100644 --- a/fastlane/metadata/android/fr-FR/changelogs/53.txt +++ b/fastlane/metadata/android/fr-FR/changelogs/53.txt @@ -1 +1,4 @@ - * \ No newline at end of file + * Détection des changements de fichiers et rechargement de base de données #794 + * Remplissage automatique avec suggestions en ligne (Android R) (Android R) #827 + * Ajout du fichier de clé XML version 2 #844 + * Correction des binaires de 64 Octets #835 \ No newline at end of file