diff --git a/CHANGELOG b/CHANGELOG index 9c4373565..31b354f05 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +KeePassDX(3.2.0) + * Manage data merge #840 #977 + KeePassDX(3.1.0) * Add breadcrumb * Add path in search results #1148 diff --git a/app/build.gradle b/app/build.gradle index 5fa9ea69a..2dce2f675 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 15 targetSdkVersion 31 - versionCode = 92 - versionName = "3.1.0" + versionCode = 93 + versionName = "3.2.0" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" 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 e8cf1f74b..2bbb71dba 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -376,6 +376,9 @@ class EntryActivity : DatabaseLockActivity() { menu?.findItem(R.id.menu_save_database)?.isVisible = false menu?.findItem(R.id.menu_edit)?.isVisible = false } + if (!mMergeDataAllowed || mDatabaseReadOnly) { + menu?.findItem(R.id.menu_merge_database)?.isVisible = false + } if (mSpecialMode != SpecialMode.DEFAULT) { menu?.findItem(R.id.menu_reload_database)?.isVisible = false } @@ -455,6 +458,9 @@ class EntryActivity : DatabaseLockActivity() { R.id.menu_save_database -> { saveDatabase() } + R.id.menu_merge_database -> { + mergeDatabase() + } R.id.menu_reload_database -> { reloadDatabase() } 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 5b8455680..43f8e9f9c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -970,6 +970,9 @@ class GroupActivity : DatabaseLockActivity(), if (mDatabaseReadOnly) { menu.findItem(R.id.menu_save_database)?.isVisible = false } + if (!mMergeDataAllowed || mDatabaseReadOnly) { + menu.findItem(R.id.menu_merge_database)?.isVisible = false + } if (mSpecialMode == SpecialMode.DEFAULT) { MenuUtil.defaultMenuInflater(inflater, menu) } else { @@ -1093,6 +1096,10 @@ class GroupActivity : DatabaseLockActivity(), saveDatabase() return true } + R.id.menu_merge_database -> { + mergeDatabase() + return true + } R.id.menu_reload_database -> { reloadDatabase() return true diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt index f5de6dd2e..6b5e79d64 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt @@ -62,6 +62,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), private var mExitLock: Boolean = false protected var mDatabaseReadOnly: Boolean = true + protected var mMergeDataAllowed: Boolean = false private var mAutoSaveEnable: Boolean = true protected var mIconDrawableFactory: IconDrawableFactory? = null @@ -87,8 +88,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), mDatabaseTaskProvider?.startDatabaseSave(save) } + mDatabaseViewModel.mergeDatabase.observe(this) { fixDuplicateUuid -> + mDatabaseTaskProvider?.startDatabaseMerge(fixDuplicateUuid) + } + mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid -> - mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid) + mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) { + mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid) + } } mDatabaseViewModel.saveName.observe(this) { @@ -197,6 +204,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), } mDatabaseReadOnly = database.isReadOnly + mMergeDataAllowed = database.isMergeDataAllowed() mIconDrawableFactory = database.iconDrawableFactory checkRegister() @@ -212,6 +220,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), ) { super.onDatabaseActionFinished(database, actionTask, result) when (actionTask) { + DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK, DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> { // Reload the current activity if (result.isSuccess) { @@ -254,8 +263,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), mDatabaseTaskProvider?.startDatabaseSave(true) } + fun mergeDatabase() { + mDatabaseTaskProvider?.startDatabaseMerge(false) + } + fun reloadDatabase() { - mDatabaseTaskProvider?.startDatabaseReload(false) + mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) { + mDatabaseTaskProvider?.startDatabaseReload(false) + } } fun createEntry(newEntry: Entry, diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt index f5d121d73..9ae3beef7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/SearchEntryCursorAdapter.kt @@ -35,6 +35,7 @@ import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDBX +import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.view.strikeOut @@ -112,7 +113,7 @@ class SearchEntryCursorAdapter(private val context: Context, database.getStandardIcon(standardIconId) }, { customIconId -> - database.getCustomIcon(customIconId) + database.getCustomIcon(customIconId) ?: IconImageCustom(customIconId) } ) } @@ -122,7 +123,7 @@ class SearchEntryCursorAdapter(private val context: Context, database.getStandardIcon(standardIconId) }, { customIconId -> - database.getCustomIcon(customIconId) + database.getCustomIcon(customIconId) ?: IconImageCustom(customIconId) } ) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt index 3bb1ed0f7..a49096425 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/AssignPasswordInDatabaseRunnable.kt @@ -43,7 +43,7 @@ open class AssignPasswordInDatabaseRunnable ( System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size) val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri) - database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream) + database.assignMasterKey(mMainCredential.masterPassword, uriInputStream) } catch (e: Exception) { erase(mBackupKey) setError(e) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt index 20848b4a5..c98a44eeb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/DatabaseTaskProvider.kt @@ -27,6 +27,7 @@ import android.os.Bundle import android.os.IBinder import android.util.Log import android.widget.Toast +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope import com.kunzisoft.keepass.R @@ -53,6 +54,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion. import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MERGE_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK @@ -354,6 +356,13 @@ class DatabaseTaskProvider { , ACTION_DATABASE_LOAD_TASK) } + fun startDatabaseMerge(fixDuplicateUuid: Boolean) { + start(Bundle().apply { + putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) + } + , ACTION_DATABASE_MERGE_TASK) + } + fun startDatabaseReload(fixDuplicateUuid: Boolean) { start(Bundle().apply { putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid) @@ -361,6 +370,19 @@ class DatabaseTaskProvider { , ACTION_DATABASE_RELOAD_TASK) } + fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) { + if (conditionToAsk) { + AlertDialog.Builder(context) + .setMessage(R.string.warning_database_info_reloaded) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> + approved.invoke() + }.create().show() + } else { + approved.invoke() + } + } + fun startDatabaseAssignPassword(databaseUri: Uri, mainCredential: MainCredential) { 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 a5b2408ca..ef1c2be70 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 @@ -60,7 +60,6 @@ class LoadDatabaseRunnable(private val context: Context, { memoryWanted -> BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) }, - LoadedKey.generateNewCipherKey(), mFixDuplicateUUID, progressTaskUpdater) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt new file mode 100644 index 000000000..1a6a09c95 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/MergeDatabaseRunnable.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2021 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.database.action + +import android.content.Context +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.binary.BinaryData +import com.kunzisoft.keepass.database.element.binary.LoadedKey +import com.kunzisoft.keepass.database.exception.LoadDatabaseException +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.tasks.ProgressTaskUpdater +import com.kunzisoft.keepass.utils.UriUtil + +class MergeDatabaseRunnable(private val context: Context, + private val mDatabase: Database, + private val progressTaskUpdater: ProgressTaskUpdater?, + private val mLoadDatabaseResult: ((Result) -> Unit)?) + : ActionRunnable() { + + override fun onStartRun() { + mDatabase.wasReloaded = true + } + + override fun onActionRun() { + try { + mDatabase.mergeData(context.contentResolver, + { memoryWanted -> + BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) + }, + progressTaskUpdater) + } catch (e: LoadDatabaseException) { + setError(e) + } + + if (result.isSuccess) { + // Register the current time to init the lock timer + PreferencesUtil.saveCurrentTime(context) + } + } + + override fun onFinishRun() { + mLoadDatabaseResult?.invoke(result) + } +} 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 index 7ee45188c..231c63d87 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/ReloadDatabaseRunnable.kt @@ -22,7 +22,6 @@ package com.kunzisoft.keepass.database.action import android.content.Context import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.binary.BinaryData -import com.kunzisoft.keepass.database.element.binary.LoadedKey import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable @@ -35,23 +34,18 @@ class ReloadDatabaseRunnable(private val context: Context, private val mLoadDatabaseResult: ((Result) -> Unit)?) : ActionRunnable() { - private var tempCipherKey: LoadedKey? = null - override fun onStartRun() { - tempCipherKey = mDatabase.binaryCache.loadedCipherKey // Clear before we load - mDatabase.clear(UriUtil.getBinaryDir(context)) + mDatabase.clearIndexesAndBinaries(UriUtil.getBinaryDir(context)) mDatabase.wasReloaded = true } override fun onActionRun() { try { mDatabase.reloadData(context.contentResolver, - UriUtil.getBinaryDir(context), { memoryWanted -> BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) }, - tempCipherKey ?: LoadedKey.generateNewCipherKey(), progressTaskUpdater) } catch (e: LoadDatabaseException) { setError(e) @@ -61,7 +55,6 @@ class ReloadDatabaseRunnable(private val context: Context, // Register the current time to init the lock timer PreferencesUtil.saveCurrentTime(context) } else { - tempCipherKey = null mDatabase.clearAndClose(context) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt index 1816abd60..2175a6b08 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/DeleteNodesRunnable.kt @@ -40,7 +40,7 @@ class DeleteNodesRunnable(context: Context, foreachNode@ for(nodeToDelete in mNodesToDelete) { mOldParent = nodeToDelete.parent - mOldParent?.touch(modified = false, touchParents = true) + nodeToDelete.touch(modified = true, touchParents = true) when (nodeToDelete.type) { Type.GROUP -> { @@ -50,9 +50,9 @@ class DeleteNodesRunnable(context: Context, // Remove Node from parent mCanRecycle = database.canRecycle(groupToDelete) if (mCanRecycle) { - groupToDelete.touch(modified = false, touchParents = true) database.recycle(groupToDelete, context.resources) groupToDelete.setPreviousParentGroup(mOldParent) + groupToDelete.touch(modified = true, touchParents = true) } else { database.deleteGroup(groupToDelete) } @@ -64,9 +64,9 @@ class DeleteNodesRunnable(context: Context, // Remove Node from parent mCanRecycle = database.canRecycle(entryToDelete) if (mCanRecycle) { - entryToDelete.touch(modified = false, touchParents = true) database.recycle(entryToDelete, context.resources) entryToDelete.setPreviousParentGroup(mOldParent) + entryToDelete.touch(modified = true, touchParents = true) } else { database.deleteEntry(entryToDelete) } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt index d93a0af93..17ad24fa3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/action/node/MoveNodesRunnable.kt @@ -43,6 +43,7 @@ class MoveNodesRunnable constructor( foreachNode@ for(nodeToMove in mNodesToMove) { // Move node in new parent mOldParent = nodeToMove.parent + nodeToMove.touch(modified = true, touchParents = true) when (nodeToMove.type) { Type.GROUP -> { @@ -52,9 +53,9 @@ class MoveNodesRunnable constructor( // and if not in the current group && groupToMove != mNewParent && !mNewParent.isContainedIn(groupToMove)) { - groupToMove.touch(modified = true, touchParents = true) database.moveGroupTo(groupToMove, mNewParent) groupToMove.setPreviousParentGroup(mOldParent) + groupToMove.touch(modified = true, touchParents = true) } else { // Only finish thread setError(MoveGroupDatabaseException()) @@ -67,9 +68,9 @@ class MoveNodesRunnable constructor( if (mOldParent != mNewParent // and root can contains entry && (mNewParent != database.rootGroup || database.rootCanContainsEntry())) { - entryToMove.touch(modified = true, touchParents = true) database.moveEntryTo(entryToMove, mNewParent) entryToMove.setPreviousParentGroup(mOldParent) + entryToMove.touch(modified = true, touchParents = true) } else { // Only finish thread setError(MoveEntryDatabaseException()) 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 5e5c6def2..b3732bacc 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 @@ -32,7 +32,6 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.BinaryCache import com.kunzisoft.keepass.database.element.binary.BinaryData -import com.kunzisoft.keepass.database.element.binary.LoadedKey import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.DatabaseKDB import com.kunzisoft.keepass.database.element.database.DatabaseKDBX @@ -52,6 +51,7 @@ import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX +import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchParameters import com.kunzisoft.keepass.icons.IconDrawableFactory @@ -94,6 +94,8 @@ class Database { */ var wasReloaded = false + var dataModifiedSinceLastLoading = false + var loadTimestamp: Long? = null private set @@ -112,7 +114,7 @@ class Database { private val iconsManager: IconsManager get() { - return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache) + return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager() } fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) { @@ -130,7 +132,7 @@ class Database { return iconsManager.doForEachCustomIcon(action) } - fun getCustomIcon(iconId: UUID): IconImageCustom { + fun getCustomIcon(iconId: UUID): IconImageCustom? { return iconsManager.getIcon(iconId) } @@ -144,11 +146,12 @@ class Database { fun removeCustomIcon(customIcon: IconImageCustom) { iconDrawableFactory.clearFromCache(customIcon) - iconsManager.removeCustomIcon(binaryCache, customIcon.uuid) + iconsManager.removeCustomIcon(customIcon.uuid, binaryCache) + mDatabaseKDBX?.addDeletedObject(customIcon.uuid) } fun updateCustomIcon(customIcon: IconImageCustom) { - iconsManager.getIcon(customIcon.uuid).updateWith(customIcon) + iconsManager.getIcon(customIcon.uuid)?.updateWith(customIcon) } fun getTemplates(templateCreation: Boolean): List