/* * 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.services import android.app.PendingIntent import android.content.Intent import android.net.Uri import android.os.Binder import android.os.Build import android.os.Bundle import android.os.IBinder import android.util.Log import androidx.annotation.StringRes import androidx.media.app.NotificationCompat import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.ProgressMessage import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable import com.kunzisoft.keepass.database.action.MergeDatabaseRunnable import com.kunzisoft.keepass.database.action.ReloadDatabaseRunnable import com.kunzisoft.keepass.database.action.RemoveUnlinkedDataDatabaseRunnable import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable import com.kunzisoft.keepass.database.action.UpdateCompressionBinariesDatabaseRunnable import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable import com.kunzisoft.keepass.database.action.node.ActionNodesValues import com.kunzisoft.keepass.database.action.node.AddEntryRunnable import com.kunzisoft.keepass.database.action.node.AddGroupRunnable import com.kunzisoft.keepass.database.action.node.AfterActionNodesFinish import com.kunzisoft.keepass.database.action.node.CopyNodesRunnable import com.kunzisoft.keepass.database.action.node.DeleteNodesRunnable import com.kunzisoft.keepass.database.action.node.MoveNodesRunnable import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable import com.kunzisoft.keepass.database.action.node.UpdateGroupRunnable import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Group 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.hardware.HardwareKey import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode 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.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableList import com.kunzisoft.keepass.utils.putParcelableList import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.util.UUID open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater { override val notificationId: Int = 575 private var mDatabase: ContextualDatabase? = null // File description private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null private var mLastLocalSaveTime: Long = 0 private val mainScope = CoroutineScope(Dispatchers.Main) private var mDatabaseListeners = mutableListOf() private var mDatabaseInfoListeners = mutableListOf() private var mActionTaskBinder = ActionTaskBinder() private var mActionTaskListeners = mutableListOf() // Channel to connect asynchronously a response private var mResponseChallengeChannel: Channel? = null private var mActionRunning = 0 private var mTaskRemovedRequested = false private var mSaveState = false private var mProgressMessage: ProgressMessage = ProgressMessage(R.string.database_opened) override fun retrieveChannelId(): String { return CHANNEL_DATABASE_ID } override fun retrieveChannelName(): String { return getString(R.string.database) } inner class ActionTaskBinder : Binder() { fun getService(): DatabaseTaskNotificationService = this@DatabaseTaskNotificationService fun addDatabaseListener(databaseListener: DatabaseListener) { if (!mDatabaseListeners.contains(databaseListener)) mDatabaseListeners.add(databaseListener) } fun removeDatabaseListener(databaseListener: DatabaseListener) { mDatabaseListeners.remove(databaseListener) } fun addDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) { if (!mDatabaseInfoListeners.contains(databaseInfoListener)) mDatabaseInfoListeners.add(databaseInfoListener) } fun removeDatabaseFileInfoListener(databaseInfoListener: DatabaseInfoListener) { mDatabaseInfoListeners.remove(databaseInfoListener) } fun addActionTaskListener(actionTaskListener: ActionTaskListener) { if (!mActionTaskListeners.contains(actionTaskListener)) mActionTaskListeners.add(actionTaskListener) } fun removeActionTaskListener(actionTaskListener: ActionTaskListener) { mActionTaskListeners.remove(actionTaskListener) } } interface DatabaseListener { fun onDatabaseRetrieved(database: ContextualDatabase?) } interface DatabaseInfoListener { fun onDatabaseInfoChanged( previousDatabaseInfo: SnapFileDatabaseInfo, newDatabaseInfo: SnapFileDatabaseInfo, readOnlyDatabase: Boolean ) } interface ActionTaskListener { fun onActionStarted( database: ContextualDatabase, progressMessage: ProgressMessage ) fun onActionUpdated( database: ContextualDatabase, progressMessage: ProgressMessage ) fun onActionStopped( database: ContextualDatabase? = null ) fun onActionFinished( database: ContextualDatabase, actionTask: String, result: ActionRunnable.Result ) } fun checkDatabase() { mDatabaseListeners.forEach { databaseListener -> databaseListener.onDatabaseRetrieved(mDatabase) } } fun checkDatabaseInfo() { try { mDatabase?.fileUri?.let { val previousDatabaseInfo = mSnapFileDatabaseInfo val lastFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo( FileDatabaseInfo(applicationContext, it)) val oldDatabaseModification = previousDatabaseInfo?.lastModification val newDatabaseModification = lastFileDatabaseInfo.lastModification val oldDatabaseSize = previousDatabaseInfo?.size val conditionExists = previousDatabaseInfo != null && previousDatabaseInfo.exists != lastFileDatabaseInfo.exists // To prevent dialog opening too often // Add 10 seconds delta time to prevent spamming val conditionLastModification = (oldDatabaseModification != null && newDatabaseModification != null && oldDatabaseSize != null && oldDatabaseModification > 0 && newDatabaseModification > 0 && oldDatabaseSize > 0 && oldDatabaseModification < newDatabaseModification && mLastLocalSaveTime + 10000 < newDatabaseModification) if (conditionExists || conditionLastModification) { // Show the dialog only if it's real new info and not a delay after a save Log.i(TAG, "Database file modified " + "$previousDatabaseInfo != $lastFileDatabaseInfo ") // Call listener to indicate a change in database info if (!mSaveState) { mDatabaseInfoListeners.forEach { listener -> listener.onDatabaseInfoChanged( previousDatabaseInfo, lastFileDatabaseInfo, mDatabase?.isReadOnly ?: true ) } } mSnapFileDatabaseInfo = lastFileDatabaseInfo } } } catch (e: Exception) { Log.e(TAG, "Unable to check database info", e) } } fun saveDatabaseInfo() { try { mDatabase?.fileUri?.let { mSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo( FileDatabaseInfo(applicationContext, it)) Log.i(TAG, "Database file saved $mSnapFileDatabaseInfo") } } catch (e: Exception) { Log.e(TAG, "Unable to check database info", e) } } /** * Force to call [ActionTaskListener.onActionStarted] if the action is still running * or [ActionTaskListener.onActionStopped] if the action is no longer running */ fun checkAction() { mDatabase?.let { database -> // Check if action / sub-action is running if (mActionRunning > 0) { mActionTaskListeners.forEach { actionTaskListener -> actionTaskListener.onActionStarted( database, mProgressMessage ) } } else { /* Do not stopped here, service cannot be connected mActionTaskListeners.forEach { actionTaskListener -> actionTaskListener.onActionStopped( database ) }*/ } } } @OptIn(ExperimentalCoroutinesApi::class) private fun sendResponseToChallenge(response: ByteArray) { mainScope.launch { val responseChannel = mResponseChallengeChannel if (responseChannel == null || responseChannel.isEmpty) { if (response.isEmpty()) { cancelChallengeResponse(R.string.error_no_response_from_challenge) } else { mResponseChallengeChannel?.send(response) } } else { cancelChallengeResponse(R.string.error_response_already_provided) } } } private fun initializeChallengeResponse() { // Init the channels if (mResponseChallengeChannel == null) { mResponseChallengeChannel = Channel(0) } } private fun closeChallengeResponse() { mResponseChallengeChannel?.close() mResponseChallengeChannel = null } private fun cancelChallengeResponse(@StringRes error: Int) { mResponseChallengeChannel?.cancel(CancellationException(getString(error))) mResponseChallengeChannel = null } override fun onBind(intent: Intent): IBinder? { super.onBind(intent) return mActionTaskBinder } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) val database = ContextualDatabase.getInstance() if (mDatabase != database) { mDatabase = database mDatabaseListeners.forEach { listener -> listener.onDatabaseRetrieved(mDatabase) } } // Get save state mSaveState = if (intent != null) { if (intent.hasExtra(SAVE_DATABASE_KEY)) { !database.isReadOnly && intent.getBooleanExtra( SAVE_DATABASE_KEY, mSaveState ) } else (intent.action == ACTION_DATABASE_CREATE_TASK || intent.action == ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK || intent.action == ACTION_DATABASE_SAVE) } else false // Create the notification buildNotification(intent) val intentAction = intent?.action if (intentAction == null && !database.loaded) { stopService() } val actionRunnable: ActionRunnable? = when (intentAction) { ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent, database) ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent, database) ACTION_DATABASE_MERGE_TASK -> buildDatabaseMergeActionTask(intent, database) ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database) ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK -> buildDatabaseAssignCredentialActionTask(intent, database) ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent, database) ACTION_DATABASE_UPDATE_GROUP_TASK -> buildDatabaseUpdateGroupActionTask(intent, database) ACTION_DATABASE_CREATE_ENTRY_TASK -> buildDatabaseCreateEntryActionTask(intent, database) ACTION_DATABASE_UPDATE_ENTRY_TASK -> buildDatabaseUpdateEntryActionTask(intent, database) ACTION_DATABASE_COPY_NODES_TASK -> buildDatabaseCopyNodesActionTask(intent, database) ACTION_DATABASE_MOVE_NODES_TASK -> buildDatabaseMoveNodesActionTask(intent, database) ACTION_DATABASE_DELETE_NODES_TASK -> buildDatabaseDeleteNodesActionTask(intent, database) ACTION_DATABASE_RESTORE_ENTRY_HISTORY -> buildDatabaseRestoreEntryHistoryActionTask(intent, database) ACTION_DATABASE_DELETE_ENTRY_HISTORY -> buildDatabaseDeleteEntryHistoryActionTask(intent, database) ACTION_DATABASE_UPDATE_COMPRESSION_TASK -> buildDatabaseUpdateCompressionActionTask(intent, database) ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK -> buildDatabaseRemoveUnlinkedDataActionTask(intent, database) ACTION_DATABASE_UPDATE_NAME_TASK, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK, ACTION_DATABASE_UPDATE_COLOR_TASK, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK, ACTION_DATABASE_UPDATE_PARALLELISM_TASK, ACTION_DATABASE_UPDATE_ITERATIONS_TASK -> buildDatabaseUpdateElementActionTask(intent, database) ACTION_DATABASE_SAVE -> buildDatabaseSaveActionTask(intent, database) ACTION_CHALLENGE_RESPONDED -> buildChallengeRespondedActionTask(intent) else -> null } // Sub action is an action in another action, don't perform pre and post action val isMainAction = intentAction != ACTION_CHALLENGE_RESPONDED // Build and launch the action if (actionRunnable != null) { mainScope.launch { executeAction( this@DatabaseTaskNotificationService, { mActionRunning++ if (isMainAction) { TimeoutHelper.temporarilyDisableTimeout() sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply { putExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId) putExtra(DATABASE_TASK_MESSAGE_KEY, mProgressMessage.messageId) putExtra(DATABASE_TASK_WARNING_KEY, mProgressMessage.warningId) }) mActionTaskListeners.forEach { actionTaskListener -> actionTaskListener.onActionStarted( database, mProgressMessage ) } } }, { actionRunnable }, { result -> if (isMainAction) { try { mActionTaskListeners.forEach { actionTaskListener -> mTaskRemovedRequested = false actionTaskListener.onActionFinished( database, intentAction!!, result ) } } finally { // Save the database info before performing action when (intentAction) { ACTION_DATABASE_LOAD_TASK, ACTION_DATABASE_MERGE_TASK, ACTION_DATABASE_RELOAD_TASK -> { saveDatabaseInfo() } } val save = !database.isReadOnly && (intentAction == ACTION_DATABASE_SAVE || intent?.getBooleanExtra( SAVE_DATABASE_KEY, false ) == true) // Save the database info after performing save action if (save) { database.fileUri?.let { val newSnapFileDatabaseInfo = SnapFileDatabaseInfo.fromFileDatabaseInfo( FileDatabaseInfo(applicationContext, it) ) mLastLocalSaveTime = System.currentTimeMillis() mSnapFileDatabaseInfo = newSnapFileDatabaseInfo } } removeIntentData(intent) TimeoutHelper.releaseTemporarilyDisableTimeout() // Stop service after save if user remove task if (save && mTaskRemovedRequested) { stopService() } else if (TimeoutHelper.checkTimeAndLockIfTimeout(this@DatabaseTaskNotificationService)) { if (!database.loaded) { stopService() } else { // Restart the service to open lock notification try { startService( Intent( applicationContext, DatabaseTaskNotificationService::class.java ) ) } catch (e: IllegalStateException) { Log.w( TAG, "Cannot restart the database task service", e ) } } } mTaskRemovedRequested = false } sendBroadcast(Intent(DATABASE_STOP_TASK_ACTION)) } mActionRunning-- } ) } } return when (intentAction) { ACTION_DATABASE_LOAD_TASK, ACTION_DATABASE_MERGE_TASK, ACTION_DATABASE_RELOAD_TASK, null, -> { START_STICKY } else -> { // Relaunch action if failed START_REDELIVER_INTENT } } } private fun buildNotification(intent: Intent?) { // Assign elements for updates val intentAction = intent?.action // Get icon depending action state val iconId = if (intentAction == null) R.drawable.notification_ic_database_open else R.drawable.notification_ic_database_action // Title depending on action mProgressMessage.titleId = if (intentAction == null) { R.string.database_opened } else when (intentAction) { ACTION_DATABASE_CREATE_TASK -> R.string.creating_database ACTION_DATABASE_LOAD_TASK, ACTION_DATABASE_MERGE_TASK, ACTION_DATABASE_RELOAD_TASK, -> R.string.loading_database ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK, ACTION_DATABASE_SAVE, -> R.string.saving_database else -> { if (mSaveState) R.string.saving_database else R.string.command_execution } } // Updated later mProgressMessage.messageId = null // Warning if data is saved mProgressMessage.warningId = if (mSaveState) R.string.do_not_kill_app else null val notificationBuilder = buildNewNotification().apply { setSmallIcon(iconId) val titleId = mProgressMessage.titleId?.let { intent?.getIntExtra(DATABASE_TASK_TITLE_KEY, it) } ?: R.string.app_name setContentTitle(getString(titleId)) setAutoCancel(false) setContentIntent(null) } if (intentAction == null) { mDatabase?.let { database -> // Database is normally open if (database.loaded) { // Build Intents for notification action val pendingDatabaseIntent = PendingIntent.getActivity( this, randomRequestCode(), Intent(this, GroupActivity::class.java), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT } else { PendingIntent.FLAG_UPDATE_CURRENT } ) val pendingDeleteIntent = PendingIntent.getBroadcast( this, 4576, Intent(LOCK_ACTION), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.FLAG_IMMUTABLE } else { 0 } ) // Add actions in notifications notificationBuilder.apply { setContentText(database.name + " (" + database.version + ")") setContentIntent(pendingDatabaseIntent) // Unfortunately swipe is disabled in lollipop+ setDeleteIntent(pendingDeleteIntent) addAction( R.drawable.ic_lock_database_white_32dp, getString(R.string.lock), pendingDeleteIntent ) // Won't work with Xiaomi and Kitkat if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) { setStyle( NotificationCompat.MediaStyle() .setShowActionsInCompactView(0) ) } } } } } // Create the notification startForegroundCompat( notificationId, notificationBuilder, NotificationServiceType.DATABASE_TASK ) } private fun removeIntentData(intent: Intent?) { intent?.action = null intent?.removeExtra(DATABASE_TASK_TITLE_KEY) intent?.removeExtra(DATABASE_TASK_MESSAGE_KEY) intent?.removeExtra(DATABASE_TASK_WARNING_KEY) intent?.removeExtra(DATABASE_URI_KEY) intent?.removeExtra(MAIN_CREDENTIAL_KEY) intent?.removeExtra(READ_ONLY_KEY) intent?.removeExtra(CIPHER_DATABASE_KEY) intent?.removeExtra(FIX_DUPLICATE_UUID_KEY) intent?.removeExtra(GROUP_KEY) intent?.removeExtra(ENTRY_KEY) intent?.removeExtra(GROUP_ID_KEY) intent?.removeExtra(ENTRY_ID_KEY) intent?.removeExtra(GROUPS_ID_KEY) intent?.removeExtra(ENTRIES_ID_KEY) intent?.removeExtra(PARENT_ID_KEY) intent?.removeExtra(ENTRY_HISTORY_POSITION_KEY) intent?.removeExtra(SAVE_DATABASE_KEY) intent?.removeExtra(OLD_NODES_KEY) intent?.removeExtra(NEW_NODES_KEY) intent?.removeExtra(OLD_ELEMENT_KEY) intent?.removeExtra(NEW_ELEMENT_KEY) } /** * Execute action with a coroutine */ private suspend fun executeAction( progressTaskUpdater: ProgressTaskUpdater, onPreExecute: () -> Unit, onExecute: (ProgressTaskUpdater?) -> ActionRunnable?, onPostExecute: (result: ActionRunnable.Result) -> Unit, ) { onPreExecute.invoke() withContext(Dispatchers.IO) { onExecute.invoke(progressTaskUpdater)?.apply { val asyncResult: Deferred = async { // Run the actionRunnable run() result } withContext(Dispatchers.Main) { onPostExecute.invoke(asyncResult.await()) } } } } private fun notifyProgressMessage() { mDatabase?.let { database -> mActionTaskListeners.forEach { actionTaskListener -> actionTaskListener.onActionUpdated( database, mProgressMessage ) } } } private fun updateMessage(resId: Int) { mProgressMessage = mProgressMessage.copy( messageId = resId ) notifyProgressMessage() } override fun retrievingDatabaseKey() { updateMessage(R.string.retrieving_db_key) } override fun decryptingDatabase() { updateMessage(R.string.decrypting_db) } override fun stopService() { if (!TimeoutHelper.temporarilyDisableLock) { closeDatabase(mDatabase) // Remove the database during the lock // And notify each subscriber mDatabase = null mDatabaseListeners.forEach { listener -> listener.onDatabaseRetrieved(null) } // Remove the lock timer (no more needed if it exists) TimeoutHelper.cancelLockTimer(this) // Service is stopped after receive the broadcast super.stopService() } } override fun onTaskRemoved(rootIntent: Intent?) { if (TimeoutHelper.temporarilyDisableLock) { mTaskRemovedRequested = true } super.onTaskRemoved(rootIntent) } private fun retrieveResponseFromChallenge( hardwareKey: HardwareKey, seed: ByteArray?, ): ByteArray { // Request a challenge - response var response: ByteArray runBlocking { // Initialize the channels initializeChallengeResponse() val previousMessage = mProgressMessage.copy() mProgressMessage.apply { messageId = R.string.waiting_challenge_request cancelable = { cancelChallengeResponse(R.string.error_cancel_by_user) } } // Send the request notifyProgressMessage() HardwareKeyActivity .launchHardwareKeyActivity( context = this@DatabaseTaskNotificationService, hardwareKey = hardwareKey, seed = seed ) // Wait the response mProgressMessage.apply { messageId = R.string.waiting_challenge_response } notifyProgressMessage() response = mResponseChallengeChannel?.receive() ?: byteArrayOf() // Close channels closeChallengeResponse() // Restore previous message mProgressMessage = previousMessage.apply { cancelable = null } notifyProgressMessage() } return response } private fun buildDatabaseCreateActionTask( intent: Intent, database: ContextualDatabase ): ActionRunnable? { if (intent.hasExtra(DATABASE_URI_KEY) && intent.hasExtra(MAIN_CREDENTIAL_KEY) ) { val databaseUri: Uri? = intent.getParcelableExtraCompat(DATABASE_URI_KEY) val mainCredential: MainCredential = intent.getParcelableExtraCompat(MAIN_CREDENTIAL_KEY) ?: MainCredential() if (databaseUri == null) return null return CreateDatabaseRunnable(this, database, databaseUri, getString(R.string.database_default_name), getString(R.string.database), getString(R.string.template_group_name), mainCredential ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) }.apply { afterSaveDatabase = { result -> eraseCredentials(databaseUri) if (result.isSuccess) { // Add database to recent files if (PreferencesUtil.rememberDatabaseLocations(applicationContext)) { FileDatabaseHistoryAction.getInstance(applicationContext) .addOrUpdateDatabaseUri( databaseUri, if (PreferencesUtil.rememberKeyFileLocations( applicationContext ) ) mainCredential.keyFileUri else null, if (PreferencesUtil.rememberHardwareKey(applicationContext)) mainCredential.hardwareKey else null, ) } // Register the current time to init the lock timer PreferencesUtil.saveCurrentTime(applicationContext) } else { Log.e(TAG, "Unable to create the database") } // Pass result to activity result.data = Bundle().apply { putParcelable(DATABASE_URI_KEY, databaseUri) putParcelable(MAIN_CREDENTIAL_KEY, mainCredential) } } } } else { return null } } private fun buildDatabaseLoadActionTask( intent: Intent, database: ContextualDatabase ): ActionRunnable? { if (intent.hasExtra(DATABASE_URI_KEY) && intent.hasExtra(MAIN_CREDENTIAL_KEY) && intent.hasExtra(READ_ONLY_KEY) && intent.hasExtra(CIPHER_DATABASE_KEY) && intent.hasExtra(FIX_DUPLICATE_UUID_KEY) ) { val databaseUri: Uri? = intent.getParcelableExtraCompat(DATABASE_URI_KEY) val mainCredential: MainCredential = intent.getParcelableExtraCompat(MAIN_CREDENTIAL_KEY) ?: MainCredential() val readOnly: Boolean = intent.getBooleanExtra(READ_ONLY_KEY, true) val cipherEncryptDatabase: CipherEncryptDatabase? = intent.getParcelableExtraCompat(CIPHER_DATABASE_KEY) if (databaseUri == null) return null return LoadDatabaseRunnable( this, database, databaseUri, mainCredential, { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) }, readOnly, intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false), this ).apply { afterLoadDatabase = { result -> if (result.isSuccess) { // Save keyFile in app database if (PreferencesUtil.rememberDatabaseLocations(applicationContext)) { FileDatabaseHistoryAction.getInstance(applicationContext) .addOrUpdateDatabaseUri( databaseUri, if (PreferencesUtil.rememberKeyFileLocations(applicationContext)) mainCredential.keyFileUri else null, if (PreferencesUtil.rememberHardwareKey(applicationContext)) mainCredential.hardwareKey else null, ) } // Register the biometric cipherEncryptDatabase?.let { cipherDatabase -> CipherDatabaseAction.getInstance(applicationContext) .addOrUpdateCipherDatabase(cipherDatabase) // return value not called } // Register the current time to init the lock timer PreferencesUtil.saveCurrentTime(applicationContext) } // Add each info to reload database after thrown duplicate UUID exception result.data = Bundle().apply { putParcelable(DATABASE_URI_KEY, databaseUri) putParcelable(MAIN_CREDENTIAL_KEY, mainCredential) putBoolean(READ_ONLY_KEY, readOnly) putParcelable(CIPHER_DATABASE_KEY, cipherEncryptDatabase) } } } } else { return null } } private fun buildDatabaseMergeActionTask( intent: Intent, database: ContextualDatabase ): ActionRunnable { var databaseToMergeUri: Uri? = null var databaseToMergeMainCredential: MainCredential? = null if (intent.hasExtra(DATABASE_URI_KEY)) { databaseToMergeUri = intent.getParcelableExtraCompat(DATABASE_URI_KEY) } if (intent.hasExtra(MAIN_CREDENTIAL_KEY)) { databaseToMergeMainCredential = intent.getParcelableExtraCompat(MAIN_CREDENTIAL_KEY) } val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) return MergeDatabaseRunnable( this, databaseToMergeUri, databaseToMergeMainCredential, { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) }, database, !database.isReadOnly && saveDatabase, { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) }, this ).apply { afterSaveDatabase = { result -> if (result.isSuccess) { PreferencesUtil.saveCurrentTime(applicationContext) } // No need to add each info to merge database result.data = Bundle() } } } private fun buildDatabaseReloadActionTask( database: ContextualDatabase ): ActionRunnable { return ReloadDatabaseRunnable( this, database, this ).apply { afterReloadDatabase = { result -> if (result.isSuccess) { PreferencesUtil.saveCurrentTime(applicationContext) } // No need to add each info to reload database result.data = Bundle() } } } private fun buildDatabaseAssignCredentialActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(DATABASE_URI_KEY) && intent.hasExtra(MAIN_CREDENTIAL_KEY) ) { val databaseUri: Uri = intent.getParcelableExtraCompat(DATABASE_URI_KEY) ?: return null SaveDatabaseRunnable( this, database, saveDatabase = true, intent.getParcelableExtraCompat(MAIN_CREDENTIAL_KEY) ?: MainCredential(), { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) }, null ).apply { afterSaveDatabase = { eraseCredentials(databaseUri) } } } else { null } } private fun eraseCredentials(databaseUri: Uri) { // Erase the biometric CipherDatabaseAction.getInstance(this) .deleteByDatabaseUri(databaseUri) // Erase the register keyfile FileDatabaseHistoryAction.getInstance(this) .deleteKeyFileByDatabaseUri(databaseUri) } private inner class AfterActionNodesRunnable : AfterActionNodesFinish() { override fun onActionNodesFinish( result: ActionRunnable.Result, actionNodesValues: ActionNodesValues, ) { val bundle = result.data ?: Bundle() bundle.putBundle(OLD_NODES_KEY, getBundleFromListNodes(actionNodesValues.oldNodes)) bundle.putBundle(NEW_NODES_KEY, getBundleFromListNodes(actionNodesValues.newNodes)) result.data = bundle } } private fun buildDatabaseCreateGroupActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(GROUP_KEY) && intent.hasExtra(PARENT_ID_KEY) && intent.hasExtra(SAVE_DATABASE_KEY) ) { val parentId: NodeId<*>? = intent.getParcelableExtraCompat(PARENT_ID_KEY) val newGroup: Group? = intent.getParcelableExtraCompat(GROUP_KEY) if (parentId == null || newGroup == null) return null val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) database.getGroupById(parentId)?.let { parent -> AddGroupRunnable(this, database, newGroup, parent, !database.isReadOnly && saveDatabase, AfterActionNodesRunnable() ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) } } } else { null } } private fun buildDatabaseUpdateGroupActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(GROUP_ID_KEY) && intent.hasExtra(GROUP_KEY) && intent.hasExtra(SAVE_DATABASE_KEY) ) { val groupId: NodeId<*>? = intent.getParcelableExtraCompat(GROUP_ID_KEY) val newGroup: Group? = intent.getParcelableExtraCompat(GROUP_KEY) if (groupId == null || newGroup == null) return null val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) database.getGroupById(groupId)?.let { oldGroup -> UpdateGroupRunnable(this, database, oldGroup, newGroup, !database.isReadOnly && saveDatabase, AfterActionNodesRunnable() ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) } } } else { null } } private fun buildDatabaseCreateEntryActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(ENTRY_KEY) && intent.hasExtra(PARENT_ID_KEY) && intent.hasExtra(SAVE_DATABASE_KEY) ) { val parentId: NodeId<*>? = intent.getParcelableExtraCompat(PARENT_ID_KEY) val newEntry: Entry? = intent.getParcelableExtraCompat(ENTRY_KEY) if (parentId == null || newEntry == null) return null val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) database.getGroupById(parentId)?.let { parent -> AddEntryRunnable(this, database, newEntry, parent, !database.isReadOnly && saveDatabase, AfterActionNodesRunnable() ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) } } } else { null } } private fun buildDatabaseUpdateEntryActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(ENTRY_ID_KEY) && intent.hasExtra(ENTRY_KEY) && intent.hasExtra(SAVE_DATABASE_KEY) ) { val entryId: NodeId? = intent.getParcelableExtraCompat(ENTRY_ID_KEY) val newEntry: Entry? = intent.getParcelableExtraCompat(ENTRY_KEY) if (entryId == null || newEntry == null) return null val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) database.getEntryById(entryId)?.let { oldEntry -> UpdateEntryRunnable(this, database, oldEntry, newEntry, !database.isReadOnly && saveDatabase, AfterActionNodesRunnable() ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) } } } else { null } } private fun buildDatabaseCopyNodesActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(GROUPS_ID_KEY) && intent.hasExtra(ENTRIES_ID_KEY) && intent.hasExtra(PARENT_ID_KEY) && intent.hasExtra(SAVE_DATABASE_KEY) ) { val parentId: NodeId<*> = intent.getParcelableExtraCompat(PARENT_ID_KEY) ?: return null val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) database.getGroupById(parentId)?.let { newParent -> CopyNodesRunnable(this, database, getListNodesFromBundle(database, intent.extras!!), newParent, !database.isReadOnly && saveDatabase, AfterActionNodesRunnable() ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) } } } else { null } } private fun buildDatabaseMoveNodesActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(GROUPS_ID_KEY) && intent.hasExtra(ENTRIES_ID_KEY) && intent.hasExtra(PARENT_ID_KEY) && intent.hasExtra(SAVE_DATABASE_KEY) ) { val parentId: NodeId<*> = intent.getParcelableExtraCompat(PARENT_ID_KEY) ?: return null val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) database.getGroupById(parentId)?.let { newParent -> MoveNodesRunnable(this, database, getListNodesFromBundle(database, intent.extras!!), newParent, !database.isReadOnly && saveDatabase, AfterActionNodesRunnable() ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) } } } else { null } } private fun buildDatabaseDeleteNodesActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(GROUPS_ID_KEY) && intent.hasExtra(ENTRIES_ID_KEY) && intent.hasExtra(SAVE_DATABASE_KEY) ) { val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) DeleteNodesRunnable(this, database, getListNodesFromBundle(database, intent.extras!!), resources.getString(R.string.recycle_bin), !database.isReadOnly && saveDatabase, AfterActionNodesRunnable() ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) } } else { null } } private fun buildDatabaseRestoreEntryHistoryActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(ENTRY_ID_KEY) && intent.hasExtra(ENTRY_HISTORY_POSITION_KEY) && intent.hasExtra(SAVE_DATABASE_KEY) ) { val entryId: NodeId = intent.getParcelableExtraCompat(ENTRY_ID_KEY) ?: return null val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) database.getEntryById(entryId)?.let { mainEntry -> RestoreEntryHistoryDatabaseRunnable(this, database, mainEntry, intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1), !database.isReadOnly && saveDatabase ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) } } } else { null } } private fun buildDatabaseDeleteEntryHistoryActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(ENTRY_ID_KEY) && intent.hasExtra(ENTRY_HISTORY_POSITION_KEY) && intent.hasExtra(SAVE_DATABASE_KEY) ) { val entryId: NodeId = intent.getParcelableExtraCompat(ENTRY_ID_KEY) ?: return null val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) database.getEntryById(entryId)?.let { mainEntry -> DeleteEntryHistoryDatabaseRunnable(this, database, mainEntry, intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1), !database.isReadOnly && saveDatabase ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) } } } else { null } } private fun buildDatabaseUpdateCompressionActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(OLD_ELEMENT_KEY) && intent.hasExtra(NEW_ELEMENT_KEY) && intent.hasExtra(SAVE_DATABASE_KEY) ) { val oldElement: CompressionAlgorithm? = intent.getParcelableExtraCompat(OLD_ELEMENT_KEY) val newElement: CompressionAlgorithm? = intent.getParcelableExtraCompat(NEW_ELEMENT_KEY) if (oldElement == null || newElement == null) return null val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) return UpdateCompressionBinariesDatabaseRunnable(this, database, oldElement, newElement, !database.isReadOnly && saveDatabase ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) }.apply { afterSaveDatabase = { result -> result.data = intent.extras } } } else { null } } private fun buildDatabaseRemoveUnlinkedDataActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(SAVE_DATABASE_KEY)) { val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) return RemoveUnlinkedDataDatabaseRunnable(this, database, !database.isReadOnly && saveDatabase ) { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) }.apply { afterSaveDatabase = { result -> result.data = intent.extras } } } else { null } } private fun buildDatabaseUpdateElementActionTask( intent: Intent, database: ContextualDatabase, ): ActionRunnable? { return if (intent.hasExtra(SAVE_DATABASE_KEY)) { val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) return SaveDatabaseRunnable(this, database, !database.isReadOnly && saveDatabase, null, { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) } ).apply { afterSaveDatabase = { result -> result.data = intent.extras } } } else { null } } /** * Save database without parameter */ private fun buildDatabaseSaveActionTask( intent: Intent, database: ContextualDatabase ): ActionRunnable? { return if (intent.hasExtra(SAVE_DATABASE_KEY)) { val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false) var databaseCopyUri: Uri? = null if (intent.hasExtra(DATABASE_URI_KEY)) { databaseCopyUri = intent.getParcelableExtraCompat(DATABASE_URI_KEY) } SaveDatabaseRunnable(this, database, !database.isReadOnly && saveDatabase, null, { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) }, databaseCopyUri) } else { null } } private fun buildChallengeRespondedActionTask(intent: Intent): ActionRunnable? { return if (intent.hasExtra(DATA_BYTES)) { object : ActionRunnable() { override fun onStartRun() {} override fun onActionRun() { mainScope.launch { intent.getByteArrayExtra(DATA_BYTES)?.let { response -> sendResponseToChallenge(response) } } } override fun onFinishRun() {} } } else { null } } companion object { private val TAG = DatabaseTaskNotificationService::class.java.name private const val CHANNEL_DATABASE_ID = "com.kunzisoft.keepass.notification.channel.database" const val ACTION_DATABASE_CREATE_TASK = "ACTION_DATABASE_CREATE_TASK" const val ACTION_DATABASE_LOAD_TASK = "ACTION_DATABASE_LOAD_TASK" const val ACTION_DATABASE_MERGE_TASK = "ACTION_DATABASE_MERGE_TASK" const val ACTION_DATABASE_RELOAD_TASK = "ACTION_DATABASE_RELOAD_TASK" const val ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK = "ACTION_DATABASE_ASSIGN_CREDENTIAL_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" const val ACTION_DATABASE_CREATE_ENTRY_TASK = "ACTION_DATABASE_CREATE_ENTRY_TASK" const val ACTION_DATABASE_UPDATE_ENTRY_TASK = "ACTION_DATABASE_UPDATE_ENTRY_TASK" const val ACTION_DATABASE_COPY_NODES_TASK = "ACTION_DATABASE_COPY_NODES_TASK" const val ACTION_DATABASE_MOVE_NODES_TASK = "ACTION_DATABASE_MOVE_NODES_TASK" const val ACTION_DATABASE_DELETE_NODES_TASK = "ACTION_DATABASE_DELETE_NODES_TASK" const val ACTION_DATABASE_RESTORE_ENTRY_HISTORY = "ACTION_DATABASE_RESTORE_ENTRY_HISTORY" const val ACTION_DATABASE_DELETE_ENTRY_HISTORY = "ACTION_DATABASE_DELETE_ENTRY_HISTORY" const val ACTION_DATABASE_UPDATE_NAME_TASK = "ACTION_DATABASE_UPDATE_NAME_TASK" const val ACTION_DATABASE_UPDATE_DESCRIPTION_TASK = "ACTION_DATABASE_UPDATE_DESCRIPTION_TASK" const val ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK = "ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK" const val ACTION_DATABASE_UPDATE_COLOR_TASK = "ACTION_DATABASE_UPDATE_COLOR_TASK" const val ACTION_DATABASE_UPDATE_COMPRESSION_TASK = "ACTION_DATABASE_UPDATE_COMPRESSION_TASK" const val ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK = "ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK" const val ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK = "ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK" const val ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK = "ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK" const val ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK = "ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK" const val ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK ="ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK" const val ACTION_DATABASE_UPDATE_ENCRYPTION_TASK = "ACTION_DATABASE_UPDATE_ENCRYPTION_TASK" const val ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK ="ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK" const val ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK ="ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK" const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK ="ACTION_DATABASE_UPDATE_PARALLELISM_TASK" const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK" const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE" const val ACTION_CHALLENGE_RESPONDED = "ACTION_CHALLENGE_RESPONDED" const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY" const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY" const val DATABASE_TASK_WARNING_KEY = "DATABASE_TASK_WARNING_KEY" const val DATABASE_URI_KEY = "DATABASE_URI_KEY" const val MAIN_CREDENTIAL_KEY = "MAIN_CREDENTIAL_KEY" const val READ_ONLY_KEY = "READ_ONLY_KEY" const val CIPHER_DATABASE_KEY = "CIPHER_DATABASE_KEY" const val FIX_DUPLICATE_UUID_KEY = "FIX_DUPLICATE_UUID_KEY" const val GROUP_KEY = "GROUP_KEY" const val ENTRY_KEY = "ENTRY_KEY" const val GROUP_ID_KEY = "GROUP_ID_KEY" const val ENTRY_ID_KEY = "ENTRY_ID_KEY" const val GROUPS_ID_KEY = "GROUPS_ID_KEY" const val ENTRIES_ID_KEY = "ENTRIES_ID_KEY" const val PARENT_ID_KEY = "PARENT_ID_KEY" const val ENTRY_HISTORY_POSITION_KEY = "ENTRY_HISTORY_POSITION_KEY" const val SAVE_DATABASE_KEY = "SAVE_DATABASE_KEY" const val OLD_NODES_KEY = "OLD_NODES_KEY" const val NEW_NODES_KEY = "NEW_NODES_KEY" const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time const val DATA_BYTES = "DATA_BYTES" fun getListNodesFromBundle(database: ContextualDatabase, bundle: Bundle): List { val nodesAction = ArrayList() bundle.getParcelableList>(GROUPS_ID_KEY)?.forEach { database.getGroupById(it)?.let { groupRetrieve -> nodesAction.add(groupRetrieve) } } bundle.getParcelableList>(ENTRIES_ID_KEY)?.forEach { database.getEntryById(it)?.let { entryRetrieve -> nodesAction.add(entryRetrieve) } } return nodesAction } fun Bundle.getNewEntry(database: ContextualDatabase): Entry? { getBundle(NEW_NODES_KEY) ?.getParcelableList>(ENTRIES_ID_KEY) ?.get(0)?.let { return database.getEntryById(it) } return null } fun getBundleFromListNodes(nodes: List): Bundle { val groupsId = mutableListOf>() val entriesId = mutableListOf>() nodes.forEach { nodeVersioned -> when (nodeVersioned.type) { Type.GROUP -> { groupsId.add((nodeVersioned as Group).nodeId) } Type.ENTRY -> { entriesId.add((nodeVersioned as Entry).nodeId) } } } return Bundle().apply { putParcelableList(GROUPS_ID_KEY, groupsId) putParcelableList(ENTRIES_ID_KEY, entriesId) } } } }