mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
1429 lines
59 KiB
Kotlin
1429 lines
59 KiB
Kotlin
/*
|
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
*
|
|
* This file is part of KeePassDX.
|
|
*
|
|
* KeePassDX is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* KeePassDX is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
package com.kunzisoft.keepass.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<DatabaseListener>()
|
|
private var mDatabaseInfoListeners = mutableListOf<DatabaseInfoListener>()
|
|
private var mActionTaskBinder = ActionTaskBinder()
|
|
private var mActionTaskListeners = mutableListOf<ActionTaskListener>()
|
|
// Channel to connect asynchronously a response
|
|
private var mResponseChallengeChannel: Channel<ByteArray?>? = 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<ActionRunnable.Result> = 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<UUID>? = 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<UUID> = 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<UUID> = 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<Node> {
|
|
val nodesAction = ArrayList<Node>()
|
|
bundle.getParcelableList<NodeId<*>>(GROUPS_ID_KEY)?.forEach {
|
|
database.getGroupById(it)?.let { groupRetrieve ->
|
|
nodesAction.add(groupRetrieve)
|
|
}
|
|
}
|
|
bundle.getParcelableList<NodeId<UUID>>(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<NodeId<UUID>>(ENTRIES_ID_KEY)
|
|
?.get(0)?.let {
|
|
return database.getEntryById(it)
|
|
}
|
|
return null
|
|
}
|
|
|
|
fun getBundleFromListNodes(nodes: List<Node>): Bundle {
|
|
val groupsId = mutableListOf<NodeId<*>>()
|
|
val entriesId = mutableListOf<NodeId<UUID>>()
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|