From d251788b1a1299de8d3205de59c73626568a5cfe Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Sat, 29 Nov 2025 13:04:01 +0100 Subject: [PATCH] fix: Add User Verification for Entry Edition #2283 --- .../keepass/activities/EntryActivity.kt | 79 ++++++++++++------- .../keepass/activities/GroupActivity.kt | 43 ++++++++-- .../UserVerificationData.kt | 9 +++ .../UserVerificationHelper.kt | 20 ++--- .../activity/PasskeyLauncherActivity.kt | 47 +++++++---- .../keepass/viewmodels/EntryViewModel.kt | 22 ++++-- .../viewmodels/UserVerificationViewModel.kt | 22 +++--- 7 files changed, 164 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationData.kt 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 dfd503f85..5a77ca69c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -41,6 +41,9 @@ import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat import androidx.core.graphics.ColorUtils import androidx.core.view.ViewCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.AppBarLayout @@ -53,6 +56,8 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.adapters.TagsAdapter import com.kunzisoft.keepass.credentialprovider.SpecialMode +import com.kunzisoft.keepass.credentialprovider.UserVerificationData +import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.element.Attachment @@ -79,6 +84,8 @@ import com.kunzisoft.keepass.view.hideByFading import com.kunzisoft.keepass.view.setTransparentNavigationBar import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.viewmodels.EntryViewModel +import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel +import kotlinx.coroutines.launch import java.util.EnumSet import java.util.UUID @@ -100,14 +107,10 @@ class EntryActivity : DatabaseLockActivity() { private var loadingView: ProgressBar? = null private val mEntryViewModel: EntryViewModel by viewModels() + private val mUserVerificationViewModel: UserVerificationViewModel by viewModels() private val mEntryActivityEducation = EntryActivityEducation(this) - private var mMainEntryId: NodeId? = null - private var mHistoryPosition: Int = -1 - private var mEntryIsHistory: Boolean = false - private var mEntryLoaded = false - private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mExternalFileHelper: ExternalFileHelper? = null private var mAttachmentSelected: Attachment? = null @@ -238,13 +241,9 @@ class EntryActivity : DatabaseLockActivity() { mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory -> if (entryInfoHistory != null) { - this.mMainEntryId = entryInfoHistory.mainEntryId - // Manage history position val historyPosition = entryInfoHistory.historyPosition - this.mHistoryPosition = historyPosition val entryIsHistory = historyPosition > -1 - this.mEntryIsHistory = entryIsHistory // Assign history dedicated view historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE // TODO History badge @@ -279,7 +278,6 @@ class EntryActivity : DatabaseLockActivity() { mForegroundColor = if (showEntryColors) entryInfo.foregroundColor else null loadingView?.hideByFading() - mEntryLoaded = true } else { finish() } @@ -322,6 +320,33 @@ class EntryActivity : DatabaseLockActivity() { ) } } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + mUserVerificationViewModel.uiState.collect { uIState -> + when (uIState) { + is UserVerificationViewModel.UIState.Loading -> {} + is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> { + mUserVerificationViewModel.onUserVerificationReceived() + } + is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> { + uIState.dataToVerify.database?.let { database -> + uIState.dataToVerify.entryId?.let { entryId -> + EntryEditActivity.launch( + activity = this@EntryActivity, + database = database, + registrationType = EntryEditActivity.RegistrationType.UPDATE, + nodeId = entryId, + activityResultLauncher = mEntryActivityResultLauncher + ) + } + } + mUserVerificationViewModel.onUserVerificationReceived() + } + } + } + } + } } override fun finishActivityIfReloadRequested(): Boolean { @@ -410,13 +435,13 @@ class EntryActivity : DatabaseLockActivity() { override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) - if (mEntryLoaded) { + if (mEntryViewModel.entryLoaded) { val inflater = menuInflater inflater.inflate(R.menu.entry, menu) inflater.inflate(R.menu.database, menu) - if (mEntryIsHistory && !mDatabaseReadOnly) { + if (mEntryViewModel.entryIsHistory && !mDatabaseReadOnly) { inflater.inflate(R.menu.entry_history, menu) } @@ -429,7 +454,7 @@ class EntryActivity : DatabaseLockActivity() { } override fun onPrepareOptionsMenu(menu: Menu?): Boolean { - if (mEntryIsHistory || mDatabaseReadOnly) { + if (mEntryViewModel.entryIsHistory || mDatabaseReadOnly) { menu?.findItem(R.id.menu_save_database)?.isVisible = false menu?.findItem(R.id.menu_merge_database)?.isVisible = false menu?.findItem(R.id.menu_edit)?.isVisible = false @@ -477,31 +502,27 @@ class EntryActivity : DatabaseLockActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_edit -> { - mDatabase?.let { database -> - mMainEntryId?.let { entryId -> - EntryEditActivity.launch( - activity = this, - database = database, - registrationType = EntryEditActivity.RegistrationType.UPDATE, - nodeId = entryId, - activityResultLauncher = mEntryActivityResultLauncher - ) - } - } + askUserVerification( + userVerificationViewModel = mUserVerificationViewModel, + userVerificationCondition = true, + dataToVerify = UserVerificationData(mDatabase, mEntryViewModel.mainEntryId) + ) return true } R.id.menu_restore_entry_history -> { - mMainEntryId?.let { mainEntryId -> + mEntryViewModel.mainEntryId?.let { mainEntryId -> restoreEntryHistory( mainEntryId, - mHistoryPosition) + mEntryViewModel.historyPosition + ) } } R.id.menu_delete_entry_history -> { - mMainEntryId?.let { mainEntryId -> + mEntryViewModel.mainEntryId?.let { mainEntryId -> deleteEntryHistory( mainEntryId, - mHistoryPosition) + mEntryViewModel.historyPosition + ) } } R.id.menu_save_database -> { @@ -521,7 +542,7 @@ class EntryActivity : DatabaseLockActivity() { override fun finish() { // Transit data in previous Activity after an update Intent().apply { - putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId) + putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntryViewModel.mainEntryId) setResult(RESULT_OK, this) } super.finish() 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 66b55253e..9c9a1c331 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -52,7 +52,9 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.RecyclerView import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.timepicker.MaterialTimePicker @@ -73,6 +75,8 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.TypeMode +import com.kunzisoft.keepass.credentialprovider.UserVerificationData +import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult import com.kunzisoft.keepass.database.ContextualDatabase @@ -122,6 +126,7 @@ import com.kunzisoft.keepass.view.updateLockPaddingStart import com.kunzisoft.keepass.viewmodels.GroupEditViewModel import com.kunzisoft.keepass.viewmodels.GroupViewModel import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel +import com.kunzisoft.keepass.viewmodels.UserVerificationViewModel import kotlinx.coroutines.launch import org.joda.time.LocalDateTime import java.util.EnumSet @@ -158,6 +163,7 @@ class GroupActivity : DatabaseLockActivity(), private val mGroupViewModel: GroupViewModel by viewModels() private val mGroupEditViewModel: GroupEditViewModel by viewModels() private val mMainCredentialViewModel: MainCredentialViewModel by viewModels() + private val mUserVerificationViewModel: UserVerificationViewModel by viewModels() private val mGroupActivityEducation = GroupActivityEducation(this) @@ -565,6 +571,33 @@ class GroupActivity : DatabaseLockActivity(), } } } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + mUserVerificationViewModel.uiState.collect { uIState -> + when (uIState) { + is UserVerificationViewModel.UIState.Loading -> {} + is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> { + mUserVerificationViewModel.onUserVerificationReceived() + } + is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> { + uIState.dataToVerify.database?.let { database -> + uIState.dataToVerify.entryId?.let { entryId -> + EntryEditActivity.launch( + activity = this@GroupActivity, + database = database, + registrationType = EntryEditActivity.RegistrationType.UPDATE, + nodeId = entryId, + activityResultLauncher = mEntryActivityResultLauncher + ) + } + } + mUserVerificationViewModel.onUserVerificationReceived() + } + } + } + } + } } override fun viewToInvalidateTimeout(): View? { @@ -1060,12 +1093,10 @@ class GroupActivity : DatabaseLockActivity(), launchDialogForGroupUpdate(node as Group) } Type.ENTRY -> { - EntryEditActivity.launch( - activity = this@GroupActivity, - database = database, - registrationType = EntryEditActivity.RegistrationType.UPDATE, - nodeId = (node as Entry).nodeId, - activityResultLauncher = mEntryActivityResultLauncher + askUserVerification( + userVerificationViewModel = mUserVerificationViewModel, + userVerificationCondition = true, + dataToVerify = UserVerificationData(database,node.nodeId) ) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationData.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationData.kt new file mode 100644 index 000000000..f2f8e8642 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationData.kt @@ -0,0 +1,9 @@ +package com.kunzisoft.keepass.credentialprovider + +import com.kunzisoft.keepass.database.ContextualDatabase +import com.kunzisoft.keepass.database.element.node.NodeId + +data class UserVerificationData( + val database: ContextualDatabase? = null, + val entryId: NodeId<*>? = null + ) \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt index cb391a87e..31c3e1fa8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerificationHelper.kt @@ -14,7 +14,6 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment.Companion.TAG_ASK_MAIN_CREDENTIAL import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement -import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.utils.getEnumExtra import com.kunzisoft.keepass.utils.putEnumExtra import com.kunzisoft.keepass.view.toastError @@ -86,12 +85,13 @@ class UserVerificationHelper { * Ask for the database credential otherwise */ fun FragmentActivity.askUserVerification( - database: ContextualDatabase?, - userVerificationViewModel: UserVerificationViewModel + userVerificationViewModel: UserVerificationViewModel, + userVerificationCondition: Boolean, + dataToVerify: UserVerificationData ) { - if (this.intent.getUserVerificationCondition()) { + if (userVerificationCondition) { // Important to check the nullable database here - database?.let { + dataToVerify.database?.let { if (isAuthenticatorsAllowed()) { BiometricPrompt( this, ContextCompat.getMainExecutor(this), @@ -113,20 +113,20 @@ class UserVerificationHelper { toastError(SecurityException("Authentication error: $errString")) } } - userVerificationViewModel.onUserVerificationFailed(database) + userVerificationViewModel.onUserVerificationFailed(dataToVerify) } override fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult ) { super.onAuthenticationSucceeded(result) - userVerificationViewModel.onUserVerificationSucceeded(database) + userVerificationViewModel.onUserVerificationSucceeded(dataToVerify) } override fun onAuthenticationFailed() { super.onAuthenticationFailed() toastError(SecurityException(getString(R.string.device_unlock_not_recognized))) - userVerificationViewModel.onUserVerificationFailed(database) + userVerificationViewModel.onUserVerificationFailed(dataToVerify) } }).authenticate( BiometricPrompt.PromptInfo.Builder() @@ -141,7 +141,7 @@ class UserVerificationHelper { .findFragmentByTag(TAG_ASK_MAIN_CREDENTIAL) as? MainCredentialDialogFragment? if (mainCredentialDialogFragment == null) { mainCredentialDialogFragment = MainCredentialDialogFragment - .getInstance(database.fileUri) + .getInstance(dataToVerify.database.fileUri) mainCredentialDialogFragment.show( supportFragmentManager, TAG_ASK_MAIN_CREDENTIAL @@ -150,7 +150,7 @@ class UserVerificationHelper { } } } else { - userVerificationViewModel.onUserVerificationSucceeded(database) + userVerificationViewModel.onUserVerificationSucceeded(dataToVerify) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt index 3fb416acc..2fa8a0dd7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt @@ -31,7 +31,9 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity import com.kunzisoft.keepass.activities.GroupActivity @@ -43,6 +45,7 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.TypeMode +import com.kunzisoft.keepass.credentialprovider.UserVerificationData import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.addUserVerification import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerificationCondition @@ -186,19 +189,23 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { } } lifecycleScope.launch { - userVerificationViewModel.uiState.collect { uiState -> - when (uiState) { - is UserVerificationViewModel.UIState.Loading -> {} - is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> { - passkeyLauncherViewModel.launchActionIfNeeded( - userVerified = true, - intent = intent, - specialMode = mSpecialMode, - database = uiState.database - ) - } - is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> { - passkeyLauncherViewModel.cancelResult() + repeatOnLifecycle(Lifecycle.State.RESUMED) { + userVerificationViewModel.uiState.collect { uiState -> + when (uiState) { + is UserVerificationViewModel.UIState.Loading -> {} + is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> { + passkeyLauncherViewModel.launchActionIfNeeded( + userVerified = true, + intent = intent, + specialMode = mSpecialMode, + database = uiState.dataToVerify.database + ) + userVerificationViewModel.onUserVerificationReceived() + } + is UserVerificationViewModel.UIState.OnUserVerificationCanceled -> { + passkeyLauncherViewModel.cancelResult() + userVerificationViewModel.onUserVerificationReceived() + } } } } @@ -208,9 +215,11 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) { super.onUnknownDatabaseRetrieved(database) // To manage https://github.com/Kunzisoft/KeePassDX/issues/2283 + // When a database is opened askUserVerification( - database = database, - userVerificationViewModel = userVerificationViewModel + userVerificationViewModel = userVerificationViewModel, + userVerificationCondition = intent.getUserVerificationCondition(), + dataToVerify = UserVerificationData(database) ) } @@ -227,9 +236,13 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { } ACTION_DATABASE_CHECK_CREDENTIAL_TASK -> { if (result.isSuccess) { - userVerificationViewModel.onUserVerificationSucceeded(database) + userVerificationViewModel.onUserVerificationSucceeded( + UserVerificationData(database) + ) } else { - userVerificationViewModel.onUserVerificationFailed(database) + userVerificationViewModel.onUserVerificationFailed( + UserVerificationData(database) + ) } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryViewModel.kt index 0708f6527..ea2eaf18b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryViewModel.kt @@ -36,8 +36,14 @@ import java.util.UUID class EntryViewModel: ViewModel() { - private var mMainEntryId: NodeId? = null - private var mHistoryPosition: Int = -1 + var mainEntryId: NodeId? = null + private set + var historyPosition: Int = -1 + private set + var entryIsHistory: Boolean = false + private set + var entryLoaded = false + private set val entryInfoHistory : LiveData get() = _entryInfoHistory private val _entryInfoHistory = MutableLiveData() @@ -60,12 +66,12 @@ class EntryViewModel: ViewModel() { private val _historySelected = SingleLiveEvent() fun loadDatabase(database: ContextualDatabase?) { - loadEntry(database, mMainEntryId, mHistoryPosition) + loadEntry(database, mainEntryId, historyPosition) } fun loadEntry(database: ContextualDatabase?, mainEntryId: NodeId?, historyPosition: Int = -1) { - this.mMainEntryId = mainEntryId - this.mHistoryPosition = historyPosition + this.mainEntryId = mainEntryId + this.historyPosition = historyPosition if (database != null && mainEntryId != null) { IOActionTask( @@ -104,6 +110,12 @@ class EntryViewModel: ViewModel() { } }, { entryInfoHistory -> + if (entryInfoHistory != null) { + this.mainEntryId = entryInfoHistory.mainEntryId + this.historyPosition = historyPosition + this.entryIsHistory = historyPosition > -1 + this.entryLoaded = true + } _entryInfoHistory.value = entryInfoHistory _entryHistory.value = entryInfoHistory?.entryHistory } diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/UserVerificationViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/UserVerificationViewModel.kt index 3b3da6a00..acf0e25d7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/UserVerificationViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/UserVerificationViewModel.kt @@ -1,7 +1,7 @@ package com.kunzisoft.keepass.viewmodels import androidx.lifecycle.ViewModel -import com.kunzisoft.keepass.database.ContextualDatabase +import com.kunzisoft.keepass.credentialprovider.UserVerificationData import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -13,22 +13,22 @@ class UserVerificationViewModel: ViewModel() { private val mUiState = MutableStateFlow(UIState.Loading) val uiState: StateFlow = mUiState - fun onUserVerificationSucceeded(database: ContextualDatabase?) { - mUiState.value = UIState.OnUserVerificationSucceeded(database) + fun onUserVerificationSucceeded(dataToVerify: UserVerificationData) { + mUiState.value = UIState.OnUserVerificationSucceeded(dataToVerify) } - fun onUserVerificationFailed(database: ContextualDatabase? = null) { - mUiState.value = UIState.OnUserVerificationCanceled(database) + fun onUserVerificationFailed(dataToVerify: UserVerificationData = UserVerificationData()) { + mUiState.value = UIState.OnUserVerificationCanceled(dataToVerify) + } + + fun onUserVerificationReceived() { + mUiState.value = UIState.Loading } sealed class UIState { object Loading: UIState() - data class OnUserVerificationSucceeded( - val database: ContextualDatabase? - ): UIState() - data class OnUserVerificationCanceled( - val database: ContextualDatabase? - ): UIState() + data class OnUserVerificationSucceeded(val dataToVerify: UserVerificationData): UIState() + data class OnUserVerificationCanceled(val dataToVerify: UserVerificationData): UIState() } } \ No newline at end of file