From 9ec302908df1a3443ba5cce63a29b34a92de533f Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Sat, 24 Jun 2023 15:51:14 +0530 Subject: [PATCH] Migrate TOTP screen to Compose from XML (#616) * migrate totp * formatting fixes --- app/src/main/AndroidManifest.xml | 3 - .../keypass/ui/addTOTP/AddTOTPActivity.kt | 298 +++++++++++------- .../keypass/ui/addTOTP/AddTOTPViewModel.kt | 57 +--- .../ui/home/components/AccountsList.kt | 4 +- .../ui/nav/DashboardComposeActivity.kt | 2 + .../keypass/ui/nav/NavigationModel.kt | 3 +- .../ui/redux/actions/IntentNavigation.kt | 1 - .../middlewares/IntentNavigationMiddleware.kt | 5 - .../res/layout/activity_add_totpactivity.xml | 78 ----- app/src/main/res/values/strings.xml | 1 + .../yogeshpaliyal/common/data/AccountModel.kt | 3 +- 11 files changed, 198 insertions(+), 257 deletions(-) delete mode 100644 app/src/main/res/layout/activity_add_totpactivity.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2593c579..eaa3d566 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,9 +20,6 @@ - diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt index b83a40c3..acfaa6bc 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt @@ -1,142 +1,202 @@ package com.yogeshpaliyal.keypass.ui.addTOTP -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible -import androidx.lifecycle.Observer -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import com.google.zxing.integration.android.IntentIntegrator +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBackIosNew +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Done +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.yogeshpaliyal.common.data.AccountModel import com.yogeshpaliyal.common.utils.TOTPHelper import com.yogeshpaliyal.keypass.R -import com.yogeshpaliyal.keypass.databinding.ActivityAddTotpactivityBinding -import dagger.hilt.android.AndroidEntryPoint +import com.yogeshpaliyal.keypass.ui.detail.DeleteConfirmation +import com.yogeshpaliyal.keypass.ui.detail.KeyPassInputField +import com.yogeshpaliyal.keypass.ui.detail.QRScanner +import com.yogeshpaliyal.keypass.ui.redux.actions.Action +import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction +import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction +import org.reduxkotlin.compose.rememberTypedDispatcher -@AndroidEntryPoint -class AddTOTPActivity : AppCompatActivity() { +@Composable +fun TOTPScreen(id: String? = null, viewModel: AddTOTPViewModel = hiltViewModel()) { + val dispatchAction = rememberTypedDispatcher() - companion object { - - private const val ARG_ACCOUNT_ID = "account_id" - - @JvmStatic - fun start(context: Context?, accountId: String? = null) { - val starter = Intent(context, AddTOTPActivity::class.java) - starter.putExtra(ARG_ACCOUNT_ID, accountId) - context?.startActivity(starter) - } - } - - private lateinit var binding: ActivityAddTotpactivityBinding - - private val mViewModel by viewModels() - - private val accountId by lazy { - intent.extras?.getString(ARG_ACCOUNT_ID) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityAddTotpactivityBinding.inflate(layoutInflater) - binding.mViewModel = mViewModel - binding.lifecycleOwner = this - setContentView(binding.root) - - setSupportActionBar(binding.toolbar) - - binding.tilSecretKey.isVisible = accountId == null - mViewModel.loadOldAccount(accountId) - - binding.toolbar.setNavigationOnClickListener { - onBackPressed() - } - - binding.tilSecretKey.setEndIconOnClickListener { - // ScannerActivity.start(this) - IntentIntegrator(this).setPrompt("").initiateScan() - } - - mViewModel.error.observe( - this, - Observer { - it?.getContentIfNotHandled()?.let { - Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show() - } - } + // task value state + val (accountModel, setAccountModel) = remember { + mutableStateOf( + AccountModel() ) + } - mViewModel.goBack.observe( - this, - Observer { - it.getContentIfNotHandled()?.let { - onBackPressed() - } - } - ) - - binding.btnSave.setOnClickListener { - mViewModel.saveAccount(accountId) + val launcher = rememberLauncherForActivityResult(QRScanner()) { + it?.let { + val totp = TOTPHelper(it) + setAccountModel( + accountModel.copy( + password = totp.secret, + title = totp.label, + username = totp.issuer + ) + ) } } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - if (accountId != null) { - menuInflater.inflate(R.menu.menu_delete, menu) - } - return super.onCreateOptionsMenu(menu) + val goBack: () -> Unit = { + dispatchAction(GoBackAction) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.action_delete) { - deleteAccount() + // Set initial object + LaunchedEffect(key1 = Unit) { + viewModel.loadOldAccount(id) { + setAccountModel(it.copy()) } - return super.onOptionsItemSelected(item) } - private fun deleteAccount() { - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.delete_account_title)) - .setMessage(getString(R.string.delete_account_msg)) - .setPositiveButton( - getString(R.string.delete) - ) { dialog, which -> - dialog?.dismiss() - - if (accountId != null) { - mViewModel.deleteAccount(accountId!!) { - onBackPressed() + Scaffold(bottomBar = { + BottomBar( + backPressed = goBack, + showDeleteButton = accountModel.uniqueId != null, + onDeletePressed = { + accountModel.uniqueId?.let { + viewModel.deleteAccount(it) { + goBack() } } } - .setNegativeButton(getString(R.string.cancel)) { dialog, which -> - dialog.dismiss() - }.show() - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) - - if (result != null) { - if (result.contents != null) { - try { - val totp = TOTPHelper(result.contents) - totp.secret?.let { - mViewModel.setSecretKey(it) - } - mViewModel.setAccountName(totp.label) - } catch (e: Exception) { - e.printStackTrace() - } + ) { + if (accountModel.password.isNullOrEmpty()) { + dispatchAction(ToastAction(R.string.alert_black_secret_key)) + return@BottomBar + } + + if (accountModel.title.isNullOrEmpty()) { + dispatchAction(ToastAction(R.string.alert_black_account_name)) + return@BottomBar + } + + viewModel.saveAccount(accountModel, goBack) + } + }) { paddingValues -> + Surface(modifier = Modifier.padding(paddingValues)) { + Fields(accountModel = accountModel, updateAccountModel = { newAccountModel -> + setAccountModel(newAccountModel) + }) { + launcher.launch(null) } - } else { - super.onActivityResult(requestCode, resultCode, data) } } } + +@Composable +fun Fields( + modifier: Modifier = Modifier, + accountModel: AccountModel, + updateAccountModel: (newAccountModel: AccountModel) -> Unit, + scanClicked: () -> Unit +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (accountModel.uniqueId == null) { + KeyPassInputField( + modifier = Modifier.testTag("secretKey"), + placeholder = R.string.secret_key, + value = accountModel.password, + setValue = { + updateAccountModel(accountModel.copy(password = it)) + }, + trailingIcon = { + IconButton(onClick = { + scanClicked() + }) { + Icon( + painter = painterResource(id = R.drawable.ic_twotone_qr_code_scanner_24), + contentDescription = "" + ) + } + } + ) + } + + KeyPassInputField( + modifier = Modifier.testTag("accountName"), + placeholder = R.string.account_name, + value = accountModel.title, + setValue = { + updateAccountModel(accountModel.copy(title = it)) + } + ) + } +} + +@Composable +fun BottomBar( + backPressed: () -> Unit, + showDeleteButton: Boolean, + onDeletePressed: () -> Unit, + onSaveClicked: () -> Unit +) { + val (openDialog, setOpenDialog) = remember { mutableStateOf(false) } + + BottomAppBar(actions = { + IconButton(onClick = backPressed) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.ArrowBackIosNew), + contentDescription = "Go Back", + tint = MaterialTheme.colorScheme.onSurface + ) + } + if (showDeleteButton) { + IconButton(onClick = { + setOpenDialog(true) + }) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.Delete), + contentDescription = "Delete", + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + }, floatingActionButton = { + FloatingActionButton(modifier = Modifier.testTag("save"), onClick = onSaveClicked) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.Done), + contentDescription = "Save Changes" + ) + } + }) + + DeleteConfirmation( + openDialog, + updateDialogVisibility = { + setOpenDialog(it) + }, + onDeletePressed + ) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt index b2177311..02235968 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt @@ -1,14 +1,11 @@ package com.yogeshpaliyal.keypass.ui.addTOTP -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.yogeshpaliyal.common.AppDatabase import com.yogeshpaliyal.common.constants.AccountType import com.yogeshpaliyal.common.data.AccountModel -import com.yogeshpaliyal.common.utils.Event -import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.common.utils.getRandomString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -18,64 +15,32 @@ import javax.inject.Inject class AddTOTPViewModel @Inject constructor(private val appDatabase: AppDatabase) : ViewModel() { - private val _goBack = MutableLiveData>() - val goBack: LiveData> = _goBack - - private val _error = MutableLiveData>() - val error: LiveData> = _error - - val secretKey = MutableLiveData("") - - val accountName = MutableLiveData("") - - fun loadOldAccount(accountId: String?) { + fun loadOldAccount(accountId: String?, loadAccount: (accountModel: AccountModel) -> Unit) { accountId ?: return viewModelScope.launch(Dispatchers.IO) { appDatabase.getDao().getAccount(accountId)?.let { accountModel -> - accountName.postValue(accountModel.title ?: "") + loadAccount(accountModel) } } } - fun saveAccount(accountId: String?) { + fun saveAccount(accountModel: AccountModel, onComplete: () -> Unit) { viewModelScope.launch { - val secretKey = secretKey.value - val accountName = accountName.value - - if (accountId == null) { - if (secretKey.isNullOrEmpty()) { - _error.postValue(Event(R.string.alert_black_secret_key)) - return@launch - } - } - - if (accountName.isNullOrEmpty()) { - _error.postValue(Event(R.string.alert_black_account_name)) - return@launch - } - - val accountModel = if (accountId == null) { - AccountModel(password = secretKey, title = accountName, type = AccountType.TOTP) + val accountModelDb = if (accountModel.uniqueId == null) { + AccountModel(uniqueId = getRandomString(), password = accountModel.password, title = accountModel.title, type = AccountType.TOTP) } else { - appDatabase.getDao().getAccount(accountId)?.also { - it.title = accountName + appDatabase.getDao().getAccount(accountModel.uniqueId)?.also { + it.title = accountModel.title + it.password = accountModel.password } } - accountModel?.let { appDatabase.getDao().insertOrUpdateAccount(it) } - _goBack.postValue(Event(Unit)) + accountModelDb?.let { appDatabase.getDao().insertOrUpdateAccount(it) } + onComplete() } } - fun setSecretKey(secretKey: String) { - this.secretKey.value = secretKey - } - - fun setAccountName(accountName: String) { - this.accountName.value = accountName - } - fun deleteAccount(accountId: String, onDeleted: () -> Unit) { viewModelScope.launch { appDatabase.getDao().deleteAccount(accountId) diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt index 08d401d2..dfff57ee 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt @@ -51,9 +51,9 @@ import com.yogeshpaliyal.common.constants.AccountType import com.yogeshpaliyal.common.data.AccountModel import com.yogeshpaliyal.keypass.R import com.yogeshpaliyal.keypass.ui.redux.actions.CopyToClipboard -import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction import com.yogeshpaliyal.keypass.ui.redux.states.AccountDetailState +import com.yogeshpaliyal.keypass.ui.redux.states.TotpDetailState import kotlinx.coroutines.delay import org.reduxkotlin.compose.rememberDispatcher import kotlin.time.Duration.Companion.seconds @@ -77,7 +77,7 @@ fun AccountsList(accounts: List? = null) { account, onClick = { if (it.type == AccountType.TOTP) { - dispatch(IntentNavigation.AddTOTP(it.uniqueId)) + dispatch(NavigationAction(TotpDetailState(it.uniqueId))) } else { dispatch(NavigationAction(AccountDetailState(it.id))) } diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt index ea8cf817..e290b8b3 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import com.yogeshpaliyal.keypass.BuildConfig +import com.yogeshpaliyal.keypass.ui.addTOTP.TOTPScreen import com.yogeshpaliyal.keypass.ui.auth.AuthScreen import com.yogeshpaliyal.keypass.ui.backup.BackupScreen import com.yogeshpaliyal.keypass.ui.changeDefaultPasswordLength.ChangeDefaultPasswordLengthScreen @@ -171,6 +172,7 @@ fun CurrentPage() { } is TotpDetailState -> { + TOTPScreen(it.accountId) } } } diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt index 6d7c3b01..8126462d 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt @@ -4,6 +4,7 @@ import com.yogeshpaliyal.keypass.R import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction import com.yogeshpaliyal.keypass.ui.redux.states.HomeState +import com.yogeshpaliyal.keypass.ui.redux.states.TotpDetailState object NavigationModel { @@ -31,7 +32,7 @@ object NavigationModel { icon = R.drawable.ic_twotone_totp, titleRes = R.string.add_totp, checked = false, - action = IntentNavigation.AddTOTP() + action = NavigationAction(TotpDetailState()) ) ) } diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt index 4d30a674..42a65037 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt @@ -4,5 +4,4 @@ sealed interface IntentNavigation : Action { object GeneratePassword : IntentNavigation object BackupActivity : IntentNavigation object ShareApp : IntentNavigation - data class AddTOTP(val accountId: String? = null) : IntentNavigation } diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt index c72c5b3e..99530576 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt @@ -3,7 +3,6 @@ package com.yogeshpaliyal.keypass.ui.redux.middlewares import android.content.Intent import com.yogeshpaliyal.keypass.BuildConfig import com.yogeshpaliyal.keypass.R -import com.yogeshpaliyal.keypass.ui.addTOTP.AddTOTPActivity import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordActivity import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState @@ -18,10 +17,6 @@ val intentNavigationMiddleware = middleware { store, next, action state.context?.startActivity(intent) } - is IntentNavigation.AddTOTP -> { - AddTOTPActivity.start(state.context, action.accountId) - } - is IntentNavigation.BackupActivity -> { // BackupActivity.start(state.context) } diff --git a/app/src/main/res/layout/activity_add_totpactivity.xml b/app/src/main/res/layout/activity_add_totpactivity.xml deleted file mode 100644 index 26e8923b..00000000 --- a/app/src/main/res/layout/activity_add_totpactivity.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b893afd..1b8cd5f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -107,5 +107,6 @@ Authentication Error %s Change password length Default password length + Secret Key diff --git a/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt b/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt index a959a95b..fb82ec0d 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt @@ -7,7 +7,6 @@ import androidx.room.PrimaryKey import com.google.gson.annotations.SerializedName import com.yogeshpaliyal.common.constants.AccountType import com.yogeshpaliyal.common.utils.TOTPHelper -import com.yogeshpaliyal.common.utils.getRandomString /* * @author Yogesh Paliyal @@ -29,7 +28,7 @@ data class AccountModel( @ColumnInfo(name = "unique_id") @SerializedName("unique_id") - var uniqueId: String? = getRandomString(), + var uniqueId: String? = null, @ColumnInfo(name = "username") @SerializedName("username")