From 774fda83ac276a635e3402034b1eedbd10be916f Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Tue, 9 Mar 2021 14:53:11 +0530 Subject: all: reformat with ktfmt Signed-off-by: Harsh Shandilya --- .../msfjarvis/aps/ui/adapters/FieldItemAdapter.kt | 112 +- .../aps/ui/adapters/PasswordItemRecyclerAdapter.kt | 101 +- .../aps/ui/autofill/AutofillDecryptActivity.kt | 319 +++--- .../aps/ui/autofill/AutofillFilterActivity.kt | 310 +++--- .../autofill/AutofillPublisherChangedActivity.kt | 135 ++- .../aps/ui/autofill/AutofillSaveActivity.kt | 197 ++-- .../aps/ui/autofill/PasswordViewHolder.kt | 4 +- .../dev/msfjarvis/aps/ui/crypto/BasePgpActivity.kt | 442 ++++---- .../dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt | 354 +++--- .../msfjarvis/aps/ui/crypto/GetKeyIdsActivity.kt | 86 +- .../aps/ui/crypto/PasswordCreationActivity.kt | 813 +++++++------- .../msfjarvis/aps/ui/dialogs/BasicBottomSheet.kt | 228 ++-- .../aps/ui/dialogs/FolderCreationDialogFragment.kt | 129 +-- .../aps/ui/dialogs/ItemCreationBottomSheet.kt | 85 +- .../aps/ui/dialogs/OtpImportDialogFragment.kt | 50 +- .../ui/dialogs/PasswordGeneratorDialogFragment.kt | 120 +- .../dialogs/XkPasswordGeneratorDialogFragment.kt | 183 ++-- .../aps/ui/folderselect/SelectFolderActivity.kt | 65 +- .../aps/ui/folderselect/SelectFolderFragment.kt | 77 +- .../msfjarvis/aps/ui/git/base/BaseGitActivity.kt | 222 ++-- .../aps/ui/git/config/GitConfigActivity.kt | 207 ++-- .../aps/ui/git/config/GitServerConfigActivity.kt | 401 ++++--- .../dev/msfjarvis/aps/ui/git/log/GitLogActivity.kt | 42 +- .../dev/msfjarvis/aps/ui/git/log/GitLogAdapter.kt | 51 +- .../dev/msfjarvis/aps/ui/main/LaunchActivity.kt | 72 +- .../ui/onboarding/activity/OnboardingActivity.kt | 20 +- .../aps/ui/onboarding/fragments/CloneFragment.kt | 47 +- .../onboarding/fragments/KeySelectionFragment.kt | 50 +- .../onboarding/fragments/RepoLocationFragment.kt | 268 +++-- .../aps/ui/onboarding/fragments/WelcomeFragment.kt | 12 +- .../msfjarvis/aps/ui/passwords/PasswordFragment.kt | 518 +++++---- .../msfjarvis/aps/ui/passwords/PasswordStore.kt | 1159 ++++++++++---------- .../aps/ui/proxy/ProxySelectorActivity.kt | 68 +- .../msfjarvis/aps/ui/settings/AutofillSettings.kt | 164 +-- .../aps/ui/settings/DirectorySelectionActivity.kt | 63 +- .../msfjarvis/aps/ui/settings/GeneralSettings.kt | 140 +-- .../dev/msfjarvis/aps/ui/settings/MiscSettings.kt | 87 +- .../msfjarvis/aps/ui/settings/PasswordSettings.kt | 153 +-- .../aps/ui/settings/RepositorySettings.kt | 309 +++--- .../msfjarvis/aps/ui/settings/SettingsActivity.kt | 130 +-- .../msfjarvis/aps/ui/settings/SettingsProvider.kt | 10 +- .../aps/ui/sshkeygen/ShowSshKeyFragment.kt | 39 +- .../aps/ui/sshkeygen/SshKeyGenActivity.kt | 215 ++-- .../aps/ui/sshkeygen/SshKeyImportActivity.kt | 70 +- .../dev/msfjarvis/aps/ui/util/OnOffItemAnimator.kt | 100 +- 45 files changed, 4133 insertions(+), 4294 deletions(-) (limited to 'app/src/main/java/dev/msfjarvis/aps/ui') diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/adapters/FieldItemAdapter.kt b/app/src/main/java/dev/msfjarvis/aps/ui/adapters/FieldItemAdapter.kt index 0822fdd3..afb2a130 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/adapters/FieldItemAdapter.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/adapters/FieldItemAdapter.kt @@ -17,74 +17,74 @@ import dev.msfjarvis.aps.data.password.FieldItem import dev.msfjarvis.aps.databinding.ItemFieldBinding class FieldItemAdapter( - private var fieldItemList: List, - private val showPassword: Boolean, - private val copyTextToClipBoard: (text: String?) -> Unit, + private var fieldItemList: List, + private val showPassword: Boolean, + private val copyTextToClipBoard: (text: String?) -> Unit, ) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldItemViewHolder { - val binding = ItemFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return FieldItemViewHolder(binding.root, binding) - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldItemViewHolder { + val binding = ItemFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return FieldItemViewHolder(binding.root, binding) + } - override fun onBindViewHolder(holder: FieldItemViewHolder, position: Int) { - holder.bind(fieldItemList[position], showPassword, copyTextToClipBoard) - } + override fun onBindViewHolder(holder: FieldItemViewHolder, position: Int) { + holder.bind(fieldItemList[position], showPassword, copyTextToClipBoard) + } - override fun getItemCount(): Int { - return fieldItemList.size - } - - fun updateOTPCode(code: String) { - var otpItemPosition = -1; - fieldItemList = fieldItemList.mapIndexed { position, item -> - if (item.key.equals(FieldItem.ItemType.OTP.type, true)) { - otpItemPosition = position - return@mapIndexed FieldItem.createOtpField(code) - } + override fun getItemCount(): Int { + return fieldItemList.size + } - return@mapIndexed item + fun updateOTPCode(code: String) { + var otpItemPosition = -1 + fieldItemList = + fieldItemList.mapIndexed { position, item -> + if (item.key.equals(FieldItem.ItemType.OTP.type, true)) { + otpItemPosition = position + return@mapIndexed FieldItem.createOtpField(code) } - notifyItemChanged(otpItemPosition) - } + return@mapIndexed item + } - fun updateItems(itemList: List) { - fieldItemList = itemList - notifyDataSetChanged() - } + notifyItemChanged(otpItemPosition) + } + + fun updateItems(itemList: List) { + fieldItemList = itemList + notifyDataSetChanged() + } - class FieldItemViewHolder(itemView: View, val binding: ItemFieldBinding) : - RecyclerView.ViewHolder(itemView) { + class FieldItemViewHolder(itemView: View, val binding: ItemFieldBinding) : RecyclerView.ViewHolder(itemView) { - fun bind(fieldItem: FieldItem, showPassword: Boolean, copyTextToClipBoard: (String?) -> Unit) { - with(binding) { - itemText.hint = fieldItem.key - itemTextContainer.hint = fieldItem.key - itemText.setText(fieldItem.value) + fun bind(fieldItem: FieldItem, showPassword: Boolean, copyTextToClipBoard: (String?) -> Unit) { + with(binding) { + itemText.hint = fieldItem.key + itemTextContainer.hint = fieldItem.key + itemText.setText(fieldItem.value) - when (fieldItem.action) { - FieldItem.ActionType.COPY -> { - itemTextContainer.apply { - endIconDrawable = ContextCompat.getDrawable(itemView.context, R.drawable.ic_content_copy) - endIconMode = TextInputLayout.END_ICON_CUSTOM - setEndIconOnClickListener { copyTextToClipBoard(itemText.text.toString()) } - } - } - FieldItem.ActionType.HIDE -> { - itemTextContainer.apply { - endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE - setOnClickListener { copyTextToClipBoard(itemText.text.toString()) } - } - itemText.apply { - if (!showPassword) { - transformationMethod = PasswordTransformationMethod.getInstance() - } - setOnClickListener { copyTextToClipBoard(itemText.text.toString()) } - } - } - } + when (fieldItem.action) { + FieldItem.ActionType.COPY -> { + itemTextContainer.apply { + endIconDrawable = ContextCompat.getDrawable(itemView.context, R.drawable.ic_content_copy) + endIconMode = TextInputLayout.END_ICON_CUSTOM + setEndIconOnClickListener { copyTextToClipBoard(itemText.text.toString()) } + } + } + FieldItem.ActionType.HIDE -> { + itemTextContainer.apply { + endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE + setOnClickListener { copyTextToClipBoard(itemText.text.toString()) } + } + itemText.apply { + if (!showPassword) { + transformationMethod = PasswordTransformationMethod.getInstance() + } + setOnClickListener { copyTextToClipBoard(itemText.text.toString()) } } + } } + } } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/adapters/PasswordItemRecyclerAdapter.kt b/app/src/main/java/dev/msfjarvis/aps/ui/adapters/PasswordItemRecyclerAdapter.kt index b68059ad..fc3b1a7e 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/adapters/PasswordItemRecyclerAdapter.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/adapters/PasswordItemRecyclerAdapter.kt @@ -19,65 +19,66 @@ import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryAdapter import dev.msfjarvis.aps.util.viewmodel.stableId open class PasswordItemRecyclerAdapter : - SearchableRepositoryAdapter( - R.layout.password_row_layout, - ::PasswordItemViewHolder, - PasswordItemViewHolder::bind - ) { + SearchableRepositoryAdapter( + R.layout.password_row_layout, + ::PasswordItemViewHolder, + PasswordItemViewHolder::bind + ) { - fun makeSelectable(recyclerView: RecyclerView) { - makeSelectable(recyclerView, ::PasswordItemDetailsLookup) - } + fun makeSelectable(recyclerView: RecyclerView) { + makeSelectable(recyclerView, ::PasswordItemDetailsLookup) + } - override fun onItemClicked(listener: (holder: PasswordItemViewHolder, item: PasswordItem) -> Unit): PasswordItemRecyclerAdapter { - return super.onItemClicked(listener) as PasswordItemRecyclerAdapter - } + override fun onItemClicked( + listener: (holder: PasswordItemViewHolder, item: PasswordItem) -> Unit + ): PasswordItemRecyclerAdapter { + return super.onItemClicked(listener) as PasswordItemRecyclerAdapter + } - override fun onSelectionChanged(listener: (selection: Selection) -> Unit): PasswordItemRecyclerAdapter { - return super.onSelectionChanged(listener) as PasswordItemRecyclerAdapter - } + override fun onSelectionChanged(listener: (selection: Selection) -> Unit): PasswordItemRecyclerAdapter { + return super.onSelectionChanged(listener) as PasswordItemRecyclerAdapter + } - class PasswordItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { + class PasswordItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { - private val name: AppCompatTextView = itemView.findViewById(R.id.label) - private val childCount: AppCompatTextView = itemView.findViewById(R.id.child_count) - private val folderIndicator: AppCompatImageView = - itemView.findViewById(R.id.folder_indicator) - lateinit var itemDetails: ItemDetailsLookup.ItemDetails + private val name: AppCompatTextView = itemView.findViewById(R.id.label) + private val childCount: AppCompatTextView = itemView.findViewById(R.id.child_count) + private val folderIndicator: AppCompatImageView = itemView.findViewById(R.id.folder_indicator) + lateinit var itemDetails: ItemDetailsLookup.ItemDetails - fun bind(item: PasswordItem) { - val parentPath = item.fullPathToParent.replace("(^/)|(/$)".toRegex(), "") - val source = if (parentPath.isNotEmpty()) { - "$parentPath\n$item" - } else { - "$item" - } - val spannable = SpannableString(source) - spannable.setSpan(RelativeSizeSpan(0.7f), 0, parentPath.length, 0) - name.text = spannable - if (item.type == PasswordItem.TYPE_CATEGORY) { - folderIndicator.visibility = View.VISIBLE - val count = item.file.listFiles { path -> path.isDirectory || path.extension == "gpg" }?.size - ?: 0 - childCount.visibility = if (count > 0) View.VISIBLE else View.GONE - childCount.text = "$count" - } else { - childCount.visibility = View.GONE - folderIndicator.visibility = View.GONE - } - itemDetails = object : ItemDetailsLookup.ItemDetails() { - override fun getPosition() = absoluteAdapterPosition - override fun getSelectionKey() = item.stableId - } + fun bind(item: PasswordItem) { + val parentPath = item.fullPathToParent.replace("(^/)|(/$)".toRegex(), "") + val source = + if (parentPath.isNotEmpty()) { + "$parentPath\n$item" + } else { + "$item" + } + val spannable = SpannableString(source) + spannable.setSpan(RelativeSizeSpan(0.7f), 0, parentPath.length, 0) + name.text = spannable + if (item.type == PasswordItem.TYPE_CATEGORY) { + folderIndicator.visibility = View.VISIBLE + val count = item.file.listFiles { path -> path.isDirectory || path.extension == "gpg" }?.size ?: 0 + childCount.visibility = if (count > 0) View.VISIBLE else View.GONE + childCount.text = "$count" + } else { + childCount.visibility = View.GONE + folderIndicator.visibility = View.GONE + } + itemDetails = + object : ItemDetailsLookup.ItemDetails() { + override fun getPosition() = absoluteAdapterPosition + override fun getSelectionKey() = item.stableId } } + } - class PasswordItemDetailsLookup(private val recyclerView: RecyclerView) : - ItemDetailsLookup() { + class PasswordItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup() { - override fun getItemDetails(event: MotionEvent): ItemDetails? { - val view = recyclerView.findChildViewUnder(event.x, event.y) ?: return null - return (recyclerView.getChildViewHolder(view) as PasswordItemViewHolder).itemDetails - } + override fun getItemDetails(event: MotionEvent): ItemDetails? { + val view = recyclerView.findChildViewUnder(event.x, event.y) ?: return null + return (recyclerView.getChildViewHolder(view) as PasswordItemViewHolder).itemDetails } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt index 94274c37..ab1f402d 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillDecryptActivity.kt @@ -51,195 +51,184 @@ import org.openintents.openpgp.OpenPgpError @RequiresApi(Build.VERSION_CODES.O) class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope { - companion object { + companion object { - private const val EXTRA_FILE_PATH = "dev.msfjarvis.aps.autofill.oreo.EXTRA_FILE_PATH" - private const val EXTRA_SEARCH_ACTION = - "dev.msfjarvis.aps.autofill.oreo.EXTRA_SEARCH_ACTION" + private const val EXTRA_FILE_PATH = "dev.msfjarvis.aps.autofill.oreo.EXTRA_FILE_PATH" + private const val EXTRA_SEARCH_ACTION = "dev.msfjarvis.aps.autofill.oreo.EXTRA_SEARCH_ACTION" - private var decryptFileRequestCode = 1 + private var decryptFileRequestCode = 1 - fun makeDecryptFileIntent(file: File, forwardedExtras: Bundle, context: Context): Intent { - return Intent(context, AutofillDecryptActivity::class.java).apply { - putExtras(forwardedExtras) - putExtra(EXTRA_SEARCH_ACTION, true) - putExtra(EXTRA_FILE_PATH, file.absolutePath) - } - } + fun makeDecryptFileIntent(file: File, forwardedExtras: Bundle, context: Context): Intent { + return Intent(context, AutofillDecryptActivity::class.java).apply { + putExtras(forwardedExtras) + putExtra(EXTRA_SEARCH_ACTION, true) + putExtra(EXTRA_FILE_PATH, file.absolutePath) + } + } - fun makeDecryptFileIntentSender(file: File, context: Context): IntentSender { - val intent = Intent(context, AutofillDecryptActivity::class.java).apply { - putExtra(EXTRA_SEARCH_ACTION, false) - putExtra(EXTRA_FILE_PATH, file.absolutePath) - } - return PendingIntent.getActivity( - context, - decryptFileRequestCode++, - intent, - PendingIntent.FLAG_CANCEL_CURRENT - ).intentSender + fun makeDecryptFileIntentSender(file: File, context: Context): IntentSender { + val intent = + Intent(context, AutofillDecryptActivity::class.java).apply { + putExtra(EXTRA_SEARCH_ACTION, false) + putExtra(EXTRA_FILE_PATH, file.absolutePath) } + return PendingIntent.getActivity(context, decryptFileRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT) + .intentSender } + } - private val decryptInteractionRequiredAction = registerForActivityResult(StartIntentSenderForResult()) { result -> - if (continueAfterUserInteraction != null) { - val data = result.data - if (result.resultCode == RESULT_OK && data != null) { - continueAfterUserInteraction?.resume(data) - } else { - continueAfterUserInteraction?.resumeWithException(Exception("OpenPgpApi ACTION_DECRYPT_VERIFY failed to continue after user interaction")) - } - continueAfterUserInteraction = null + private val decryptInteractionRequiredAction = + registerForActivityResult(StartIntentSenderForResult()) { result -> + if (continueAfterUserInteraction != null) { + val data = result.data + if (result.resultCode == RESULT_OK && data != null) { + continueAfterUserInteraction?.resume(data) + } else { + continueAfterUserInteraction?.resumeWithException( + Exception("OpenPgpApi ACTION_DECRYPT_VERIFY failed to continue after user interaction") + ) } + continueAfterUserInteraction = null + } } - private var continueAfterUserInteraction: Continuation? = null - private lateinit var directoryStructure: DirectoryStructure + private var continueAfterUserInteraction: Continuation? = null + private lateinit var directoryStructure: DirectoryStructure - override val coroutineContext - get() = Dispatchers.IO + SupervisorJob() + override val coroutineContext + get() = Dispatchers.IO + SupervisorJob() - override fun onStart() { - super.onStart() - val filePath = intent?.getStringExtra(EXTRA_FILE_PATH) ?: run { - e { "AutofillDecryptActivity started without EXTRA_FILE_PATH" } - finish() - return + override fun onStart() { + super.onStart() + val filePath = + intent?.getStringExtra(EXTRA_FILE_PATH) + ?: run { + e { "AutofillDecryptActivity started without EXTRA_FILE_PATH" } + finish() + return } - val clientState = intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) ?: run { - e { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" } - finish() - return + val clientState = + intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) + ?: run { + e { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" } + finish() + return } - val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!! - val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match - directoryStructure = AutofillPreferences.directoryStructure(this) - d { action.toString() } - launch { - val credentials = decryptCredential(File(filePath)) - if (credentials == null) { - setResult(RESULT_CANCELED) - } else { - val fillInDataset = - AutofillResponseBuilder.makeFillInDataset( - this@AutofillDecryptActivity, - credentials, - clientState, - action - ) - withContext(Dispatchers.Main) { - setResult(RESULT_OK, Intent().apply { - putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) - }) - } - } - withContext(Dispatchers.Main) { - finish() - } + val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!! + val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match + directoryStructure = AutofillPreferences.directoryStructure(this) + d { action.toString() } + launch { + val credentials = decryptCredential(File(filePath)) + if (credentials == null) { + setResult(RESULT_CANCELED) + } else { + val fillInDataset = + AutofillResponseBuilder.makeFillInDataset(this@AutofillDecryptActivity, credentials, clientState, action) + withContext(Dispatchers.Main) { + setResult(RESULT_OK, Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }) } + } + withContext(Dispatchers.Main) { finish() } } + } - override fun onDestroy() { - super.onDestroy() - coroutineContext.cancelChildren() - } + override fun onDestroy() { + super.onDestroy() + coroutineContext.cancelChildren() + } - private suspend fun executeOpenPgpApi( - data: Intent, - input: InputStream, - output: OutputStream - ): Intent? { - var openPgpServiceConnection: OpenPgpServiceConnection? = null - val openPgpService = suspendCoroutine { cont -> - openPgpServiceConnection = OpenPgpServiceConnection( - this, - OPENPGP_PROVIDER, - object : OpenPgpServiceConnection.OnBound { - override fun onBound(service: IOpenPgpService2) { - cont.resume(service) - } + private suspend fun executeOpenPgpApi(data: Intent, input: InputStream, output: OutputStream): Intent? { + var openPgpServiceConnection: OpenPgpServiceConnection? = null + val openPgpService = + suspendCoroutine { cont -> + openPgpServiceConnection = + OpenPgpServiceConnection( + this, + OPENPGP_PROVIDER, + object : OpenPgpServiceConnection.OnBound { + override fun onBound(service: IOpenPgpService2) { + cont.resume(service) + } - override fun onError(e: Exception) { - cont.resumeWithException(e) - } - }).also { it.bindToService() } - } - return OpenPgpApi(this, openPgpService).executeApi(data, input, output).also { - openPgpServiceConnection?.unbindFromService() - } + override fun onError(e: Exception) { + cont.resumeWithException(e) + } + } + ) + .also { it.bindToService() } + } + return OpenPgpApi(this, openPgpService).executeApi(data, input, output).also { + openPgpServiceConnection?.unbindFromService() } + } - private suspend fun decryptCredential( - file: File, - resumeIntent: Intent? = null - ): Credentials? { - val command = resumeIntent ?: Intent().apply { - action = OpenPgpApi.ACTION_DECRYPT_VERIFY - } - runCatching { - file.inputStream() - }.onFailure { e -> - e(e) { "File to decrypt not found" } + private suspend fun decryptCredential(file: File, resumeIntent: Intent? = null): Credentials? { + val command = resumeIntent ?: Intent().apply { action = OpenPgpApi.ACTION_DECRYPT_VERIFY } + runCatching { file.inputStream() } + .onFailure { e -> + e(e) { "File to decrypt not found" } + return null + } + .onSuccess { encryptedInput -> + val decryptedOutput = ByteArrayOutputStream() + runCatching { executeOpenPgpApi(command, encryptedInput, decryptedOutput) } + .onFailure { e -> + e(e) { "OpenPgpApi ACTION_DECRYPT_VERIFY failed" } return null - }.onSuccess { encryptedInput -> - val decryptedOutput = ByteArrayOutputStream() - runCatching { - executeOpenPgpApi(command, encryptedInput, decryptedOutput) - }.onFailure { e -> - e(e) { "OpenPgpApi ACTION_DECRYPT_VERIFY failed" } - return null - }.onSuccess { result -> - return when (val resultCode = - result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - OpenPgpApi.RESULT_CODE_SUCCESS -> { - runCatching { - val entry = withContext(Dispatchers.IO) { - @Suppress("BlockingMethodInNonBlockingContext") - (PasswordEntry(decryptedOutput)) - } - AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure) - }.getOrElse { e -> - e(e) { "Failed to parse password entry" } - return null - } - } - OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { - val pendingIntent: PendingIntent = - result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!! - runCatching { - val intentToResume = withContext(Dispatchers.Main) { - suspendCoroutine { cont -> - continueAfterUserInteraction = cont - decryptInteractionRequiredAction.launch(IntentSenderRequest.Builder(pendingIntent.intentSender).build()) - } - } - decryptCredential(file, intentToResume) - }.getOrElse { e -> - e(e) { "OpenPgpApi ACTION_DECRYPT_VERIFY failed with user interaction" } - return null - } + } + .onSuccess { result -> + return when (val resultCode = result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + OpenPgpApi.RESULT_CODE_SUCCESS -> { + runCatching { + val entry = + withContext(Dispatchers.IO) { + @Suppress("BlockingMethodInNonBlockingContext") (PasswordEntry(decryptedOutput)) } - OpenPgpApi.RESULT_CODE_ERROR -> { - val error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR) - if (error != null) { - withContext(Dispatchers.Main) { - Toast.makeText( - applicationContext, - "Error from OpenKeyChain: ${error.message}", - Toast.LENGTH_LONG - ).show() - } - e { "OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}" } - } - null - } - else -> { - e { "Unrecognized OpenPgpApi result: $resultCode" } - null + AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure) + } + .getOrElse { e -> + e(e) { "Failed to parse password entry" } + return null + } + } + OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { + val pendingIntent: PendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!! + runCatching { + val intentToResume = + withContext(Dispatchers.Main) { + suspendCoroutine { cont -> + continueAfterUserInteraction = cont + decryptInteractionRequiredAction.launch( + IntentSenderRequest.Builder(pendingIntent.intentSender).build() + ) + } } + decryptCredential(file, intentToResume) + } + .getOrElse { e -> + e(e) { "OpenPgpApi ACTION_DECRYPT_VERIFY failed with user interaction" } + return null + } + } + OpenPgpApi.RESULT_CODE_ERROR -> { + val error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR) + if (error != null) { + withContext(Dispatchers.Main) { + Toast.makeText(applicationContext, "Error from OpenKeyChain: ${error.message}", Toast.LENGTH_LONG) + .show() + } + e { "OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}" } } + null + } + else -> { + e { "Unrecognized OpenPgpApi result: $resultCode" } + null + } } - } - return null - } + } + } + return null + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillFilterActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillFilterActivity.kt index ffff8bbd..cf833a19 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillFilterActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillFilterActivity.kt @@ -41,180 +41,164 @@ import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryViewModel @TargetApi(Build.VERSION_CODES.O) class AutofillFilterView : AppCompatActivity() { - companion object { - - private const val HEIGHT_PERCENTAGE = 0.9 - private const val WIDTH_PERCENTAGE = 0.75 - - private const val EXTRA_FORM_ORIGIN_WEB = - "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_WEB" - private const val EXTRA_FORM_ORIGIN_APP = - "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_APP" - private var matchAndDecryptFileRequestCode = 1 - - fun makeMatchAndDecryptFileIntentSender( - context: Context, - formOrigin: FormOrigin - ): IntentSender { - val intent = Intent(context, AutofillFilterView::class.java).apply { - when (formOrigin) { - is FormOrigin.Web -> putExtra(EXTRA_FORM_ORIGIN_WEB, formOrigin.identifier) - is FormOrigin.App -> putExtra(EXTRA_FORM_ORIGIN_APP, formOrigin.identifier) - } - } - return PendingIntent.getActivity( - context, - matchAndDecryptFileRequestCode++, - intent, - PendingIntent.FLAG_CANCEL_CURRENT - ).intentSender + companion object { + + private const val HEIGHT_PERCENTAGE = 0.9 + private const val WIDTH_PERCENTAGE = 0.75 + + private const val EXTRA_FORM_ORIGIN_WEB = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_WEB" + private const val EXTRA_FORM_ORIGIN_APP = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_APP" + private var matchAndDecryptFileRequestCode = 1 + + fun makeMatchAndDecryptFileIntentSender(context: Context, formOrigin: FormOrigin): IntentSender { + val intent = + Intent(context, AutofillFilterView::class.java).apply { + when (formOrigin) { + is FormOrigin.Web -> putExtra(EXTRA_FORM_ORIGIN_WEB, formOrigin.identifier) + is FormOrigin.App -> putExtra(EXTRA_FORM_ORIGIN_APP, formOrigin.identifier) + } } + return PendingIntent.getActivity( + context, + matchAndDecryptFileRequestCode++, + intent, + PendingIntent.FLAG_CANCEL_CURRENT + ) + .intentSender } - - private lateinit var formOrigin: FormOrigin - private lateinit var directoryStructure: DirectoryStructure - private val binding by viewBinding(ActivityOreoAutofillFilterBinding::inflate) - - private val model: SearchableRepositoryViewModel by viewModels { - ViewModelProvider.AndroidViewModelFactory(application) - } - - private val decryptAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - setResult(RESULT_OK, result.data) - } - finish() + } + + private lateinit var formOrigin: FormOrigin + private lateinit var directoryStructure: DirectoryStructure + private val binding by viewBinding(ActivityOreoAutofillFilterBinding::inflate) + + private val model: SearchableRepositoryViewModel by viewModels { + ViewModelProvider.AndroidViewModelFactory(application) + } + + private val decryptAction = + registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + setResult(RESULT_OK, result.data) + } + finish() } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - setFinishOnTouchOutside(true) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + setFinishOnTouchOutside(true) - val params = window.attributes - params.height = (HEIGHT_PERCENTAGE * resources.displayMetrics.heightPixels).toInt() - params.width = (WIDTH_PERCENTAGE * resources.displayMetrics.widthPixels).toInt() - window.attributes = params + val params = window.attributes + params.height = (HEIGHT_PERCENTAGE * resources.displayMetrics.heightPixels).toInt() + params.width = (WIDTH_PERCENTAGE * resources.displayMetrics.widthPixels).toInt() + window.attributes = params - if (intent?.hasExtra(AutofillManager.EXTRA_CLIENT_STATE) != true) { - e { "AutofillFilterActivity started without EXTRA_CLIENT_STATE" } - finish() - return + if (intent?.hasExtra(AutofillManager.EXTRA_CLIENT_STATE) != true) { + e { "AutofillFilterActivity started without EXTRA_CLIENT_STATE" } + finish() + return + } + formOrigin = + when { + intent?.hasExtra(EXTRA_FORM_ORIGIN_WEB) == true -> { + FormOrigin.Web(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_WEB)!!) } - formOrigin = when { - intent?.hasExtra(EXTRA_FORM_ORIGIN_WEB) == true -> { - FormOrigin.Web(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_WEB)!!) - } - intent?.hasExtra(EXTRA_FORM_ORIGIN_APP) == true -> { - FormOrigin.App(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_APP)!!) - } - else -> { - e { "AutofillFilterActivity started without EXTRA_FORM_ORIGIN_WEB or EXTRA_FORM_ORIGIN_APP" } - finish() - return - } + intent?.hasExtra(EXTRA_FORM_ORIGIN_APP) == true -> { + FormOrigin.App(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_APP)!!) } - directoryStructure = AutofillPreferences.directoryStructure(this) - - supportActionBar?.hide() - bindUI() - updateSearch() - setResult(RESULT_CANCELED) - } - - private fun bindUI() { - with(binding) { - rvPassword.apply { - adapter = SearchableRepositoryAdapter( - R.layout.oreo_autofill_filter_row, - ::PasswordViewHolder - ) { item -> - val file = item.file.relativeTo(item.rootDir) - val pathToIdentifier = directoryStructure.getPathToIdentifierFor(file) - val identifier = directoryStructure.getIdentifierFor(file) - val accountPart = directoryStructure.getAccountPartFor(file) - check(identifier != null || accountPart != null) { "At least one of identifier and accountPart should always be non-null" } - title.text = if (identifier != null) { - buildSpannedString { - if (pathToIdentifier != null) - append("$pathToIdentifier/") - bold { underline { append(identifier) } } - } - } else { - accountPart - } - subtitle.apply { - if (identifier != null && accountPart != null) { - text = accountPart - visibility = View.VISIBLE - } else { - visibility = View.GONE - } - } - }.onItemClicked { _, item -> - decryptAndFill(item) - } - layoutManager = LinearLayoutManager(context) - } - search.apply { - val initialSearch = - formOrigin.getPrettyIdentifier(applicationContext, untrusted = false) - setText(initialSearch, TextView.BufferType.EDITABLE) - addTextChangedListener { updateSearch() } - } - origin.text = buildSpannedString { - append(getString(R.string.oreo_autofill_select_and_fill_into)) - append("\n") - bold { - append(formOrigin.getPrettyIdentifier(applicationContext, untrusted = true)) - } - } - strictDomainSearch.apply { - visibility = if (formOrigin is FormOrigin.Web) View.VISIBLE else View.GONE - isChecked = formOrigin is FormOrigin.Web - setOnCheckedChangeListener { _, _ -> updateSearch() } + else -> { + e { "AutofillFilterActivity started without EXTRA_FORM_ORIGIN_WEB or EXTRA_FORM_ORIGIN_APP" } + finish() + return + } + } + directoryStructure = AutofillPreferences.directoryStructure(this) + + supportActionBar?.hide() + bindUI() + updateSearch() + setResult(RESULT_CANCELED) + } + + private fun bindUI() { + with(binding) { + rvPassword.apply { + adapter = + SearchableRepositoryAdapter(R.layout.oreo_autofill_filter_row, ::PasswordViewHolder) { item -> + val file = item.file.relativeTo(item.rootDir) + val pathToIdentifier = directoryStructure.getPathToIdentifierFor(file) + val identifier = directoryStructure.getIdentifierFor(file) + val accountPart = directoryStructure.getAccountPartFor(file) + check(identifier != null || accountPart != null) { + "At least one of identifier and accountPart should always be non-null" } - shouldMatch.text = getString( - R.string.oreo_autofill_match_with, - formOrigin.getPrettyIdentifier(applicationContext) - ) - model.searchResult.observe(this@AutofillFilterView) { result -> - val list = result.passwordItems - (rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) { - rvPassword.scrollToPosition(0) - } - // Switch RecyclerView out for a "no results" message if the new list is empty and - // the message is not yet shown (and vice versa). - if ((list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) || - (list.isNotEmpty() && rvPasswordSwitcher.nextView.id == rvPassword.id) - ) { - rvPasswordSwitcher.showNext() + title.text = + if (identifier != null) { + buildSpannedString { + if (pathToIdentifier != null) append("$pathToIdentifier/") + bold { underline { append(identifier) } } } + } else { + accountPart + } + subtitle.apply { + if (identifier != null && accountPart != null) { + text = accountPart + visibility = View.VISIBLE + } else { + visibility = View.GONE + } } + } + .onItemClicked { _, item -> decryptAndFill(item) } + layoutManager = LinearLayoutManager(context) + } + search.apply { + val initialSearch = formOrigin.getPrettyIdentifier(applicationContext, untrusted = false) + setText(initialSearch, TextView.BufferType.EDITABLE) + addTextChangedListener { updateSearch() } + } + origin.text = + buildSpannedString { + append(getString(R.string.oreo_autofill_select_and_fill_into)) + append("\n") + bold { append(formOrigin.getPrettyIdentifier(applicationContext, untrusted = true)) } } + strictDomainSearch.apply { + visibility = if (formOrigin is FormOrigin.Web) View.VISIBLE else View.GONE + isChecked = formOrigin is FormOrigin.Web + setOnCheckedChangeListener { _, _ -> updateSearch() } + } + shouldMatch.text = + getString(R.string.oreo_autofill_match_with, formOrigin.getPrettyIdentifier(applicationContext)) + model.searchResult.observe(this@AutofillFilterView) { result -> + val list = result.passwordItems + (rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) { rvPassword.scrollToPosition(0) } + // Switch RecyclerView out for a "no results" message if the new list is empty and + // the message is not yet shown (and vice versa). + if ((list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) || + (list.isNotEmpty() && rvPasswordSwitcher.nextView.id == rvPassword.id) + ) { + rvPasswordSwitcher.showNext() + } + } } - - private fun updateSearch() { - model.search( - binding.search.text.toString().trim(), - filterMode = if (binding.strictDomainSearch.isChecked) FilterMode.StrictDomain else FilterMode.Fuzzy, - searchMode = SearchMode.RecursivelyInSubdirectories, - listMode = ListMode.FilesOnly - ) - } - - private fun decryptAndFill(item: PasswordItem) { - if (binding.shouldClear.isChecked) AutofillMatcher.clearMatchesFor(applicationContext, formOrigin) - if (binding.shouldMatch.isChecked) AutofillMatcher.addMatchFor( - applicationContext, - formOrigin, - item.file - ) - // intent?.extras? is checked to be non-null in onCreate - decryptAction.launch(AutofillDecryptActivity.makeDecryptFileIntent( - item.file, - intent!!.extras!!, - this - )) - } + } + + private fun updateSearch() { + model.search( + binding.search.text.toString().trim(), + filterMode = if (binding.strictDomainSearch.isChecked) FilterMode.StrictDomain else FilterMode.Fuzzy, + searchMode = SearchMode.RecursivelyInSubdirectories, + listMode = ListMode.FilesOnly + ) + } + + private fun decryptAndFill(item: PasswordItem) { + if (binding.shouldClear.isChecked) AutofillMatcher.clearMatchesFor(applicationContext, formOrigin) + if (binding.shouldMatch.isChecked) AutofillMatcher.addMatchFor(applicationContext, formOrigin, item.file) + // intent?.extras? is checked to be non-null in onCreate + decryptAction.launch(AutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this)) + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillPublisherChangedActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillPublisherChangedActivity.kt index faf1f1c0..bea2bcd3 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillPublisherChangedActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillPublisherChangedActivity.kt @@ -31,84 +31,83 @@ import dev.msfjarvis.aps.util.extensions.viewBinding @TargetApi(Build.VERSION_CODES.O) class AutofillPublisherChangedActivity : AppCompatActivity() { - companion object { + companion object { - private const val EXTRA_APP_PACKAGE = - "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_APP_PACKAGE" - private const val EXTRA_FILL_RESPONSE_AFTER_RESET = - "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FILL_RESPONSE_AFTER_RESET" - private var publisherChangedRequestCode = 1 + private const val EXTRA_APP_PACKAGE = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_APP_PACKAGE" + private const val EXTRA_FILL_RESPONSE_AFTER_RESET = + "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FILL_RESPONSE_AFTER_RESET" + private var publisherChangedRequestCode = 1 - fun makePublisherChangedIntentSender( - context: Context, - publisherChangedException: AutofillPublisherChangedException, - fillResponseAfterReset: FillResponse?, - ): IntentSender { - val intent = Intent(context, AutofillPublisherChangedActivity::class.java).apply { - putExtra(EXTRA_APP_PACKAGE, publisherChangedException.formOrigin.identifier) - putExtra(EXTRA_FILL_RESPONSE_AFTER_RESET, fillResponseAfterReset) - } - return PendingIntent.getActivity( - context, publisherChangedRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT - ).intentSender + fun makePublisherChangedIntentSender( + context: Context, + publisherChangedException: AutofillPublisherChangedException, + fillResponseAfterReset: FillResponse?, + ): IntentSender { + val intent = + Intent(context, AutofillPublisherChangedActivity::class.java).apply { + putExtra(EXTRA_APP_PACKAGE, publisherChangedException.formOrigin.identifier) + putExtra(EXTRA_FILL_RESPONSE_AFTER_RESET, fillResponseAfterReset) } + return PendingIntent.getActivity( + context, + publisherChangedRequestCode++, + intent, + PendingIntent.FLAG_CANCEL_CURRENT + ) + .intentSender } + } - private lateinit var appPackage: String - private val binding by viewBinding(ActivityOreoAutofillPublisherChangedBinding::inflate) + private lateinit var appPackage: String + private val binding by viewBinding(ActivityOreoAutofillPublisherChangedBinding::inflate) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - setFinishOnTouchOutside(true) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + setFinishOnTouchOutside(true) - appPackage = intent.getStringExtra(EXTRA_APP_PACKAGE) ?: run { - e { "AutofillPublisherChangedActivity started without EXTRA_PACKAGE_NAME" } - finish() - return - } - supportActionBar?.hide() - showPackageInfo() - with(binding) { - okButton.setOnClickListener { finish() } - advancedButton.setOnClickListener { - advancedButton.visibility = View.GONE - warningAppAdvancedInfo.visibility = View.VISIBLE - resetButton.visibility = View.VISIBLE - } - resetButton.setOnClickListener { - AutofillMatcher.clearMatchesFor(this@AutofillPublisherChangedActivity, FormOrigin.App(appPackage)) - val fillResponse = intent.getParcelableExtra(EXTRA_FILL_RESPONSE_AFTER_RESET) - setResult(RESULT_OK, Intent().apply { - putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) - }) - finish() - } + appPackage = + intent.getStringExtra(EXTRA_APP_PACKAGE) + ?: run { + e { "AutofillPublisherChangedActivity started without EXTRA_PACKAGE_NAME" } + finish() + return } + supportActionBar?.hide() + showPackageInfo() + with(binding) { + okButton.setOnClickListener { finish() } + advancedButton.setOnClickListener { + advancedButton.visibility = View.GONE + warningAppAdvancedInfo.visibility = View.VISIBLE + resetButton.visibility = View.VISIBLE + } + resetButton.setOnClickListener { + AutofillMatcher.clearMatchesFor(this@AutofillPublisherChangedActivity, FormOrigin.App(appPackage)) + val fillResponse = intent.getParcelableExtra(EXTRA_FILL_RESPONSE_AFTER_RESET) + setResult(RESULT_OK, Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) }) + finish() + } } + } - private fun showPackageInfo() { - runCatching { - with(binding) { - val packageInfo = - packageManager.getPackageInfo(appPackage, PackageManager.GET_META_DATA) - val installTime = DateUtils.getRelativeTimeSpanString(packageInfo.firstInstallTime) - warningAppInstallDate.text = - getString(R.string.oreo_autofill_warning_publisher_install_time, installTime) - val appInfo = - packageManager.getApplicationInfo(appPackage, PackageManager.GET_META_DATA) - warningAppName.text = "“${packageManager.getApplicationLabel(appInfo)}”" + private fun showPackageInfo() { + runCatching { + with(binding) { + val packageInfo = packageManager.getPackageInfo(appPackage, PackageManager.GET_META_DATA) + val installTime = DateUtils.getRelativeTimeSpanString(packageInfo.firstInstallTime) + warningAppInstallDate.text = getString(R.string.oreo_autofill_warning_publisher_install_time, installTime) + val appInfo = packageManager.getApplicationInfo(appPackage, PackageManager.GET_META_DATA) + warningAppName.text = "“${packageManager.getApplicationLabel(appInfo)}”" - val currentHash = computeCertificatesHash(this@AutofillPublisherChangedActivity, appPackage) - warningAppAdvancedInfo.text = getString( - R.string.oreo_autofill_warning_publisher_advanced_info_template, - appPackage, - currentHash - ) - } - }.onFailure { e -> - e(e) { "Failed to retrieve package info for $appPackage" } - finish() - } + val currentHash = computeCertificatesHash(this@AutofillPublisherChangedActivity, appPackage) + warningAppAdvancedInfo.text = + getString(R.string.oreo_autofill_warning_publisher_advanced_info_template, appPackage, currentHash) + } } + .onFailure { e -> + e(e) { "Failed to retrieve package info for $appPackage" } + finish() + } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillSaveActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillSaveActivity.kt index f307d481..dfec29be 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillSaveActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/AutofillSaveActivity.kt @@ -29,121 +29,106 @@ import java.io.File @RequiresApi(Build.VERSION_CODES.O) class AutofillSaveActivity : AppCompatActivity() { - companion object { + companion object { - private const val EXTRA_FOLDER_NAME = - "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FOLDER_NAME" - private const val EXTRA_PASSWORD = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_PASSWORD" - private const val EXTRA_NAME = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_NAME" - private const val EXTRA_SHOULD_MATCH_APP = - "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_APP" - private const val EXTRA_SHOULD_MATCH_WEB = - "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_WEB" - private const val EXTRA_GENERATE_PASSWORD = - "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_GENERATE_PASSWORD" + private const val EXTRA_FOLDER_NAME = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FOLDER_NAME" + private const val EXTRA_PASSWORD = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_PASSWORD" + private const val EXTRA_NAME = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_NAME" + private const val EXTRA_SHOULD_MATCH_APP = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_APP" + private const val EXTRA_SHOULD_MATCH_WEB = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_WEB" + private const val EXTRA_GENERATE_PASSWORD = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_GENERATE_PASSWORD" - private var saveRequestCode = 1 + private var saveRequestCode = 1 - fun makeSaveIntentSender( - context: Context, - credentials: Credentials?, - formOrigin: FormOrigin - ): IntentSender { - val identifier = formOrigin.getPrettyIdentifier(context, untrusted = false) - // Prevent directory traversals - val sanitizedIdentifier = identifier.replace('\\', '_') - .replace('/', '_') - .trimStart('.') - .takeUnless { it.isBlank() } ?: formOrigin.identifier - val directoryStructure = AutofillPreferences.directoryStructure(context) - val folderName = directoryStructure.getSaveFolderName( - sanitizedIdentifier = sanitizedIdentifier, - username = credentials?.username + fun makeSaveIntentSender(context: Context, credentials: Credentials?, formOrigin: FormOrigin): IntentSender { + val identifier = formOrigin.getPrettyIdentifier(context, untrusted = false) + // Prevent directory traversals + val sanitizedIdentifier = + identifier.replace('\\', '_').replace('/', '_').trimStart('.').takeUnless { it.isBlank() } + ?: formOrigin.identifier + val directoryStructure = AutofillPreferences.directoryStructure(context) + val folderName = + directoryStructure.getSaveFolderName( + sanitizedIdentifier = sanitizedIdentifier, + username = credentials?.username + ) + val fileName = directoryStructure.getSaveFileName(username = credentials?.username, identifier = identifier) + val intent = + Intent(context, AutofillSaveActivity::class.java).apply { + putExtras( + bundleOf( + EXTRA_FOLDER_NAME to folderName, + EXTRA_NAME to fileName, + EXTRA_PASSWORD to credentials?.password, + EXTRA_SHOULD_MATCH_APP to formOrigin.identifier.takeIf { formOrigin is FormOrigin.App }, + EXTRA_SHOULD_MATCH_WEB to formOrigin.identifier.takeIf { formOrigin is FormOrigin.Web }, + EXTRA_GENERATE_PASSWORD to (credentials == null) ) - val fileName = directoryStructure.getSaveFileName(username = credentials?.username, identifier = identifier) - val intent = Intent(context, AutofillSaveActivity::class.java).apply { - putExtras( - bundleOf( - EXTRA_FOLDER_NAME to folderName, - EXTRA_NAME to fileName, - EXTRA_PASSWORD to credentials?.password, - EXTRA_SHOULD_MATCH_APP to formOrigin.identifier.takeIf { formOrigin is FormOrigin.App }, - EXTRA_SHOULD_MATCH_WEB to formOrigin.identifier.takeIf { formOrigin is FormOrigin.Web }, - EXTRA_GENERATE_PASSWORD to (credentials == null) - ) - ) - } - return PendingIntent.getActivity( - context, - saveRequestCode++, - intent, - PendingIntent.FLAG_CANCEL_CURRENT - ).intentSender + ) } + return PendingIntent.getActivity(context, saveRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT) + .intentSender } + } - private val formOrigin by lazy(LazyThreadSafetyMode.NONE) { - val shouldMatchApp: String? = intent.getStringExtra(EXTRA_SHOULD_MATCH_APP) - val shouldMatchWeb: String? = intent.getStringExtra(EXTRA_SHOULD_MATCH_WEB) - if (shouldMatchApp != null && shouldMatchWeb == null) { - FormOrigin.App(shouldMatchApp) - } else if (shouldMatchApp == null && shouldMatchWeb != null) { - FormOrigin.Web(shouldMatchWeb) - } else { - null - } + private val formOrigin by lazy(LazyThreadSafetyMode.NONE) { + val shouldMatchApp: String? = intent.getStringExtra(EXTRA_SHOULD_MATCH_APP) + val shouldMatchWeb: String? = intent.getStringExtra(EXTRA_SHOULD_MATCH_WEB) + if (shouldMatchApp != null && shouldMatchWeb == null) { + FormOrigin.App(shouldMatchApp) + } else if (shouldMatchApp == null && shouldMatchWeb != null) { + FormOrigin.Web(shouldMatchWeb) + } else { + null } + } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val repo = PasswordRepository.getRepositoryDirectory() - val saveIntent = Intent(this, PasswordCreationActivity::class.java).apply { - putExtras( - bundleOf( - "REPO_PATH" to repo.absolutePath, - "FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath, - PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME), - PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD), - PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) - ) - ) - } - registerForActivityResult(StartActivityForResult()) { result -> - val data = result.data - if (result.resultCode == RESULT_OK && data != null) { - val createdPath = data.getStringExtra("CREATED_FILE")!! - formOrigin?.let { - AutofillMatcher.addMatchFor(this, it, File(createdPath)) - } - val password = data.getStringExtra("PASSWORD") - val resultIntent = if (password != null) { - // Password was generated and should be filled into a form. - val username = data.getStringExtra("USERNAME") - val clientState = - intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) ?: run { - e { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" } - finish() - return@registerForActivityResult - } - val credentials = Credentials(username, password, null) - val fillInDataset = AutofillResponseBuilder.makeFillInDataset( - this, - credentials, - clientState, - AutofillAction.Generate - ) - Intent().apply { - putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) - } - } else { - // Password was extracted from a form, there is nothing to fill. - Intent() - } - setResult(RESULT_OK, resultIntent) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val repo = PasswordRepository.getRepositoryDirectory() + val saveIntent = + Intent(this, PasswordCreationActivity::class.java).apply { + putExtras( + bundleOf( + "REPO_PATH" to repo.absolutePath, + "FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath, + PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME), + PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD), + PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) + ) + ) + } + registerForActivityResult(StartActivityForResult()) { result -> + val data = result.data + if (result.resultCode == RESULT_OK && data != null) { + val createdPath = data.getStringExtra("CREATED_FILE")!! + formOrigin?.let { AutofillMatcher.addMatchFor(this, it, File(createdPath)) } + val password = data.getStringExtra("PASSWORD") + val resultIntent = + if (password != null) { + // Password was generated and should be filled into a form. + val username = data.getStringExtra("USERNAME") + val clientState = + intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) + ?: run { + e { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" } + finish() + return@registerForActivityResult + } + val credentials = Credentials(username, password, null) + val fillInDataset = + AutofillResponseBuilder.makeFillInDataset(this, credentials, clientState, AutofillAction.Generate) + Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) } } else { - setResult(RESULT_CANCELED) + // Password was extracted from a form, there is nothing to fill. + Intent() } - finish() - }.launch(saveIntent) - } + setResult(RESULT_OK, resultIntent) + } else { + setResult(RESULT_CANCELED) + } + finish() + } + .launch(saveIntent) + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/PasswordViewHolder.kt b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/PasswordViewHolder.kt index 2b708591..eacd49c3 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/autofill/PasswordViewHolder.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/autofill/PasswordViewHolder.kt @@ -11,6 +11,6 @@ import dev.msfjarvis.aps.R class PasswordViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val title: TextView = itemView.findViewById(R.id.title) - val subtitle: TextView = itemView.findViewById(R.id.subtitle) + val title: TextView = itemView.findViewById(R.id.title) + val subtitle: TextView = itemView.findViewById(R.id.subtitle) } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/BasePgpActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/BasePgpActivity.kt index 5dee98cb..fc03b76b 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/BasePgpActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/BasePgpActivity.kt @@ -42,269 +42,249 @@ import org.openintents.openpgp.OpenPgpError @Suppress("Registered") open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound { - /** - * Full path to the repository - */ - val repoPath by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra("REPO_PATH")!! } + /** Full path to the repository */ + val repoPath by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra("REPO_PATH")!! } - /** - * Full path to the password file being worked on - */ - val fullPath by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra("FILE_PATH")!! } + /** Full path to the password file being worked on */ + val fullPath by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra("FILE_PATH")!! } - /** - * Name of the password file - * - * Converts personal/auth.foo.org/john_doe@example.org.gpg to john_doe.example.org - */ - val name: String by lazy(LazyThreadSafetyMode.NONE) { File(fullPath).nameWithoutExtension } + /** + * Name of the password file + * + * Converts personal/auth.foo.org/john_doe@example.org.gpg to john_doe.example.org + */ + val name: String by lazy(LazyThreadSafetyMode.NONE) { File(fullPath).nameWithoutExtension } - /** - * Get the timestamp for when this file was last modified. - */ - val lastChangedString: CharSequence by lazy(LazyThreadSafetyMode.NONE) { - getLastChangedString( - intent.getLongExtra( - "LAST_CHANGED_TIMESTAMP", - -1L - ) - ) - } + /** Get the timestamp for when this file was last modified. */ + val lastChangedString: CharSequence by lazy(LazyThreadSafetyMode.NONE) { + getLastChangedString(intent.getLongExtra("LAST_CHANGED_TIMESTAMP", -1L)) + } - /** - * [SharedPreferences] instance used by subclasses to persist settings - */ - val settings: SharedPreferences by lazy(LazyThreadSafetyMode.NONE) { sharedPrefs } + /** [SharedPreferences] instance used by subclasses to persist settings */ + val settings: SharedPreferences by lazy(LazyThreadSafetyMode.NONE) { sharedPrefs } - /** - * Handle to the [OpenPgpApi] instance that is used by subclasses to interface with OpenKeychain. - */ - private var serviceConnection: OpenPgpServiceConnection? = null - var api: OpenPgpApi? = null + /** + * Handle to the [OpenPgpApi] instance that is used by subclasses to interface with OpenKeychain. + */ + private var serviceConnection: OpenPgpServiceConnection? = null + var api: OpenPgpApi? = null - /** - * A [OpenPgpServiceConnection.OnBound] instance for the last listener that we wish to bind with - * in case the previous attempt was cancelled due to missing [OPENPGP_PROVIDER] package. - */ - private var previousListener: OpenPgpServiceConnection.OnBound? = null + /** + * A [OpenPgpServiceConnection.OnBound] instance for the last listener that we wish to bind with + * in case the previous attempt was cancelled due to missing [OPENPGP_PROVIDER] package. + */ + private var previousListener: OpenPgpServiceConnection.OnBound? = null - /** - * [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots - * or recent apps screen. - */ - @CallSuper - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) - tag(TAG) - } + /** + * [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots or + * recent apps screen. + */ + @CallSuper + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + tag(TAG) + } - /** - * [onDestroy] handles unbinding from the OpenPgp service linked with [serviceConnection]. This - * is annotated with [CallSuper] because it's critical to unbind the service to ensure we're not - * leaking things. - */ - @CallSuper - override fun onDestroy() { - super.onDestroy() - serviceConnection?.unbindFromService() - previousListener = null - } + /** + * [onDestroy] handles unbinding from the OpenPgp service linked with [serviceConnection]. This is + * annotated with [CallSuper] because it's critical to unbind the service to ensure we're not + * leaking things. + */ + @CallSuper + override fun onDestroy() { + super.onDestroy() + serviceConnection?.unbindFromService() + previousListener = null + } - /** - * [onResume] controls the flow for resumption of a PGP operation that was previously interrupted - * by the [OPENPGP_PROVIDER] package being missing. - */ - override fun onResume() { - super.onResume() - previousListener?.let { bindToOpenKeychain(it) } - } + /** + * [onResume] controls the flow for resumption of a PGP operation that was previously interrupted + * by the [OPENPGP_PROVIDER] package being missing. + */ + override fun onResume() { + super.onResume() + previousListener?.let { bindToOpenKeychain(it) } + } - /** - * Sets up [api] once the service is bound. Downstream consumers must call super this to - * initialize [api] - */ - @CallSuper - override fun onBound(service: IOpenPgpService2) { - api = OpenPgpApi(this, service) - } + /** + * Sets up [api] once the service is bound. Downstream consumers must call super this to + * initialize [api] + */ + @CallSuper + override fun onBound(service: IOpenPgpService2) { + api = OpenPgpApi(this, service) + } - /** - * Mandatory error handling from [OpenPgpServiceConnection.OnBound]. All subclasses must handle - * their own errors, and hence this class simply logs and rethrows. Subclasses Must NOT call super. - */ - override fun onError(e: Exception) { - e(e) { "Callers must handle their own exceptions" } - throw e - } + /** + * Mandatory error handling from [OpenPgpServiceConnection.OnBound]. All subclasses must handle + * their own errors, and hence this class simply logs and rethrows. Subclasses Must NOT call + * super. + */ + override fun onError(e: Exception) { + e(e) { "Callers must handle their own exceptions" } + throw e + } - /** - * Method for subclasses to initiate binding with [OpenPgpServiceConnection]. - */ - fun bindToOpenKeychain(onBoundListener: OpenPgpServiceConnection.OnBound) { - val installed = runCatching { - packageManager.getPackageInfo(OPENPGP_PROVIDER, 0) - true - }.getOr(false) - if (!installed) { - previousListener = onBoundListener - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.openkeychain_not_installed_title)) - .setMessage(getString(R.string.openkeychain_not_installed_message)) - .setPositiveButton(getString(R.string.openkeychain_not_installed_google_play)) { _, _ -> - runCatching { - val intent = Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(getString(R.string.play_deeplink_template, OPENPGP_PROVIDER)) - setPackage("com.android.vending") - } - startActivity(intent) - } - } - .setNeutralButton(getString(R.string.openkeychain_not_installed_fdroid)) { _, _ -> - runCatching { - val intent = Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(getString(R.string.fdroid_deeplink_template, OPENPGP_PROVIDER)) - } - startActivity(intent) - } - } - .setOnCancelListener { finish() } - .show() - return - } else { - previousListener = null - serviceConnection = OpenPgpServiceConnection(this, OPENPGP_PROVIDER, onBoundListener).also { - it.bindToService() - } + /** Method for subclasses to initiate binding with [OpenPgpServiceConnection]. */ + fun bindToOpenKeychain(onBoundListener: OpenPgpServiceConnection.OnBound) { + val installed = + runCatching { + packageManager.getPackageInfo(OPENPGP_PROVIDER, 0) + true } + .getOr(false) + if (!installed) { + previousListener = onBoundListener + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.openkeychain_not_installed_title)) + .setMessage(getString(R.string.openkeychain_not_installed_message)) + .setPositiveButton(getString(R.string.openkeychain_not_installed_google_play)) { _, _ -> + runCatching { + val intent = + Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(getString(R.string.play_deeplink_template, OPENPGP_PROVIDER)) + setPackage("com.android.vending") + } + startActivity(intent) + } + } + .setNeutralButton(getString(R.string.openkeychain_not_installed_fdroid)) { _, _ -> + runCatching { + val intent = + Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(getString(R.string.fdroid_deeplink_template, OPENPGP_PROVIDER)) + } + startActivity(intent) + } + } + .setOnCancelListener { finish() } + .show() + return + } else { + previousListener = null + serviceConnection = OpenPgpServiceConnection(this, OPENPGP_PROVIDER, onBoundListener).also { it.bindToService() } } + } - /** - * Handle the case where OpenKeychain returns that it needs to interact with the user - * - * @param result The intent returned by OpenKeychain - */ - fun getUserInteractionRequestIntent(result: Intent): IntentSender { - i { "RESULT_CODE_USER_INTERACTION_REQUIRED" } - return result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!!.intentSender - } - - /** - * Gets a relative string describing when this shape was last changed - * (e.g. "one hour ago") - */ - private fun getLastChangedString(timeStamp: Long): CharSequence { - if (timeStamp < 0) { - throw RuntimeException() - } + /** + * Handle the case where OpenKeychain returns that it needs to interact with the user + * + * @param result The intent returned by OpenKeychain + */ + fun getUserInteractionRequestIntent(result: Intent): IntentSender { + i { "RESULT_CODE_USER_INTERACTION_REQUIRED" } + return result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!!.intentSender + } - return DateUtils.getRelativeTimeSpanString(this, timeStamp, true) + /** Gets a relative string describing when this shape was last changed (e.g. "one hour ago") */ + private fun getLastChangedString(timeStamp: Long): CharSequence { + if (timeStamp < 0) { + throw RuntimeException() } - /** - * Base handling of OpenKeychain errors based on the error contained in [result]. Subclasses - * can use this when they want to default to sane error handling. - */ - fun handleError(result: Intent) { - val error: OpenPgpError? = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR) - if (error != null) { - when (error.errorId) { - OpenPgpError.NO_OR_WRONG_PASSPHRASE -> { - snackbar(message = getString(R.string.openpgp_error_wrong_passphrase)) - } - OpenPgpError.NO_USER_IDS -> { - snackbar(message = getString(R.string.openpgp_error_no_user_ids)) - } - else -> { - snackbar(message = getString(R.string.openpgp_error_unknown, error.message)) - e { "onError getErrorId: ${error.errorId}" } - e { "onError getMessage: ${error.message}" } - } - } + return DateUtils.getRelativeTimeSpanString(this, timeStamp, true) + } + + /** + * Base handling of OpenKeychain errors based on the error contained in [result]. Subclasses can + * use this when they want to default to sane error handling. + */ + fun handleError(result: Intent) { + val error: OpenPgpError? = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR) + if (error != null) { + when (error.errorId) { + OpenPgpError.NO_OR_WRONG_PASSPHRASE -> { + snackbar(message = getString(R.string.openpgp_error_wrong_passphrase)) + } + OpenPgpError.NO_USER_IDS -> { + snackbar(message = getString(R.string.openpgp_error_no_user_ids)) + } + else -> { + snackbar(message = getString(R.string.openpgp_error_unknown, error.message)) + e { "onError getErrorId: ${error.errorId}" } + e { "onError getMessage: ${error.message}" } } + } } + } - /** - * Copies provided [text] to the clipboard. Shows a [Snackbar] which can be disabled by passing - * [showSnackbar] as false. - */ - fun copyTextToClipboard( - text: String?, - showSnackbar: Boolean = true, - @StringRes snackbarTextRes: Int = R.string.clipboard_copied_text - ) { - val clipboard = clipboard ?: return - val clip = ClipData.newPlainText("pgp_handler_result_pm", text) - clipboard.setPrimaryClip(clip) - if (showSnackbar) { - snackbar(message = resources.getString(snackbarTextRes)) - } + /** + * Copies provided [text] to the clipboard. Shows a [Snackbar] which can be disabled by passing + * [showSnackbar] as false. + */ + fun copyTextToClipboard( + text: String?, + showSnackbar: Boolean = true, + @StringRes snackbarTextRes: Int = R.string.clipboard_copied_text + ) { + val clipboard = clipboard ?: return + val clip = ClipData.newPlainText("pgp_handler_result_pm", text) + clipboard.setPrimaryClip(clip) + if (showSnackbar) { + snackbar(message = resources.getString(snackbarTextRes)) } + } - /** - * Copies a provided [password] string to the clipboard. This wraps [copyTextToClipboard] to - * hide the default [Snackbar] and starts off an instance of [ClipboardService] to provide a - * way of clearing the clipboard. - */ - fun copyPasswordToClipboard(password: String?) { - copyTextToClipboard(password, showSnackbar = false) + /** + * Copies a provided [password] string to the clipboard. This wraps [copyTextToClipboard] to hide + * the default [Snackbar] and starts off an instance of [ClipboardService] to provide a way of + * clearing the clipboard. + */ + fun copyPasswordToClipboard(password: String?) { + copyTextToClipboard(password, showSnackbar = false) - val clearAfter = settings.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() ?: 45 + val clearAfter = settings.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() ?: 45 - if (clearAfter != 0) { - val service = Intent(this, ClipboardService::class.java).apply { - action = ClipboardService.ACTION_START - putExtra(ClipboardService.EXTRA_NOTIFICATION_TIME, clearAfter) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(service) - } else { - startService(service) - } - snackbar(message = resources.getString(R.string.clipboard_password_toast_text, clearAfter)) - } else { - snackbar(message = resources.getString(R.string.clipboard_password_no_clear_toast_text)) + if (clearAfter != 0) { + val service = + Intent(this, ClipboardService::class.java).apply { + action = ClipboardService.ACTION_START + putExtra(ClipboardService.EXTRA_NOTIFICATION_TIME, clearAfter) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(service) + } else { + startService(service) + } + snackbar(message = resources.getString(R.string.clipboard_password_toast_text, clearAfter)) + } else { + snackbar(message = resources.getString(R.string.clipboard_password_no_clear_toast_text)) } + } - companion object { + companion object { - private const val TAG = "APS/BasePgpActivity" - const val KEY_PWGEN_TYPE_CLASSIC = "classic" - const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd" + private const val TAG = "APS/BasePgpActivity" + const val KEY_PWGEN_TYPE_CLASSIC = "classic" + const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd" - /** - * Gets the relative path to the repository - */ - fun getRelativePath(fullPath: String, repositoryPath: String): String = - fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") + /** Gets the relative path to the repository */ + fun getRelativePath(fullPath: String, repositoryPath: String): String = + fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") - /** - * Gets the Parent path, relative to the repository - */ - fun getParentPath(fullPath: String, repositoryPath: String): String { - val relativePath = getRelativePath(fullPath, repositoryPath) - val index = relativePath.lastIndexOf("/") - return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace("/+".toRegex(), "/") - } + /** Gets the Parent path, relative to the repository */ + fun getParentPath(fullPath: String, repositoryPath: String): String { + val relativePath = getRelativePath(fullPath, repositoryPath) + val index = relativePath.lastIndexOf("/") + return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace("/+".toRegex(), "/") + } - /** - * /path/to/store/social/facebook.gpg -> social/facebook - */ - @JvmStatic - fun getLongName(fullPath: String, repositoryPath: String, basename: String): String { - var relativePath = getRelativePath(fullPath, repositoryPath) - return if (relativePath.isNotEmpty() && relativePath != "/") { - // remove preceding '/' - relativePath = relativePath.substring(1) - if (relativePath.endsWith('/')) { - relativePath + basename - } else { - "$relativePath/$basename" - } - } else { - basename - } + /** /path/to/store/social/facebook.gpg -> social/facebook */ + @JvmStatic + fun getLongName(fullPath: String, repositoryPath: String, basename: String): String { + var relativePath = getRelativePath(fullPath, repositoryPath) + return if (relativePath.isNotEmpty() && relativePath != "/") { + // remove preceding '/' + relativePath = relativePath.substring(1) + if (relativePath.endsWith('/')) { + relativePath + basename + } else { + "$relativePath/$basename" } + } else { + basename + } } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt index 2a1af099..9e70bb98 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/DecryptActivity.kt @@ -37,202 +37,196 @@ import org.openintents.openpgp.IOpenPgpService2 class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound { - private val binding by viewBinding(DecryptLayoutBinding::inflate) + private val binding by viewBinding(DecryptLayoutBinding::inflate) - private val relativeParentPath by lazy(LazyThreadSafetyMode.NONE) { getParentPath(fullPath, repoPath) } - private var passwordEntry: PasswordEntry? = null + private val relativeParentPath by lazy(LazyThreadSafetyMode.NONE) { getParentPath(fullPath, repoPath) } + private var passwordEntry: PasswordEntry? = null - private val userInteractionRequiredResult = registerForActivityResult(StartIntentSenderForResult()) { result -> - if (result.data == null) { - setResult(RESULT_CANCELED, null) - finish() - return@registerForActivityResult - } - - when (result.resultCode) { - RESULT_OK -> decryptAndVerify(result.data) - RESULT_CANCELED -> { - setResult(RESULT_CANCELED, result.data) - finish() - } + private val userInteractionRequiredResult = + registerForActivityResult(StartIntentSenderForResult()) { result -> + if (result.data == null) { + setResult(RESULT_CANCELED, null) + finish() + return@registerForActivityResult + } + + when (result.resultCode) { + RESULT_OK -> decryptAndVerify(result.data) + RESULT_CANCELED -> { + setResult(RESULT_CANCELED, result.data) + finish() } + } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - bindToOpenKeychain(this) - title = name - with(binding) { - setContentView(root) - passwordCategory.text = relativeParentPath - passwordFile.text = name - passwordFile.setOnLongClickListener { - copyTextToClipboard(name) - true - } - passwordLastChanged.run { - runCatching { - text = resources.getString(R.string.last_changed, lastChangedString) - }.onFailure { - visibility = View.GONE - } - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + bindToOpenKeychain(this) + title = name + with(binding) { + setContentView(root) + passwordCategory.text = relativeParentPath + passwordFile.text = name + passwordFile.setOnLongClickListener { + copyTextToClipboard(name) + true + } + passwordLastChanged.run { + runCatching { text = resources.getString(R.string.last_changed, lastChangedString) }.onFailure { + visibility = View.GONE } + } } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(R.menu.pgp_handler, menu) - passwordEntry?.let { entry -> - if (menu != null) { - menu.findItem(R.id.edit_password).isVisible = true - if (entry.password.isNotEmpty()) { - menu.findItem(R.id.share_password_as_plaintext).isVisible = true - menu.findItem(R.id.copy_password).isVisible = true - } - } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.pgp_handler, menu) + passwordEntry?.let { entry -> + if (menu != null) { + menu.findItem(R.id.edit_password).isVisible = true + if (entry.password.isNotEmpty()) { + menu.findItem(R.id.share_password_as_plaintext).isVisible = true + menu.findItem(R.id.copy_password).isVisible = true } - return true + } } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> onBackPressed() - R.id.edit_password -> editPassword() - R.id.share_password_as_plaintext -> shareAsPlaintext() - R.id.copy_password -> copyPasswordToClipboard(passwordEntry?.password) - else -> return super.onOptionsItemSelected(item) - } - return true + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> onBackPressed() + R.id.edit_password -> editPassword() + R.id.share_password_as_plaintext -> shareAsPlaintext() + R.id.copy_password -> copyPasswordToClipboard(passwordEntry?.password) + else -> return super.onOptionsItemSelected(item) } - - override fun onBound(service: IOpenPgpService2) { - super.onBound(service) - decryptAndVerify() + return true + } + + override fun onBound(service: IOpenPgpService2) { + super.onBound(service) + decryptAndVerify() + } + + override fun onError(e: Exception) { + e(e) + } + + /** + * Automatically finishes the activity 60 seconds after decryption succeeded to prevent + * information leaks from stale activities. + */ + @OptIn(ExperimentalTime::class) + private fun startAutoDismissTimer() { + lifecycleScope.launch { + delay(60.seconds) + finish() } - - override fun onError(e: Exception) { - e(e) - } - - /** - * Automatically finishes the activity 60 seconds after decryption succeeded to prevent - * information leaks from stale activities. - */ - @OptIn(ExperimentalTime::class) - private fun startAutoDismissTimer() { - lifecycleScope.launch { - delay(60.seconds) - finish() - } - } - - /** - * Edit the current password and hide all the fields populated by encrypted data so that when - * the result triggers they can be repopulated with new data. - */ - private fun editPassword() { - val intent = Intent(this, PasswordCreationActivity::class.java) - intent.putExtra("FILE_PATH", relativeParentPath) - intent.putExtra("REPO_PATH", repoPath) - intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name) - intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password) - intent.putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, passwordEntry?.extraContent) - intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true) - startActivity(intent) - finish() + } + + /** + * Edit the current password and hide all the fields populated by encrypted data so that when the + * result triggers they can be repopulated with new data. + */ + private fun editPassword() { + val intent = Intent(this, PasswordCreationActivity::class.java) + intent.putExtra("FILE_PATH", relativeParentPath) + intent.putExtra("REPO_PATH", repoPath) + intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name) + intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password) + intent.putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, passwordEntry?.extraContent) + intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true) + startActivity(intent) + finish() + } + + private fun shareAsPlaintext() { + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, passwordEntry?.password) + type = "text/plain" + } + // Always show a picker to give the user a chance to cancel + startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to))) + } + + @OptIn(ExperimentalTime::class) + private fun decryptAndVerify(receivedIntent: Intent? = null) { + if (api == null) { + bindToOpenKeychain(this) + return } + val data = receivedIntent ?: Intent() + data.action = OpenPgpApi.ACTION_DECRYPT_VERIFY + + val inputStream = File(fullPath).inputStream() + val outputStream = ByteArrayOutputStream() + + lifecycleScope.launch(Dispatchers.IO) { + api?.executeApiAsync(data, inputStream, outputStream) { result -> + when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + OpenPgpApi.RESULT_CODE_SUCCESS -> { + startAutoDismissTimer() + runCatching { + val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true) + val entry = PasswordEntry(outputStream) + val items = arrayListOf() + val adapter = FieldItemAdapter(emptyList(), showPassword) { text -> copyTextToClipboard(text) } + + if (settings.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false)) { + copyPasswordToClipboard(entry.password) + } + + passwordEntry = entry + invalidateOptionsMenu() + + if (entry.password.isNotEmpty()) { + items.add(FieldItem.createPasswordField(entry.password)) + } + + if (entry.hasTotp()) { + launch(Dispatchers.IO) { + // Calculate the actual remaining time for the first pass + // then return to the standard 30 second affair. + val remainingTime = entry.totpPeriod - (System.currentTimeMillis() % entry.totpPeriod) + withContext(Dispatchers.Main) { + val code = entry.calculateTotpCode() ?: "Error" + items.add(FieldItem.createOtpField(code)) + } + delay(remainingTime.seconds) + repeat(Int.MAX_VALUE) { + val code = entry.calculateTotpCode() ?: "Error" + withContext(Dispatchers.Main) { adapter.updateOTPCode(code) } + delay(30.seconds) + } + } + } - private fun shareAsPlaintext() { - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, passwordEntry?.password) - type = "text/plain" - } - // Always show a picker to give the user a chance to cancel - startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to))) - } + if (!entry.username.isNullOrEmpty()) { + items.add(FieldItem.createUsernameField(entry.username)) + } - @OptIn(ExperimentalTime::class) - private fun decryptAndVerify(receivedIntent: Intent? = null) { - if (api == null) { - bindToOpenKeychain(this) - return - } - val data = receivedIntent ?: Intent() - data.action = OpenPgpApi.ACTION_DECRYPT_VERIFY - - val inputStream = File(fullPath).inputStream() - val outputStream = ByteArrayOutputStream() - - lifecycleScope.launch(Dispatchers.IO) { - api?.executeApiAsync(data, inputStream, outputStream) { result -> - when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - OpenPgpApi.RESULT_CODE_SUCCESS -> { - startAutoDismissTimer() - runCatching { - val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true) - val entry = PasswordEntry(outputStream) - val items = arrayListOf() - val adapter = FieldItemAdapter(emptyList(), showPassword) { text -> - copyTextToClipboard(text) - } - - if (settings.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false)) { - copyPasswordToClipboard(entry.password) - } - - passwordEntry = entry - invalidateOptionsMenu() - - if (entry.password.isNotEmpty()) { - items.add(FieldItem.createPasswordField(entry.password)) - } - - if (entry.hasTotp()) { - launch(Dispatchers.IO) { - // Calculate the actual remaining time for the first pass - // then return to the standard 30 second affair. - val remainingTime = - entry.totpPeriod - (System.currentTimeMillis() % entry.totpPeriod) - withContext(Dispatchers.Main) { - val code = entry.calculateTotpCode() ?: "Error" - items.add(FieldItem.createOtpField(code)) - } - delay(remainingTime.seconds) - repeat(Int.MAX_VALUE) { - val code = entry.calculateTotpCode() ?: "Error" - withContext(Dispatchers.Main) { - adapter.updateOTPCode(code) - } - delay(30.seconds) - } - } - } - - if (!entry.username.isNullOrEmpty()) { - items.add(FieldItem.createUsernameField(entry.username)) - } - - if (entry.hasExtraContentWithoutAuthData()) { - entry.extraContentMap.forEach { (key, value) -> - items.add(FieldItem(key, value, FieldItem.ActionType.COPY)) - } - } - - binding.recyclerView.adapter = adapter - adapter.updateItems(items) - }.onFailure { e -> - e(e) - } - } - OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { - val sender = getUserInteractionRequestIntent(result) - userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build()) - } - OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) + if (entry.hasExtraContentWithoutAuthData()) { + entry.extraContentMap.forEach { (key, value) -> + items.add(FieldItem(key, value, FieldItem.ActionType.COPY)) } + } + + binding.recyclerView.adapter = adapter + adapter.updateItems(items) } + .onFailure { e -> e(e) } + } + OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { + val sender = getUserInteractionRequestIntent(result) + userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build()) + } + OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) } + } } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/GetKeyIdsActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/GetKeyIdsActivity.kt index 98492c00..9da4044a 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/GetKeyIdsActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/GetKeyIdsActivity.kt @@ -21,56 +21,54 @@ import org.openintents.openpgp.IOpenPgpService2 class GetKeyIdsActivity : BasePgpActivity() { - private val userInteractionRequiredResult = registerForActivityResult(StartIntentSenderForResult()) { result -> - if (result.data == null || result.resultCode == RESULT_CANCELED) { - setResult(RESULT_CANCELED, result.data) - finish() - return@registerForActivityResult - } - getKeyIds(result.data!!) + private val userInteractionRequiredResult = + registerForActivityResult(StartIntentSenderForResult()) { result -> + if (result.data == null || result.resultCode == RESULT_CANCELED) { + setResult(RESULT_CANCELED, result.data) + finish() + return@registerForActivityResult + } + getKeyIds(result.data!!) } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - bindToOpenKeychain(this) - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bindToOpenKeychain(this) + } - override fun onBound(service: IOpenPgpService2) { - super.onBound(service) - getKeyIds() - } + override fun onBound(service: IOpenPgpService2) { + super.onBound(service) + getKeyIds() + } - override fun onError(e: Exception) { - e(e) - } + override fun onError(e: Exception) { + e(e) + } - /** - * Get the Key ids from OpenKeychain - */ - private fun getKeyIds(data: Intent = Intent()) { - data.action = OpenPgpApi.ACTION_GET_KEY_IDS - lifecycleScope.launch(Dispatchers.IO) { - api?.executeApiAsync(data, null, null) { result -> - when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - OpenPgpApi.RESULT_CODE_SUCCESS -> { - runCatching { - val ids = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)?.map { - OpenPgpUtils.convertKeyIdToHex(it) - } ?: emptyList() - val keyResult = Intent().putExtra(OpenPgpApi.EXTRA_KEY_IDS, ids.toTypedArray()) - setResult(RESULT_OK, keyResult) - finish() - }.onFailure { e -> - e(e) - } - } - OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { - val sender = getUserInteractionRequestIntent(result) - userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build()) - } - OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) - } + /** Get the Key ids from OpenKeychain */ + private fun getKeyIds(data: Intent = Intent()) { + data.action = OpenPgpApi.ACTION_GET_KEY_IDS + lifecycleScope.launch(Dispatchers.IO) { + api?.executeApiAsync(data, null, null) { result -> + when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + OpenPgpApi.RESULT_CODE_SUCCESS -> { + runCatching { + val ids = + result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)?.map { OpenPgpUtils.convertKeyIdToHex(it) } + ?: emptyList() + val keyResult = Intent().putExtra(OpenPgpApi.EXTRA_KEY_IDS, ids.toTypedArray()) + setResult(RESULT_OK, keyResult) + finish() } + .onFailure { e -> e(e) } + } + OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { + val sender = getUserInteractionRequestIntent(result) + userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build()) + } + OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) } + } } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt index 760faba6..1e9066b0 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/crypto/PasswordCreationActivity.kt @@ -55,454 +55,443 @@ import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound { - private val binding by viewBinding(PasswordCreationActivityBinding::inflate) - - private val suggestedName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) } - private val suggestedPass by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_PASSWORD) } - private val suggestedExtra by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_EXTRA_CONTENT) } - private val shouldGeneratePassword by lazy(LazyThreadSafetyMode.NONE) { intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) } - private val editing by lazy(LazyThreadSafetyMode.NONE) { intent.getBooleanExtra(EXTRA_EDITING, false) } - private val oldFileName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) } - private var oldCategory: String? = null - private var copy: Boolean = false - private var encryptionIntent: Intent = Intent() - - private val userInteractionRequiredResult = registerForActivityResult(StartIntentSenderForResult()) { result -> - if (result.data == null) { - setResult(RESULT_CANCELED, null) - finish() - return@registerForActivityResult - } - - when (result.resultCode) { - RESULT_OK -> encrypt(result.data) - RESULT_CANCELED -> { - setResult(RESULT_CANCELED, result.data) - finish() - } + private val binding by viewBinding(PasswordCreationActivityBinding::inflate) + + private val suggestedName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) } + private val suggestedPass by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_PASSWORD) } + private val suggestedExtra by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_EXTRA_CONTENT) } + private val shouldGeneratePassword by lazy(LazyThreadSafetyMode.NONE) { + intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) + } + private val editing by lazy(LazyThreadSafetyMode.NONE) { intent.getBooleanExtra(EXTRA_EDITING, false) } + private val oldFileName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) } + private var oldCategory: String? = null + private var copy: Boolean = false + private var encryptionIntent: Intent = Intent() + + private val userInteractionRequiredResult = + registerForActivityResult(StartIntentSenderForResult()) { result -> + if (result.data == null) { + setResult(RESULT_CANCELED, null) + finish() + return@registerForActivityResult + } + + when (result.resultCode) { + RESULT_OK -> encrypt(result.data) + RESULT_CANCELED -> { + setResult(RESULT_CANCELED, result.data) + finish() } + } } - private val otpImportAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - binding.otpImportButton.isVisible = false - val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data) - val contents = "${intentResult.contents}\n" - val currentExtras = binding.extraContent.text.toString() - if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') - binding.extraContent.append("\n$contents") - else - binding.extraContent.append(contents) - snackbar(message = getString(R.string.otp_import_success)) - } else { - snackbar(message = getString(R.string.otp_import_failure)) - } + private val otpImportAction = + registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + binding.otpImportButton.isVisible = false + val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data) + val contents = "${intentResult.contents}\n" + val currentExtras = binding.extraContent.text.toString() + if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') binding.extraContent.append("\n$contents") + else binding.extraContent.append(contents) + snackbar(message = getString(R.string.otp_import_success)) + } else { + snackbar(message = getString(R.string.otp_import_failure)) + } } - private val gpgKeySelectAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> - lifecycleScope.launch { - val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id") - withContext(Dispatchers.IO) { - gpgIdentifierFile.writeText((keyIds + "").joinToString("\n")) - } - commitChange(getString( - R.string.git_commit_gpg_id, - getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) - )).onSuccess { - encrypt(encryptionIntent) - } - } - } + private val gpgKeySelectAction = + registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> + lifecycleScope.launch { + val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id") + withContext(Dispatchers.IO) { gpgIdentifierFile.writeText((keyIds + "").joinToString("\n")) } + commitChange( + getString( + R.string.git_commit_gpg_id, + getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) + ) + ) + .onSuccess { encrypt(encryptionIntent) } + } } + } } - private fun File.findTillRoot(fileName: String, rootPath: File): File? { - val gpgFile = File(this, fileName) - if (gpgFile.exists()) return gpgFile - - if (this.absolutePath == rootPath.absolutePath) { - return null - } + private fun File.findTillRoot(fileName: String, rootPath: File): File? { + val gpgFile = File(this, fileName) + if (gpgFile.exists()) return gpgFile - val parent = parentFile - return if (parent != null && parent.exists()) { - parent.findTillRoot(fileName, rootPath) - } else { - null - } + if (this.absolutePath == rootPath.absolutePath) { + return null } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - bindToOpenKeychain(this) - title = if (editing) - getString(R.string.edit_password) - else - getString(R.string.new_password_title) - with(binding) { - setContentView(root) - generatePassword.setOnClickListener { generatePassword() } - otpImportButton.setOnClickListener { - supportFragmentManager.setFragmentResultListener(OTP_RESULT_REQUEST_KEY, this@PasswordCreationActivity) { requestKey, bundle -> - if (requestKey == OTP_RESULT_REQUEST_KEY) { - val contents = bundle.getString(RESULT) - val currentExtras = binding.extraContent.text.toString() - if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') - binding.extraContent.append("\n$contents") - else - binding.extraContent.append(contents) - } - } - val items = arrayOf(getString(R.string.otp_import_qr_code), getString(R.string.otp_import_manual_entry)) - MaterialAlertDialogBuilder(this@PasswordCreationActivity) - .setItems(items) { _, index -> - if (index == 0) { - otpImportAction.launch(IntentIntegrator(this@PasswordCreationActivity) - .setOrientationLocked(false) - .setBeepEnabled(false) - .setDesiredBarcodeFormats(QR_CODE) - .createScanIntent()) - } else if (index == 1) { - OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") - } - } - .show() + val parent = parentFile + return if (parent != null && parent.exists()) { + parent.findTillRoot(fileName, rootPath) + } else { + null + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + bindToOpenKeychain(this) + title = if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title) + with(binding) { + setContentView(root) + generatePassword.setOnClickListener { generatePassword() } + otpImportButton.setOnClickListener { + supportFragmentManager.setFragmentResultListener(OTP_RESULT_REQUEST_KEY, this@PasswordCreationActivity) { + requestKey, + bundle -> + if (requestKey == OTP_RESULT_REQUEST_KEY) { + val contents = bundle.getString(RESULT) + val currentExtras = binding.extraContent.text.toString() + if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') binding.extraContent.append("\n$contents") + else binding.extraContent.append(contents) + } + } + val items = arrayOf(getString(R.string.otp_import_qr_code), getString(R.string.otp_import_manual_entry)) + MaterialAlertDialogBuilder(this@PasswordCreationActivity) + .setItems(items) { _, index -> + if (index == 0) { + otpImportAction.launch( + IntentIntegrator(this@PasswordCreationActivity) + .setOrientationLocked(false) + .setBeepEnabled(false) + .setDesiredBarcodeFormats(QR_CODE) + .createScanIntent() + ) + } else if (index == 1) { + OtpImportDialogFragment().show(supportFragmentManager, "OtpImport") } + } + .show() + } - directoryInputLayout.apply { - if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) { - isEnabled = true - } else { - setBackgroundColor(getColor(android.R.color.transparent)) - } - val path = getRelativePath(fullPath, repoPath) - // Keep empty path field visible if it is editable. - if (path.isEmpty() && !isEnabled) - visibility = View.GONE - else { - directory.setText(path) - oldCategory = path - } - } - if (suggestedName != null) { - filename.setText(suggestedName) + directoryInputLayout.apply { + if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) { + isEnabled = true + } else { + setBackgroundColor(getColor(android.R.color.transparent)) + } + val path = getRelativePath(fullPath, repoPath) + // Keep empty path field visible if it is editable. + if (path.isEmpty() && !isEnabled) visibility = View.GONE + else { + directory.setText(path) + oldCategory = path + } + } + if (suggestedName != null) { + filename.setText(suggestedName) + } else { + filename.requestFocus() + } + // Allow the user to quickly switch between storing the username as the filename or + // in the encrypted extras. This only makes sense if the directory structure is + // FileBased. + if (suggestedName == null && + AutofillPreferences.directoryStructure(this@PasswordCreationActivity) == DirectoryStructure.FileBased + ) { + encryptUsername.apply { + visibility = View.VISIBLE + setOnClickListener { + if (isChecked) { + // User wants to enable username encryption, so we add it to the + // encrypted extras as the first line. + val username = filename.text.toString() + val extras = "username:$username\n${extraContent.text}" + + filename.text?.clear() + extraContent.setText(extras) } else { - filename.requestFocus() - } - // Allow the user to quickly switch between storing the username as the filename or - // in the encrypted extras. This only makes sense if the directory structure is - // FileBased. - if (suggestedName == null && - AutofillPreferences.directoryStructure(this@PasswordCreationActivity) == - DirectoryStructure.FileBased - ) { - encryptUsername.apply { - visibility = View.VISIBLE - setOnClickListener { - if (isChecked) { - // User wants to enable username encryption, so we add it to the - // encrypted extras as the first line. - val username = filename.text.toString() - val extras = "username:$username\n${extraContent.text}" - - filename.text?.clear() - extraContent.setText(extras) - } else { - // User wants to disable username encryption, so we extract the - // username from the encrypted extras and use it as the filename. - val entry = PasswordEntry("PASSWORD\n${extraContent.text}") - val username = entry.username - - // username should not be null here by the logic in - // updateViewState, but it could still happen due to - // input lag. - if (username != null) { - filename.setText(username) - extraContent.setText(entry.extraContentWithoutAuthData) - } - } - } - } - listOf(filename, extraContent).forEach { - it.doOnTextChanged { _, _, _, _ -> updateViewState() } - } - } - suggestedPass?.let { - password.setText(it) - password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - } - suggestedExtra?.let { extraContent.setText(it) } - if (shouldGeneratePassword) { - generatePassword() - password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + // User wants to disable username encryption, so we extract the + // username from the encrypted extras and use it as the filename. + val entry = PasswordEntry("PASSWORD\n${extraContent.text}") + val username = entry.username + + // username should not be null here by the logic in + // updateViewState, but it could still happen due to + // input lag. + if (username != null) { + filename.setText(username) + extraContent.setText(entry.extraContentWithoutAuthData) + } } + } } - updateViewState() + listOf(filename, extraContent).forEach { it.doOnTextChanged { _, _, _, _ -> updateViewState() } } + } + suggestedPass?.let { + password.setText(it) + password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + } + suggestedExtra?.let { extraContent.setText(it) } + if (shouldGeneratePassword) { + generatePassword() + password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + } } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(R.menu.pgp_handler_new_password, menu) - return true + updateViewState() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.pgp_handler_new_password, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + setResult(RESULT_CANCELED) + onBackPressed() + } + R.id.save_password -> { + copy = false + encrypt() + } + R.id.save_and_copy_password -> { + copy = true + encrypt() + } + else -> return super.onOptionsItemSelected(item) } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - setResult(RESULT_CANCELED) - onBackPressed() - } - R.id.save_password -> { - copy = false - encrypt() - } - R.id.save_and_copy_password -> { - copy = true - encrypt() - } - else -> return super.onOptionsItemSelected(item) - } - return true + return true + } + + private fun generatePassword() { + supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) { requestKey, bundle -> + if (requestKey == PASSWORD_RESULT_REQUEST_KEY) { + binding.password.setText(bundle.getString(RESULT)) + } } - - private fun generatePassword() { - supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) { requestKey, bundle -> - if (requestKey == PASSWORD_RESULT_REQUEST_KEY) { - binding.password.setText(bundle.getString(RESULT)) - } - } - when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) { - KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment() - .show(supportFragmentManager, "generator") - KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment() - .show(supportFragmentManager, "xkpwgenerator") - } + when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) { + KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator") + KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment().show(supportFragmentManager, "xkpwgenerator") } - - private fun updateViewState() = with(binding) { - // Use PasswordEntry to parse extras for username - val entry = PasswordEntry("PLACEHOLDER\n${extraContent.text}") - encryptUsername.apply { - if (visibility != View.VISIBLE) - return@apply - val hasUsernameInFileName = filename.text.toString().isNotBlank() - val hasUsernameInExtras = entry.hasUsername() - isEnabled = hasUsernameInFileName xor hasUsernameInExtras - isChecked = hasUsernameInExtras - } - otpImportButton.isVisible = !entry.hasTotp() + } + + private fun updateViewState() = + with(binding) { + // Use PasswordEntry to parse extras for username + val entry = PasswordEntry("PLACEHOLDER\n${extraContent.text}") + encryptUsername.apply { + if (visibility != View.VISIBLE) return@apply + val hasUsernameInFileName = filename.text.toString().isNotBlank() + val hasUsernameInExtras = entry.hasUsername() + isEnabled = hasUsernameInFileName xor hasUsernameInExtras + isChecked = hasUsernameInExtras + } + otpImportButton.isVisible = !entry.hasTotp() } - /** - * Encrypts the password and the extra content - */ - private fun encrypt(receivedIntent: Intent? = null) { - with(binding) { - val editName = filename.text.toString().trim() - val editPass = password.text.toString() - val editExtra = extraContent.text.toString() - - if (editName.isEmpty()) { - snackbar(message = resources.getString(R.string.file_toast_text)) - return@with - } else if (editName.contains('/')) { - snackbar(message = resources.getString(R.string.invalid_filename_text)) - return@with + /** Encrypts the password and the extra content */ + private fun encrypt(receivedIntent: Intent? = null) { + with(binding) { + val editName = filename.text.toString().trim() + val editPass = password.text.toString() + val editExtra = extraContent.text.toString() + + if (editName.isEmpty()) { + snackbar(message = resources.getString(R.string.file_toast_text)) + return@with + } else if (editName.contains('/')) { + snackbar(message = resources.getString(R.string.invalid_filename_text)) + return@with + } + + if (editPass.isEmpty() && editExtra.isEmpty()) { + snackbar(message = resources.getString(R.string.empty_toast_text)) + return@with + } + + if (copy) { + copyPasswordToClipboard(editPass) + } + + encryptionIntent = receivedIntent ?: Intent() + encryptionIntent.action = OpenPgpApi.ACTION_ENCRYPT + + // pass enters the key ID into `.gpg-id`. + val repoRoot = PasswordRepository.getRepositoryDirectory() + val gpgIdentifierFile = + File(repoRoot, directory.text.toString()).findTillRoot(".gpg-id", repoRoot) + ?: File(repoRoot, ".gpg-id").apply { createNewFile() } + val gpgIdentifiers = + gpgIdentifierFile.readLines().filter { it.isNotBlank() }.map { line -> + GpgIdentifier.fromString(line) + ?: run { + // The line being empty means this is most likely an empty `.gpg-id` + // file + // we created. Skip the validation so we can make the user add a real + // ID. + if (line.isEmpty()) return@run + if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex())) { + snackbar(message = resources.getString(R.string.short_key_ids_unsupported)) + } else { + snackbar(message = resources.getString(R.string.invalid_gpg_id)) + } + return@with } - - if (editPass.isEmpty() && editExtra.isEmpty()) { - snackbar(message = resources.getString(R.string.empty_toast_text)) - return@with + } + if (gpgIdentifiers.isEmpty()) { + gpgKeySelectAction.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java)) + return@with + } + val keyIds = gpgIdentifiers.filterIsInstance().map { it.id }.toLongArray() + if (keyIds.isNotEmpty()) { + encryptionIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIds) + } + val userIds = gpgIdentifiers.filterIsInstance().map { it.email }.toTypedArray() + if (userIds.isNotEmpty()) { + encryptionIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, userIds) + } + + encryptionIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) + + val content = "$editPass\n$editExtra" + val inputStream = ByteArrayInputStream(content.toByteArray()) + val outputStream = ByteArrayOutputStream() + + val path = + when { + // If we allowed the user to edit the relative path, we have to consider it here + // instead + // of fullPath. + directoryInputLayout.isEnabled -> { + val editRelativePath = directory.text.toString().trim() + if (editRelativePath.isEmpty()) { + snackbar(message = resources.getString(R.string.path_toast_text)) + return } - - if (copy) { - copyPasswordToClipboard(editPass) + val passwordDirectory = File("$repoPath/${editRelativePath.trim('/')}") + if (!passwordDirectory.exists() && !passwordDirectory.mkdir()) { + snackbar(message = "Failed to create directory ${editRelativePath.trim('/')}") + return } - encryptionIntent = receivedIntent ?: Intent() - encryptionIntent.action = OpenPgpApi.ACTION_ENCRYPT - - // pass enters the key ID into `.gpg-id`. - val repoRoot = PasswordRepository.getRepositoryDirectory() - val gpgIdentifierFile = File(repoRoot, directory.text.toString()).findTillRoot(".gpg-id", repoRoot) - ?: File(repoRoot, ".gpg-id").apply { createNewFile() } - val gpgIdentifiers = gpgIdentifierFile.readLines() - .filter { it.isNotBlank() } - .map { line -> - GpgIdentifier.fromString(line) ?: run { - // The line being empty means this is most likely an empty `.gpg-id` file - // we created. Skip the validation so we can make the user add a real ID. - if (line.isEmpty()) return@run - if (line.removePrefix("0x").matches("[a-fA-F0-9]{8}".toRegex())) { - snackbar(message = resources.getString(R.string.short_key_ids_unsupported)) - } else { - snackbar(message = resources.getString(R.string.invalid_gpg_id)) - } - return@with - } + "${passwordDirectory.path}/$editName.gpg" + } + else -> "$fullPath/$editName.gpg" + } + + lifecycleScope.launch(Dispatchers.IO) { + api?.executeApiAsync(encryptionIntent, inputStream, outputStream) { result -> + when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + OpenPgpApi.RESULT_CODE_SUCCESS -> { + runCatching { + val file = File(path) + // If we're not editing, this file should not already exist! + // Additionally, if we were editing and the incoming and outgoing + // filenames differ, it means we renamed. Ensure that the target + // doesn't already exist to prevent an accidental overwrite. + if ((!editing || (editing && suggestedName != file.nameWithoutExtension)) && file.exists()) { + snackbar(message = getString(R.string.password_creation_duplicate_error)) + return@executeApiAsync } - if (gpgIdentifiers.isEmpty()) { - gpgKeySelectAction.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java)) - return@with - } - val keyIds = gpgIdentifiers.filterIsInstance().map { it.id }.toLongArray() - if (keyIds.isNotEmpty()) { - encryptionIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIds) - } - val userIds = gpgIdentifiers.filterIsInstance().map { it.email }.toTypedArray() - if (userIds.isNotEmpty()) { - encryptionIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, userIds) - } - encryptionIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) + if (!file.isInsideRepository()) { + snackbar(message = getString(R.string.message_error_destination_outside_repo)) + return@executeApiAsync + } - val content = "$editPass\n$editExtra" - val inputStream = ByteArrayInputStream(content.toByteArray()) - val outputStream = ByteArrayOutputStream() + file.outputStream().use { it.write(outputStream.toByteArray()) } + + // associate the new password name with the last name's timestamp in + // history + val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + val oldFilePathHash = "$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg".base64() + val timestamp = preference.getString(oldFilePathHash) + if (timestamp != null) { + preference.edit { + remove(oldFilePathHash) + putString(file.absolutePath.base64(), timestamp) + } + } - val path = when { - // If we allowed the user to edit the relative path, we have to consider it here instead - // of fullPath. - directoryInputLayout.isEnabled -> { - val editRelativePath = directory.text.toString().trim() - if (editRelativePath.isEmpty()) { - snackbar(message = resources.getString(R.string.path_toast_text)) - return - } - val passwordDirectory = File("$repoPath/${editRelativePath.trim('/')}") - if (!passwordDirectory.exists() && !passwordDirectory.mkdir()) { - snackbar(message = "Failed to create directory ${editRelativePath.trim('/')}") - return - } + val returnIntent = Intent() + returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path) + returnIntent.putExtra(RETURN_EXTRA_NAME, editName) + returnIntent.putExtra(RETURN_EXTRA_LONG_NAME, getLongName(fullPath, repoPath, editName)) + + if (shouldGeneratePassword) { + val directoryStructure = AutofillPreferences.directoryStructure(applicationContext) + val entry = PasswordEntry(content) + returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password) + val username = entry.username ?: directoryStructure.getUsernameFor(file) + returnIntent.putExtra(RETURN_EXTRA_USERNAME, username) + } - "${passwordDirectory.path}/$editName.gpg" + if (directoryInputLayout.isVisible && directoryInputLayout.isEnabled && oldFileName != null) { + val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg") + if (oldFile.path != file.path && !oldFile.delete()) { + setResult(RESULT_CANCELED) + MaterialAlertDialogBuilder(this@PasswordCreationActivity) + .setTitle(R.string.password_creation_file_fail_title) + .setMessage(getString(R.string.password_creation_file_delete_fail_message, oldFileName)) + .setCancelable(false) + .setPositiveButton(android.R.string.ok) { _, _ -> finish() } + .show() + return@executeApiAsync + } } - else -> "$fullPath/$editName.gpg" - } - lifecycleScope.launch(Dispatchers.IO) { - api?.executeApiAsync(encryptionIntent, inputStream, outputStream) { result -> - when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - OpenPgpApi.RESULT_CODE_SUCCESS -> { - runCatching { - val file = File(path) - // If we're not editing, this file should not already exist! - // Additionally, if we were editing and the incoming and outgoing - // filenames differ, it means we renamed. Ensure that the target - // doesn't already exist to prevent an accidental overwrite. - if ((!editing || (editing && suggestedName != file.nameWithoutExtension)) && file.exists()) { - snackbar(message = getString(R.string.password_creation_duplicate_error)) - return@executeApiAsync - } - - if (!file.isInsideRepository()) { - snackbar(message = getString(R.string.message_error_destination_outside_repo)) - return@executeApiAsync - } - - file.outputStream().use { - it.write(outputStream.toByteArray()) - } - - //associate the new password name with the last name's timestamp in history - val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) - val oldFilePathHash = "$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg".base64() - val timestamp = preference.getString(oldFilePathHash) - if (timestamp != null) { - preference.edit { - remove(oldFilePathHash) - putString(file.absolutePath.base64(), timestamp) - } - } - - val returnIntent = Intent() - returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path) - returnIntent.putExtra(RETURN_EXTRA_NAME, editName) - returnIntent.putExtra(RETURN_EXTRA_LONG_NAME, getLongName(fullPath, repoPath, editName)) - - if (shouldGeneratePassword) { - val directoryStructure = - AutofillPreferences.directoryStructure(applicationContext) - val entry = PasswordEntry(content) - returnIntent.putExtra(RETURN_EXTRA_PASSWORD, entry.password) - val username = entry.username - ?: directoryStructure.getUsernameFor(file) - returnIntent.putExtra(RETURN_EXTRA_USERNAME, username) - } - - if (directoryInputLayout.isVisible && directoryInputLayout.isEnabled && oldFileName != null) { - val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg") - if (oldFile.path != file.path && !oldFile.delete()) { - setResult(RESULT_CANCELED) - MaterialAlertDialogBuilder(this@PasswordCreationActivity) - .setTitle(R.string.password_creation_file_fail_title) - .setMessage(getString(R.string.password_creation_file_delete_fail_message, oldFileName)) - .setCancelable(false) - .setPositiveButton(android.R.string.ok) { _, _ -> - finish() - } - .show() - return@executeApiAsync - } - } - - val commitMessageRes = if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text - lifecycleScope.launch { - commitChange(resources.getString( - commitMessageRes, - getLongName(fullPath, repoPath, editName) - )).onSuccess { - setResult(RESULT_OK, returnIntent) - finish() - } - } - - }.onFailure { e -> - if (e is IOException) { - e(e) { "Failed to write password file" } - setResult(RESULT_CANCELED) - MaterialAlertDialogBuilder(this@PasswordCreationActivity) - .setTitle(getString(R.string.password_creation_file_fail_title)) - .setMessage(getString(R.string.password_creation_file_write_fail_message)) - .setCancelable(false) - .setPositiveButton(android.R.string.ok) { _, _ -> - finish() - } - .show() - } else { - e(e) - } - } - } - OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { - val sender = getUserInteractionRequestIntent(result) - userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build()) - } - OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) + val commitMessageRes = if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text + lifecycleScope.launch { + commitChange(resources.getString(commitMessageRes, getLongName(fullPath, repoPath, editName))) + .onSuccess { + setResult(RESULT_OK, returnIntent) + finish() } } + } + .onFailure { e -> + if (e is IOException) { + e(e) { "Failed to write password file" } + setResult(RESULT_CANCELED) + MaterialAlertDialogBuilder(this@PasswordCreationActivity) + .setTitle(getString(R.string.password_creation_file_fail_title)) + .setMessage(getString(R.string.password_creation_file_write_fail_message)) + .setCancelable(false) + .setPositiveButton(android.R.string.ok) { _, _ -> finish() } + .show() + } else { + e(e) + } + } + } + OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { + val sender = getUserInteractionRequestIntent(result) + userInteractionRequiredResult.launch(IntentSenderRequest.Builder(sender).build()) } + OpenPgpApi.RESULT_CODE_ERROR -> handleError(result) + } } + } } - - companion object { - - private const val KEY_PWGEN_TYPE_CLASSIC = "classic" - private const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd" - const val PASSWORD_RESULT_REQUEST_KEY = "PASSWORD_GENERATOR" - const val OTP_RESULT_REQUEST_KEY = "OTP_IMPORT" - const val RESULT = "RESULT" - const val RETURN_EXTRA_CREATED_FILE = "CREATED_FILE" - const val RETURN_EXTRA_NAME = "NAME" - const val RETURN_EXTRA_LONG_NAME = "LONG_NAME" - const val RETURN_EXTRA_USERNAME = "USERNAME" - const val RETURN_EXTRA_PASSWORD = "PASSWORD" - const val EXTRA_FILE_NAME = "FILENAME" - const val EXTRA_PASSWORD = "PASSWORD" - const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT" - const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD" - const val EXTRA_EDITING = "EDITING" - } + } + + companion object { + + private const val KEY_PWGEN_TYPE_CLASSIC = "classic" + private const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd" + const val PASSWORD_RESULT_REQUEST_KEY = "PASSWORD_GENERATOR" + const val OTP_RESULT_REQUEST_KEY = "OTP_IMPORT" + const val RESULT = "RESULT" + const val RETURN_EXTRA_CREATED_FILE = "CREATED_FILE" + const val RETURN_EXTRA_NAME = "NAME" + const val RETURN_EXTRA_LONG_NAME = "LONG_NAME" + const val RETURN_EXTRA_USERNAME = "USERNAME" + const val RETURN_EXTRA_PASSWORD = "PASSWORD" + const val EXTRA_FILE_NAME = "FILENAME" + const val EXTRA_PASSWORD = "PASSWORD" + const val EXTRA_EXTRA_CONTENT = "EXTRA_CONTENT" + const val EXTRA_GENERATE_PASSWORD = "GENERATE_PASSWORD" + const val EXTRA_EDITING = "EDITING" + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/BasicBottomSheet.kt b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/BasicBottomSheet.kt index e92d87aa..fa188a22 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/BasicBottomSheet.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/BasicBottomSheet.kt @@ -25,138 +25,136 @@ import dev.msfjarvis.aps.util.extensions.resolveAttribute import dev.msfjarvis.aps.util.extensions.viewBinding /** - * [BottomSheetDialogFragment] that exposes a simple [androidx.appcompat.app.AlertDialog] like - * API through [Builder] to create a similar UI, just at the bottom of the screen. + * [BottomSheetDialogFragment] that exposes a simple [androidx.appcompat.app.AlertDialog] like API + * through [Builder] to create a similar UI, just at the bottom of the screen. */ -class BasicBottomSheet private constructor( - val title: String?, - val message: String, - val positiveButtonLabel: String?, - val negativeButtonLabel: String?, - val positiveButtonClickListener: View.OnClickListener?, - val negativeButtonClickListener: View.OnClickListener?, +class BasicBottomSheet +private constructor( + val title: String?, + val message: String, + val positiveButtonLabel: String?, + val negativeButtonLabel: String?, + val positiveButtonClickListener: View.OnClickListener?, + val negativeButtonClickListener: View.OnClickListener?, ) : BottomSheetDialogFragment() { - private val binding by viewBinding(BasicBottomSheetBinding::bind) + private val binding by viewBinding(BasicBottomSheetBinding::bind) - private var behavior: BottomSheetBehavior? = null - private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() { - override fun onSlide(bottomSheet: View, slideOffset: Float) { - } + private var behavior: BottomSheetBehavior? = null + private val bottomSheetCallback = + object : BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, slideOffset: Float) {} - override fun onStateChanged(bottomSheet: View, newState: Int) { - if (newState == BottomSheetBehavior.STATE_COLLAPSED) { - dismiss() - } + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_COLLAPSED) { + dismiss() } + } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - if (savedInstanceState != null) dismiss() - return layoutInflater.inflate(R.layout.basic_bottom_sheet, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - view.viewTreeObserver.removeOnGlobalLayoutListener(this) - val dialog = dialog as BottomSheetDialog? ?: return - behavior = dialog.behavior - behavior?.apply { - state = BottomSheetBehavior.STATE_EXPANDED - peekHeight = 0 - addBottomSheetCallback(bottomSheetCallback) - } - if (!title.isNullOrEmpty()) { - binding.bottomSheetTitle.isVisible = true - binding.bottomSheetTitle.text = title - } - binding.bottomSheetMessage.text = message - if (positiveButtonClickListener != null) { - positiveButtonLabel?.let { buttonLbl -> - binding.bottomSheetOkButton.text = buttonLbl - } - binding.bottomSheetOkButton.isVisible = true - binding.bottomSheetOkButton.setOnClickListener { - positiveButtonClickListener.onClick(it) - dismiss() - } - } - if (negativeButtonClickListener != null) { - binding.bottomSheetCancelButton.isVisible = true - negativeButtonLabel?.let { buttonLbl -> - binding.bottomSheetCancelButton.text = buttonLbl - } - binding.bottomSheetCancelButton.setOnClickListener { - negativeButtonClickListener.onClick(it) - dismiss() - } - } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + if (savedInstanceState != null) dismiss() + return layoutInflater.inflate(R.layout.basic_bottom_sheet, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + view.viewTreeObserver.addOnGlobalLayoutListener( + object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + view.viewTreeObserver.removeOnGlobalLayoutListener(this) + val dialog = dialog as BottomSheetDialog? ?: return + behavior = dialog.behavior + behavior?.apply { + state = BottomSheetBehavior.STATE_EXPANDED + peekHeight = 0 + addBottomSheetCallback(bottomSheetCallback) + } + if (!title.isNullOrEmpty()) { + binding.bottomSheetTitle.isVisible = true + binding.bottomSheetTitle.text = title + } + binding.bottomSheetMessage.text = message + if (positiveButtonClickListener != null) { + positiveButtonLabel?.let { buttonLbl -> binding.bottomSheetOkButton.text = buttonLbl } + binding.bottomSheetOkButton.isVisible = true + binding.bottomSheetOkButton.setOnClickListener { + positiveButtonClickListener.onClick(it) + dismiss() + } + } + if (negativeButtonClickListener != null) { + binding.bottomSheetCancelButton.isVisible = true + negativeButtonLabel?.let { buttonLbl -> binding.bottomSheetCancelButton.text = buttonLbl } + binding.bottomSheetCancelButton.setOnClickListener { + negativeButtonClickListener.onClick(it) + dismiss() } - }) - val gradientDrawable = GradientDrawable().apply { - setColor(requireContext().resolveAttribute(android.R.attr.windowBackground)) + } } - view.background = gradientDrawable + } + ) + val gradientDrawable = + GradientDrawable().apply { setColor(requireContext().resolveAttribute(android.R.attr.windowBackground)) } + view.background = gradientDrawable + } + + override fun dismiss() { + super.dismiss() + behavior?.removeBottomSheetCallback(bottomSheetCallback) + } + + class Builder(val context: Context) { + + private var title: String? = null + private var message: String? = null + private var positiveButtonLabel: String? = null + private var negativeButtonLabel: String? = null + private var positiveButtonClickListener: View.OnClickListener? = null + private var negativeButtonClickListener: View.OnClickListener? = null + + fun setTitleRes(@StringRes titleRes: Int): Builder { + this.title = context.resources.getString(titleRes) + return this } - override fun dismiss() { - super.dismiss() - behavior?.removeBottomSheetCallback(bottomSheetCallback) + fun setTitle(title: String): Builder { + this.title = title + return this } - class Builder(val context: Context) { - - private var title: String? = null - private var message: String? = null - private var positiveButtonLabel: String? = null - private var negativeButtonLabel: String? = null - private var positiveButtonClickListener: View.OnClickListener? = null - private var negativeButtonClickListener: View.OnClickListener? = null - - fun setTitleRes(@StringRes titleRes: Int): Builder { - this.title = context.resources.getString(titleRes) - return this - } - - fun setTitle(title: String): Builder { - this.title = title - return this - } - - fun setMessageRes(@StringRes messageRes: Int): Builder { - this.message = context.resources.getString(messageRes) - return this - } + fun setMessageRes(@StringRes messageRes: Int): Builder { + this.message = context.resources.getString(messageRes) + return this + } - fun setMessage(message: String): Builder { - this.message = message - return this - } + fun setMessage(message: String): Builder { + this.message = message + return this + } - fun setPositiveButtonClickListener(buttonLabel: String? = null, listener: View.OnClickListener): Builder { - this.positiveButtonClickListener = listener - this.positiveButtonLabel = buttonLabel - return this - } + fun setPositiveButtonClickListener(buttonLabel: String? = null, listener: View.OnClickListener): Builder { + this.positiveButtonClickListener = listener + this.positiveButtonLabel = buttonLabel + return this + } - fun setNegativeButtonClickListener(buttonLabel: String? = null, listener: View.OnClickListener): Builder { - this.negativeButtonClickListener = listener - this.negativeButtonLabel = buttonLabel - return this - } + fun setNegativeButtonClickListener(buttonLabel: String? = null, listener: View.OnClickListener): Builder { + this.negativeButtonClickListener = listener + this.negativeButtonLabel = buttonLabel + return this + } - fun build(): BasicBottomSheet { - require(message != null) { "Message needs to be set" } - return BasicBottomSheet( - title, - message!!, - positiveButtonLabel, - negativeButtonLabel, - positiveButtonClickListener, - negativeButtonClickListener - ) - } + fun build(): BasicBottomSheet { + require(message != null) { "Message needs to be set" } + return BasicBottomSheet( + title, + message!!, + positiveButtonLabel, + negativeButtonLabel, + positiveButtonClickListener, + negativeButtonClickListener + ) } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/FolderCreationDialogFragment.kt index 177bfbe3..45dfba2b 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/FolderCreationDialogFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/FolderCreationDialogFragment.kt @@ -30,77 +30,82 @@ import me.msfjarvis.openpgpktx.util.OpenPgpApi class FolderCreationDialogFragment : DialogFragment() { - private lateinit var newFolder: File + private lateinit var newFolder: File - private val keySelectAction = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK) { - result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> - val gpgIdentifierFile = File(newFolder, ".gpg-id") - gpgIdentifierFile.writeText(keyIds.joinToString("\n")) - val repo = PasswordRepository.getRepository(null) - if (repo != null) { - lifecycleScope.launch { - val repoPath = PasswordRepository.getRepositoryDirectory().absolutePath - requireActivity().commitChange( - getString( - R.string.git_commit_gpg_id, - BasePgpActivity.getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name) - ), - ) - dismiss() - } - } + private val keySelectAction = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK) { + result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> + val gpgIdentifierFile = File(newFolder, ".gpg-id") + gpgIdentifierFile.writeText(keyIds.joinToString("\n")) + val repo = PasswordRepository.getRepository(null) + if (repo != null) { + lifecycleScope.launch { + val repoPath = PasswordRepository.getRepositoryDirectory().absolutePath + requireActivity() + .commitChange( + getString( + R.string.git_commit_gpg_id, + BasePgpActivity.getLongName( + gpgIdentifierFile.parentFile!!.absolutePath, + repoPath, + gpgIdentifierFile.name + ) + ), + ) + dismiss() } + } } + } } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext()) - alertDialogBuilder.setTitle(R.string.title_create_folder) - alertDialogBuilder.setView(R.layout.folder_dialog_fragment) - alertDialogBuilder.setPositiveButton(getString(R.string.button_create), null) - alertDialogBuilder.setNegativeButton(getString(android.R.string.cancel)) { _, _ -> - dismiss() - } - val dialog = alertDialogBuilder.create() - dialog.requestInputFocusOnView(R.id.folder_name_text) - dialog.setOnShowListener { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { - createDirectory(requireArguments().getString(CURRENT_DIR_EXTRA)!!) - } - } - return dialog + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext()) + alertDialogBuilder.setTitle(R.string.title_create_folder) + alertDialogBuilder.setView(R.layout.folder_dialog_fragment) + alertDialogBuilder.setPositiveButton(getString(R.string.button_create), null) + alertDialogBuilder.setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dismiss() } + val dialog = alertDialogBuilder.create() + dialog.requestInputFocusOnView(R.id.folder_name_text) + dialog.setOnShowListener { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + createDirectory(requireArguments().getString(CURRENT_DIR_EXTRA)!!) + } } + return dialog + } - private fun createDirectory(currentDir: String) { - val dialog = requireDialog() - val folderNameView = dialog.findViewById(R.id.folder_name_text) - val folderNameViewContainer = dialog.findViewById(R.id.folder_name_container) - newFolder = File("$currentDir/${folderNameView.text}") - folderNameViewContainer.error = when { - newFolder.isFile -> getString(R.string.folder_creation_err_file_exists) - newFolder.isDirectory -> getString(R.string.folder_creation_err_folder_exists) - else -> null - } - if (folderNameViewContainer.error != null) return - newFolder.mkdirs() - (requireActivity() as PasswordStore).refreshPasswordList(newFolder) - if (dialog.findViewById(R.id.set_gpg_key).isChecked) { - keySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java)) - return - } else { - dismiss() - } + private fun createDirectory(currentDir: String) { + val dialog = requireDialog() + val folderNameView = dialog.findViewById(R.id.folder_name_text) + val folderNameViewContainer = dialog.findViewById(R.id.folder_name_container) + newFolder = File("$currentDir/${folderNameView.text}") + folderNameViewContainer.error = + when { + newFolder.isFile -> getString(R.string.folder_creation_err_file_exists) + newFolder.isDirectory -> getString(R.string.folder_creation_err_folder_exists) + else -> null + } + if (folderNameViewContainer.error != null) return + newFolder.mkdirs() + (requireActivity() as PasswordStore).refreshPasswordList(newFolder) + if (dialog.findViewById(R.id.set_gpg_key).isChecked) { + keySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java)) + return + } else { + dismiss() } + } - companion object { + companion object { - private const val CURRENT_DIR_EXTRA = "CURRENT_DIRECTORY" - fun newInstance(startingDirectory: String): FolderCreationDialogFragment { - val extras = bundleOf(CURRENT_DIR_EXTRA to startingDirectory) - val fragment = FolderCreationDialogFragment() - fragment.arguments = extras - return fragment - } + private const val CURRENT_DIR_EXTRA = "CURRENT_DIRECTORY" + fun newInstance(startingDirectory: String): FolderCreationDialogFragment { + val extras = bundleOf(CURRENT_DIR_EXTRA to startingDirectory) + val fragment = FolderCreationDialogFragment() + fragment.arguments = extras + return fragment } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/ItemCreationBottomSheet.kt b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/ItemCreationBottomSheet.kt index 40a876e1..fcf7b372 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/ItemCreationBottomSheet.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/ItemCreationBottomSheet.kt @@ -25,53 +25,54 @@ import dev.msfjarvis.aps.util.extensions.resolveAttribute class ItemCreationBottomSheet : BottomSheetDialogFragment() { - private var behavior: BottomSheetBehavior? = null - private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() { - override fun onSlide(bottomSheet: View, slideOffset: Float) { - } + private var behavior: BottomSheetBehavior? = null + private val bottomSheetCallback = + object : BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, slideOffset: Float) {} - override fun onStateChanged(bottomSheet: View, newState: Int) { - if (newState == BottomSheetBehavior.STATE_COLLAPSED) { - dismiss() - } + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_COLLAPSED) { + dismiss() } + } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - if (savedInstanceState != null) dismiss() - return inflater.inflate(R.layout.item_create_sheet, container, false) - } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + if (savedInstanceState != null) dismiss() + return inflater.inflate(R.layout.item_create_sheet, container, false) + } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - view.viewTreeObserver.removeOnGlobalLayoutListener(this) - val dialog = dialog as BottomSheetDialog? ?: return - behavior = dialog.behavior - behavior?.apply { - state = BottomSheetBehavior.STATE_EXPANDED - peekHeight = 0 - addBottomSheetCallback(bottomSheetCallback) - } - dialog.findViewById(R.id.create_folder)?.setOnClickListener { - setFragmentResult(ITEM_CREATION_REQUEST_KEY, bundleOf(ACTION_KEY to ACTION_FOLDER)) - dismiss() - } - dialog.findViewById(R.id.create_password)?.setOnClickListener { - setFragmentResult(ITEM_CREATION_REQUEST_KEY, bundleOf(ACTION_KEY to ACTION_PASSWORD)) - dismiss() - } - } - }) - val gradientDrawable = GradientDrawable().apply { - setColor(requireContext().resolveAttribute(android.R.attr.windowBackground)) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + view.viewTreeObserver.addOnGlobalLayoutListener( + object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + view.viewTreeObserver.removeOnGlobalLayoutListener(this) + val dialog = dialog as BottomSheetDialog? ?: return + behavior = dialog.behavior + behavior?.apply { + state = BottomSheetBehavior.STATE_EXPANDED + peekHeight = 0 + addBottomSheetCallback(bottomSheetCallback) + } + dialog.findViewById(R.id.create_folder)?.setOnClickListener { + setFragmentResult(ITEM_CREATION_REQUEST_KEY, bundleOf(ACTION_KEY to ACTION_FOLDER)) + dismiss() + } + dialog.findViewById(R.id.create_password)?.setOnClickListener { + setFragmentResult(ITEM_CREATION_REQUEST_KEY, bundleOf(ACTION_KEY to ACTION_PASSWORD)) + dismiss() + } } - view.background = gradientDrawable - } + } + ) + val gradientDrawable = + GradientDrawable().apply { setColor(requireContext().resolveAttribute(android.R.attr.windowBackground)) } + view.background = gradientDrawable + } - override fun dismiss() { - super.dismiss() - behavior?.removeBottomSheetCallback(bottomSheetCallback) - } + override fun dismiss() { + super.dismiss() + behavior?.removeBottomSheetCallback(bottomSheetCallback) + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/OtpImportDialogFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/OtpImportDialogFragment.kt index 44c7a43a..5507218b 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/OtpImportDialogFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/OtpImportDialogFragment.kt @@ -20,32 +20,30 @@ import dev.msfjarvis.aps.util.extensions.requestInputFocusOnView class OtpImportDialogFragment : DialogFragment() { - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = MaterialAlertDialogBuilder(requireContext()) - val binding = FragmentManualOtpEntryBinding.inflate(layoutInflater) - builder.setView(binding.root) - builder.setPositiveButton(android.R.string.ok) { _, _ -> - setFragmentResult( - PasswordCreationActivity.OTP_RESULT_REQUEST_KEY, - bundleOf( - PasswordCreationActivity.RESULT to getTOTPUri(binding) - ) - ) - } - val dialog = builder.create() - dialog.requestInputFocusOnView(R.id.secret) - return dialog + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val builder = MaterialAlertDialogBuilder(requireContext()) + val binding = FragmentManualOtpEntryBinding.inflate(layoutInflater) + builder.setView(binding.root) + builder.setPositiveButton(android.R.string.ok) { _, _ -> + setFragmentResult( + PasswordCreationActivity.OTP_RESULT_REQUEST_KEY, + bundleOf(PasswordCreationActivity.RESULT to getTOTPUri(binding)) + ) } + val dialog = builder.create() + dialog.requestInputFocusOnView(R.id.secret) + return dialog + } - private fun getTOTPUri(binding: FragmentManualOtpEntryBinding): String { - val secret = binding.secret.text.toString() - val account = binding.account.text.toString() - if (secret.isBlank()) return "" - val builder = Uri.Builder() - builder.scheme("otpauth") - builder.authority("totp") - builder.appendQueryParameter("secret", secret) - if (account.isNotBlank()) builder.appendQueryParameter("issuer", account) - return builder.build().toString() - } + private fun getTOTPUri(binding: FragmentManualOtpEntryBinding): String { + val secret = binding.secret.text.toString() + val account = binding.account.text.toString() + if (secret.isBlank()) return "" + val builder = Uri.Builder() + builder.scheme("otpauth") + builder.authority("totp") + builder.appendQueryParameter("secret", secret) + if (account.isNotBlank()) builder.appendQueryParameter("issuer", account) + return builder.build().toString() + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt index ad9fad1d..16a9eefb 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/PasswordGeneratorDialogFragment.kt @@ -31,72 +31,70 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys class PasswordGeneratorDialogFragment : DialogFragment() { - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = MaterialAlertDialogBuilder(requireContext()) - val callingActivity = requireActivity() - val binding = FragmentPwgenBinding.inflate(layoutInflater) - val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf") - val prefs = requireActivity().applicationContext - .getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val builder = MaterialAlertDialogBuilder(requireContext()) + val callingActivity = requireActivity() + val binding = FragmentPwgenBinding.inflate(layoutInflater) + val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf") + val prefs = requireActivity().applicationContext.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE) - builder.setView(binding.root) + builder.setView(binding.root) - binding.numerals.isChecked = !prefs.getBoolean(PasswordOption.NoDigits.key, false) - binding.symbols.isChecked = prefs.getBoolean(PasswordOption.AtLeastOneSymbol.key, false) - binding.uppercase.isChecked = !prefs.getBoolean(PasswordOption.NoUppercaseLetters.key, false) - binding.lowercase.isChecked = !prefs.getBoolean(PasswordOption.NoLowercaseLetters.key, false) - binding.ambiguous.isChecked = !prefs.getBoolean(PasswordOption.NoAmbiguousCharacters.key, false) - binding.pronounceable.isChecked = !prefs.getBoolean(PasswordOption.FullyRandom.key, true) + binding.numerals.isChecked = !prefs.getBoolean(PasswordOption.NoDigits.key, false) + binding.symbols.isChecked = prefs.getBoolean(PasswordOption.AtLeastOneSymbol.key, false) + binding.uppercase.isChecked = !prefs.getBoolean(PasswordOption.NoUppercaseLetters.key, false) + binding.lowercase.isChecked = !prefs.getBoolean(PasswordOption.NoLowercaseLetters.key, false) + binding.ambiguous.isChecked = !prefs.getBoolean(PasswordOption.NoAmbiguousCharacters.key, false) + binding.pronounceable.isChecked = !prefs.getBoolean(PasswordOption.FullyRandom.key, true) - binding.lengthNumber.setText(prefs.getInt(PreferenceKeys.LENGTH, 20).toString()) - binding.passwordText.typeface = monoTypeface - return builder.run { - setTitle(R.string.pwgen_title) - setPositiveButton(R.string.dialog_ok) { _, _ -> - setFragmentResult( - PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY, - bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}") - ) - } - setNeutralButton(R.string.dialog_cancel) { _, _ -> } - setNegativeButton(R.string.pwgen_generate, null) - create() - }.apply { - setOnShowListener { - generate(binding.passwordText) - getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { - generate(binding.passwordText) - } - } + binding.lengthNumber.setText(prefs.getInt(PreferenceKeys.LENGTH, 20).toString()) + binding.passwordText.typeface = monoTypeface + return builder + .run { + setTitle(R.string.pwgen_title) + setPositiveButton(R.string.dialog_ok) { _, _ -> + setFragmentResult( + PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY, + bundleOf(PasswordCreationActivity.RESULT to "${binding.passwordText.text}") + ) } - } - - private fun generate(passwordField: AppCompatTextView) { - setPreferences() - passwordField.text = runCatching { - generate(requireContext().applicationContext) - }.getOrElse { e -> - Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show() - "" + setNeutralButton(R.string.dialog_cancel) { _, _ -> } + setNegativeButton(R.string.pwgen_generate, null) + create() + } + .apply { + setOnShowListener { + generate(binding.passwordText) + getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { generate(binding.passwordText) } } - } + } + } + + private fun generate(passwordField: AppCompatTextView) { + setPreferences() + passwordField.text = + runCatching { generate(requireContext().applicationContext) }.getOrElse { e -> + Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show() + "" + } + } - private fun isChecked(@IdRes id: Int): Boolean { - return requireDialog().findViewById(id).isChecked - } + private fun isChecked(@IdRes id: Int): Boolean { + return requireDialog().findViewById(id).isChecked + } - private fun setPreferences() { - val preferences = listOfNotNull( - PasswordOption.NoDigits.takeIf { !isChecked(R.id.numerals) }, - PasswordOption.AtLeastOneSymbol.takeIf { isChecked(R.id.symbols) }, - PasswordOption.NoUppercaseLetters.takeIf { !isChecked(R.id.uppercase) }, - PasswordOption.NoAmbiguousCharacters.takeIf { !isChecked(R.id.ambiguous) }, - PasswordOption.FullyRandom.takeIf { !isChecked(R.id.pronounceable) }, - PasswordOption.NoLowercaseLetters.takeIf { !isChecked(R.id.lowercase) } - ) - val lengthText = requireDialog().findViewById(R.id.lengthNumber).text.toString() - val length = lengthText.toIntOrNull()?.takeIf { it >= 0 } - ?: PasswordGenerator.DEFAULT_LENGTH - setPrefs(requireActivity().applicationContext, preferences, length) - } + private fun setPreferences() { + val preferences = + listOfNotNull( + PasswordOption.NoDigits.takeIf { !isChecked(R.id.numerals) }, + PasswordOption.AtLeastOneSymbol.takeIf { isChecked(R.id.symbols) }, + PasswordOption.NoUppercaseLetters.takeIf { !isChecked(R.id.uppercase) }, + PasswordOption.NoAmbiguousCharacters.takeIf { !isChecked(R.id.ambiguous) }, + PasswordOption.FullyRandom.takeIf { !isChecked(R.id.pronounceable) }, + PasswordOption.NoLowercaseLetters.takeIf { !isChecked(R.id.lowercase) } + ) + val lengthText = requireDialog().findViewById(R.id.lengthNumber).text.toString() + val length = lengthText.toIntOrNull()?.takeIf { it >= 0 } ?: PasswordGenerator.DEFAULT_LENGTH + setPrefs(requireActivity().applicationContext, preferences, length) + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/XkPasswordGeneratorDialogFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/XkPasswordGeneratorDialogFragment.kt index 0e7b0a1d..e2e7426b 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/XkPasswordGeneratorDialogFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/dialogs/XkPasswordGeneratorDialogFragment.kt @@ -27,103 +27,102 @@ import dev.msfjarvis.aps.util.extensions.getString import dev.msfjarvis.aps.util.pwgenxkpwd.CapsType import dev.msfjarvis.aps.util.pwgenxkpwd.PasswordBuilder -/** A placeholder fragment containing a simple view. */ +/** A placeholder fragment containing a simple view. */ class XkPasswordGeneratorDialogFragment : DialogFragment() { - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = MaterialAlertDialogBuilder(requireContext()) - val callingActivity = requireActivity() - val inflater = callingActivity.layoutInflater - val binding = FragmentXkpwgenBinding.inflate(inflater) - val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf") - val prefs = callingActivity.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE) - - builder.setView(binding.root) - - val previousStoredCapStyle: String = runCatching { - prefs.getString(PREF_KEY_CAPITALS_STYLE)!! - }.getOr(DEFAULT_CAPS_STYLE) - - val lastCapitalsStyleIndex: Int = runCatching { - CapsType.valueOf(previousStoredCapStyle).ordinal - }.getOr(DEFAULT_CAPS_INDEX) - binding.xkCapType.setSelection(lastCapitalsStyleIndex) - binding.xkNumWords.setText(prefs.getString(PREF_KEY_NUM_WORDS, DEFAULT_NUMBER_OF_WORDS)) - - binding.xkSeparator.setText(prefs.getString(PREF_KEY_SEPARATOR, DEFAULT_WORD_SEPARATOR)) - binding.xkNumberSymbolMask.setText(prefs.getString(PREF_KEY_EXTRA_SYMBOLS_MASK, DEFAULT_EXTRA_SYMBOLS_MASK)) - - binding.xkPasswordText.typeface = monoTypeface - - builder.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> - setPreferences(binding, prefs) - setFragmentResult( - PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY, - bundleOf(PasswordCreationActivity.RESULT to "${binding.xkPasswordText.text}") - ) - } - - // flip neutral and negative buttons - builder.setNeutralButton(resources.getString(R.string.dialog_cancel)) { _, _ -> } - builder.setNegativeButton(resources.getString(R.string.pwgen_generate), null) - - val dialog = builder.setTitle(this.resources.getString(R.string.xkpwgen_title)).create() - - dialog.setOnShowListener { - setPreferences(binding, prefs) - makeAndSetPassword(binding) - - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { - setPreferences(binding, prefs) - makeAndSetPassword(binding) - } - } - return dialog - } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val builder = MaterialAlertDialogBuilder(requireContext()) + val callingActivity = requireActivity() + val inflater = callingActivity.layoutInflater + val binding = FragmentXkpwgenBinding.inflate(inflater) + val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf") + val prefs = callingActivity.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE) - private fun makeAndSetPassword(binding: FragmentXkpwgenBinding) { - PasswordBuilder(requireContext()) - .setNumberOfWords(Integer.valueOf(binding.xkNumWords.text.toString())) - .setMinimumWordLength(DEFAULT_MIN_WORD_LENGTH) - .setMaximumWordLength(DEFAULT_MAX_WORD_LENGTH) - .setSeparator(binding.xkSeparator.text.toString()) - .appendNumbers(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_DIGIT }) - .appendSymbols(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_SYMBOL }) - .setCapitalization(CapsType.valueOf(binding.xkCapType.selectedItem.toString())).create() - .fold( - success = { binding.xkPasswordText.text = it }, - failure = { e -> - Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show() - tag("xkpw").e(e, "failure generating xkpasswd") - binding.xkPasswordText.text = FALLBACK_ERROR_PASS - }, - ) - } + builder.setView(binding.root) + + val previousStoredCapStyle: String = + runCatching { prefs.getString(PREF_KEY_CAPITALS_STYLE)!! }.getOr(DEFAULT_CAPS_STYLE) + + val lastCapitalsStyleIndex: Int = + runCatching { CapsType.valueOf(previousStoredCapStyle).ordinal }.getOr(DEFAULT_CAPS_INDEX) + binding.xkCapType.setSelection(lastCapitalsStyleIndex) + binding.xkNumWords.setText(prefs.getString(PREF_KEY_NUM_WORDS, DEFAULT_NUMBER_OF_WORDS)) + + binding.xkSeparator.setText(prefs.getString(PREF_KEY_SEPARATOR, DEFAULT_WORD_SEPARATOR)) + binding.xkNumberSymbolMask.setText(prefs.getString(PREF_KEY_EXTRA_SYMBOLS_MASK, DEFAULT_EXTRA_SYMBOLS_MASK)) - private fun setPreferences(binding: FragmentXkpwgenBinding, prefs: SharedPreferences) { - prefs.edit { - putString(PREF_KEY_CAPITALS_STYLE, binding.xkCapType.selectedItem.toString()) - putString(PREF_KEY_NUM_WORDS, binding.xkNumWords.text.toString()) - putString(PREF_KEY_SEPARATOR, binding.xkSeparator.text.toString()) - putString(PREF_KEY_EXTRA_SYMBOLS_MASK, binding.xkNumberSymbolMask.text.toString()) - } + binding.xkPasswordText.typeface = monoTypeface + + builder.setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> + setPreferences(binding, prefs) + setFragmentResult( + PasswordCreationActivity.PASSWORD_RESULT_REQUEST_KEY, + bundleOf(PasswordCreationActivity.RESULT to "${binding.xkPasswordText.text}") + ) } - companion object { - - const val PREF_KEY_CAPITALS_STYLE = "pref_key_capitals_style" - const val PREF_KEY_NUM_WORDS = "pref_key_num_words" - const val PREF_KEY_SEPARATOR = "pref_key_separator" - const val PREF_KEY_EXTRA_SYMBOLS_MASK = "pref_key_xkpwgen_extra_symbols_mask" - val DEFAULT_CAPS_STYLE = CapsType.Sentence.name - val DEFAULT_CAPS_INDEX = CapsType.Sentence.ordinal - const val DEFAULT_NUMBER_OF_WORDS = "3" - const val DEFAULT_WORD_SEPARATOR = "." - const val DEFAULT_EXTRA_SYMBOLS_MASK = "ds" - const val DEFAULT_MIN_WORD_LENGTH = 3 - const val DEFAULT_MAX_WORD_LENGTH = 9 - const val FALLBACK_ERROR_PASS = "42" - const val EXTRA_CHAR_PLACEHOLDER_DIGIT = 'd' - const val EXTRA_CHAR_PLACEHOLDER_SYMBOL = 's' + // flip neutral and negative buttons + builder.setNeutralButton(resources.getString(R.string.dialog_cancel)) { _, _ -> } + builder.setNegativeButton(resources.getString(R.string.pwgen_generate), null) + + val dialog = builder.setTitle(this.resources.getString(R.string.xkpwgen_title)).create() + + dialog.setOnShowListener { + setPreferences(binding, prefs) + makeAndSetPassword(binding) + + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { + setPreferences(binding, prefs) + makeAndSetPassword(binding) + } + } + return dialog + } + + private fun makeAndSetPassword(binding: FragmentXkpwgenBinding) { + PasswordBuilder(requireContext()) + .setNumberOfWords(Integer.valueOf(binding.xkNumWords.text.toString())) + .setMinimumWordLength(DEFAULT_MIN_WORD_LENGTH) + .setMaximumWordLength(DEFAULT_MAX_WORD_LENGTH) + .setSeparator(binding.xkSeparator.text.toString()) + .appendNumbers(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_DIGIT }) + .appendSymbols(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_SYMBOL }) + .setCapitalization(CapsType.valueOf(binding.xkCapType.selectedItem.toString())) + .create() + .fold( + success = { binding.xkPasswordText.text = it }, + failure = { e -> + Toast.makeText(requireActivity(), e.message, Toast.LENGTH_SHORT).show() + tag("xkpw").e(e, "failure generating xkpasswd") + binding.xkPasswordText.text = FALLBACK_ERROR_PASS + }, + ) + } + + private fun setPreferences(binding: FragmentXkpwgenBinding, prefs: SharedPreferences) { + prefs.edit { + putString(PREF_KEY_CAPITALS_STYLE, binding.xkCapType.selectedItem.toString()) + putString(PREF_KEY_NUM_WORDS, binding.xkNumWords.text.toString()) + putString(PREF_KEY_SEPARATOR, binding.xkSeparator.text.toString()) + putString(PREF_KEY_EXTRA_SYMBOLS_MASK, binding.xkNumberSymbolMask.text.toString()) } + } + + companion object { + + const val PREF_KEY_CAPITALS_STYLE = "pref_key_capitals_style" + const val PREF_KEY_NUM_WORDS = "pref_key_num_words" + const val PREF_KEY_SEPARATOR = "pref_key_separator" + const val PREF_KEY_EXTRA_SYMBOLS_MASK = "pref_key_xkpwgen_extra_symbols_mask" + val DEFAULT_CAPS_STYLE = CapsType.Sentence.name + val DEFAULT_CAPS_INDEX = CapsType.Sentence.ordinal + const val DEFAULT_NUMBER_OF_WORDS = "3" + const val DEFAULT_WORD_SEPARATOR = "." + const val DEFAULT_EXTRA_SYMBOLS_MASK = "ds" + const val DEFAULT_MIN_WORD_LENGTH = 3 + const val DEFAULT_MAX_WORD_LENGTH = 9 + const val FALLBACK_ERROR_PASS = "42" + const val EXTRA_CHAR_PLACEHOLDER_DIGIT = 'd' + const val EXTRA_CHAR_PLACEHOLDER_SYMBOL = 's' + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/folderselect/SelectFolderActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/folderselect/SelectFolderActivity.kt index 910f7c27..1c6a236e 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/folderselect/SelectFolderActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/folderselect/SelectFolderActivity.kt @@ -15,49 +15,46 @@ import dev.msfjarvis.aps.data.repo.PasswordRepository import dev.msfjarvis.aps.ui.passwords.PASSWORD_FRAGMENT_TAG import dev.msfjarvis.aps.ui.passwords.PasswordStore - class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) { - private lateinit var passwordList: SelectFolderFragment + private lateinit var passwordList: SelectFolderFragment - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) - passwordList = SelectFolderFragment() - val args = Bundle() - args.putString(PasswordStore.REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath) + passwordList = SelectFolderFragment() + val args = Bundle() + args.putString(PasswordStore.REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath) - passwordList.arguments = args + passwordList.arguments = args - supportActionBar?.show() + supportActionBar?.show() - supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - supportFragmentManager.commit { - replace(R.id.pgp_handler_linearlayout, passwordList, PASSWORD_FRAGMENT_TAG) - } - } + supportFragmentManager.commit { replace(R.id.pgp_handler_linearlayout, passwordList, PASSWORD_FRAGMENT_TAG) } + } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.pgp_handler_select_folder, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - setResult(RESULT_CANCELED) - onBackPressed() - } - R.id.crypto_select -> selectFolder() - else -> return super.onOptionsItemSelected(item) - } - return true - } + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.pgp_handler_select_folder, menu) + return true + } - private fun selectFolder() { - intent.putExtra("SELECTED_FOLDER_PATH", passwordList.currentDir.absolutePath) - setResult(RESULT_OK, intent) - finish() + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + setResult(RESULT_CANCELED) + onBackPressed() + } + R.id.crypto_select -> selectFolder() + else -> return super.onOptionsItemSelected(item) } + return true + } + + private fun selectFolder() { + intent.putExtra("SELECTED_FOLDER_PATH", passwordList.currentDir.absolutePath) + setResult(RESULT_OK, intent) + finish() + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/folderselect/SelectFolderFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/folderselect/SelectFolderFragment.kt index 2a192e78..1905aab0 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/folderselect/SelectFolderFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/folderselect/SelectFolderFragment.kt @@ -26,56 +26,51 @@ import me.zhanghai.android.fastscroll.FastScrollerBuilder class SelectFolderFragment : Fragment(R.layout.password_recycler_view) { - private val binding by viewBinding(PasswordRecyclerViewBinding::bind) - private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter - private lateinit var listener: OnFragmentInteractionListener + private val binding by viewBinding(PasswordRecyclerViewBinding::bind) + private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter + private lateinit var listener: OnFragmentInteractionListener - private val model: SearchableRepositoryViewModel by activityViewModels() + private val model: SearchableRepositoryViewModel by activityViewModels() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.fab.hide() - recyclerAdapter = PasswordItemRecyclerAdapter() - .onItemClicked { _, item -> - listener.onFragmentInteraction(item) - } - binding.passRecycler.apply { - layoutManager = LinearLayoutManager(requireContext()) - itemAnimator = null - adapter = recyclerAdapter - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.fab.hide() + recyclerAdapter = PasswordItemRecyclerAdapter().onItemClicked { _, item -> listener.onFragmentInteraction(item) } + binding.passRecycler.apply { + layoutManager = LinearLayoutManager(requireContext()) + itemAnimator = null + adapter = recyclerAdapter + } - FastScrollerBuilder(binding.passRecycler).build() - registerForContextMenu(binding.passRecycler) + FastScrollerBuilder(binding.passRecycler).build() + registerForContextMenu(binding.passRecycler) - val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH)) - model.navigateTo(File(path), listMode = ListMode.DirectoriesOnly, pushPreviousLocation = false) - model.searchResult.observe(viewLifecycleOwner) { result -> - recyclerAdapter.submitList(result.passwordItems) - } - } + val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH)) + model.navigateTo(File(path), listMode = ListMode.DirectoriesOnly, pushPreviousLocation = false) + model.searchResult.observe(viewLifecycleOwner) { result -> recyclerAdapter.submitList(result.passwordItems) } + } - override fun onAttach(context: Context) { - super.onAttach(context) - runCatching { - listener = object : OnFragmentInteractionListener { - override fun onFragmentInteraction(item: PasswordItem) { - if (item.type == PasswordItem.TYPE_CATEGORY) { - model.navigateTo(item.file, listMode = ListMode.DirectoriesOnly) - (requireActivity() as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - } + override fun onAttach(context: Context) { + super.onAttach(context) + runCatching { + listener = + object : OnFragmentInteractionListener { + override fun onFragmentInteraction(item: PasswordItem) { + if (item.type == PasswordItem.TYPE_CATEGORY) { + model.navigateTo(item.file, listMode = ListMode.DirectoriesOnly) + (requireActivity() as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true) } - }.onFailure { - throw ClassCastException("$context must implement OnFragmentInteractionListener") + } } } + .onFailure { throw ClassCastException("$context must implement OnFragmentInteractionListener") } + } - val currentDir: File - get() = model.currentDir.value!! + val currentDir: File + get() = model.currentDir.value!! - interface OnFragmentInteractionListener { + interface OnFragmentInteractionListener { - fun onFragmentInteraction(item: PasswordItem) - } + fun onFragmentInteraction(item: PasswordItem) + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/git/base/BaseGitActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/git/base/BaseGitActivity.kt index a60be0d3..ac4c400d 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/git/base/BaseGitActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/git/base/BaseGitActivity.kt @@ -33,132 +33,130 @@ import net.schmizz.sshj.transport.TransportException import net.schmizz.sshj.userauth.UserAuthException /** - * Abstract [AppCompatActivity] that holds some information that is commonly shared across git-related - * tasks and makes sense to be held here. + * Abstract [AppCompatActivity] that holds some information that is commonly shared across + * git-related tasks and makes sense to be held here. */ abstract class BaseGitActivity : ContinuationContainerActivity() { - /** - * Enum of possible Git operations than can be run through [launchGitOperation]. - */ - enum class GitOp { + /** Enum of possible Git operations than can be run through [launchGitOperation]. */ + enum class GitOp { + BREAK_OUT_OF_DETACHED, + CLONE, + PULL, + PUSH, + RESET, + SYNC, + } - BREAK_OUT_OF_DETACHED, - CLONE, - PULL, - PUSH, - RESET, - SYNC, + /** + * Attempt to launch the requested Git operation. + * @param operation The type of git operation to launch + */ + suspend fun launchGitOperation(operation: GitOp): Result { + if (GitSettings.url == null) { + return Err(IllegalStateException("Git url is not set!")) } - - /** - * Attempt to launch the requested Git operation. - * @param operation The type of git operation to launch - */ - suspend fun launchGitOperation(operation: GitOp): Result { - if (GitSettings.url == null) { - return Err(IllegalStateException("Git url is not set!")) - } - if (operation == GitOp.SYNC && !GitSettings.useMultiplexing) { - // If the server does not support multiple SSH channels per connection, we cannot run - // a sync operation without reconnecting and thus break sync into its two parts. - return launchGitOperation(GitOp.PULL).andThen { launchGitOperation(GitOp.PUSH) } - } - val op = when (operation) { - GitOp.CLONE -> CloneOperation(this, GitSettings.url!!) - GitOp.PULL -> PullOperation(this, GitSettings.rebaseOnPull) - GitOp.PUSH -> PushOperation(this) - GitOp.SYNC -> SyncOperation(this, GitSettings.rebaseOnPull) - GitOp.BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(this) - GitOp.RESET -> ResetToRemoteOperation(this) - } - return op.executeAfterAuthentication(GitSettings.authMode).mapError(::transformGitError) + if (operation == GitOp.SYNC && !GitSettings.useMultiplexing) { + // If the server does not support multiple SSH channels per connection, we cannot run + // a sync operation without reconnecting and thus break sync into its two parts. + return launchGitOperation(GitOp.PULL).andThen { launchGitOperation(GitOp.PUSH) } } + val op = + when (operation) { + GitOp.CLONE -> CloneOperation(this, GitSettings.url!!) + GitOp.PULL -> PullOperation(this, GitSettings.rebaseOnPull) + GitOp.PUSH -> PushOperation(this) + GitOp.SYNC -> SyncOperation(this, GitSettings.rebaseOnPull) + GitOp.BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(this) + GitOp.RESET -> ResetToRemoteOperation(this) + } + return op.executeAfterAuthentication(GitSettings.authMode).mapError(::transformGitError) + } - fun finishOnSuccessHandler(@Suppress("UNUSED_PARAMETER") nothing: Unit) { - finish() - } + fun finishOnSuccessHandler(@Suppress("UNUSED_PARAMETER") nothing: Unit) { + finish() + } - suspend fun promptOnErrorHandler(err: Throwable, onPromptDone: () -> Unit = {}) { - val error = rootCauseException(err) - if (!isExplicitlyUserInitiatedError(error)) { - getEncryptedGitPrefs().edit { - remove(PreferenceKeys.HTTPS_PASSWORD) - } - sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) } - d(error) - withContext(Dispatchers.Main) { - MaterialAlertDialogBuilder(this@BaseGitActivity).run { - setTitle(resources.getString(R.string.jgit_error_dialog_title)) - setMessage(ErrorMessages[error]) - setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> } - setOnDismissListener { - onPromptDone() - } - show() - } - } - } else { - onPromptDone() + suspend fun promptOnErrorHandler(err: Throwable, onPromptDone: () -> Unit = {}) { + val error = rootCauseException(err) + if (!isExplicitlyUserInitiatedError(error)) { + getEncryptedGitPrefs().edit { remove(PreferenceKeys.HTTPS_PASSWORD) } + sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) } + d(error) + withContext(Dispatchers.Main) { + MaterialAlertDialogBuilder(this@BaseGitActivity).run { + setTitle(resources.getString(R.string.jgit_error_dialog_title)) + setMessage(ErrorMessages[error]) + setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> } + setOnDismissListener { onPromptDone() } + show() } + } + } else { + onPromptDone() } + } - /** - * Takes the result of [launchGitOperation] and applies any necessary transformations - * on the [throwable] returned from it - */ - private fun transformGitError(throwable: Throwable): Throwable { - val err = rootCauseException(throwable) - return when { - err.message?.contains("cannot open additional channels") == true -> { - GitSettings.useMultiplexing = false - SSHException(DisconnectReason.TOO_MANY_CONNECTIONS, "The server does not support multiple Git operations per SSH session. Please try again, a slower fallback mode will be used.") - } - err.message?.contains("int org.eclipse.jgit.lib.AnyObjectId.w1") == true -> { - IllegalStateException("Your local repository appears to be an incomplete Git clone, please delete and re-clone from settings") - } - err is TransportException && err.disconnectReason == DisconnectReason.HOST_KEY_NOT_VERIFIABLE -> { - SSHException(DisconnectReason.HOST_KEY_NOT_VERIFIABLE, - "WARNING: The remote host key has changed. If this is expected, please go to Git server settings and clear the saved host key." - ) - } - else -> { - err - } - } + /** + * Takes the result of [launchGitOperation] and applies any necessary transformations on the + * [throwable] returned from it + */ + private fun transformGitError(throwable: Throwable): Throwable { + val err = rootCauseException(throwable) + return when { + err.message?.contains("cannot open additional channels") == true -> { + GitSettings.useMultiplexing = false + SSHException( + DisconnectReason.TOO_MANY_CONNECTIONS, + "The server does not support multiple Git operations per SSH session. Please try again, a slower fallback mode will be used." + ) + } + err.message?.contains("int org.eclipse.jgit.lib.AnyObjectId.w1") == true -> { + IllegalStateException( + "Your local repository appears to be an incomplete Git clone, please delete and re-clone from settings" + ) + } + err is TransportException && err.disconnectReason == DisconnectReason.HOST_KEY_NOT_VERIFIABLE -> { + SSHException( + DisconnectReason.HOST_KEY_NOT_VERIFIABLE, + "WARNING: The remote host key has changed. If this is expected, please go to Git server settings and clear the saved host key." + ) + } + else -> { + err + } } + } - /** - * Check if a given [Throwable] is the result of an error caused by the user cancelling the - * operation. - */ - private fun isExplicitlyUserInitiatedError(throwable: Throwable): Boolean { - var cause: Throwable? = throwable - while (cause != null) { - if (cause is SSHException && - cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER) - return true - cause = cause.cause - } - return false + /** + * Check if a given [Throwable] is the result of an error caused by the user cancelling the + * operation. + */ + private fun isExplicitlyUserInitiatedError(throwable: Throwable): Boolean { + var cause: Throwable? = throwable + while (cause != null) { + if (cause is SSHException && cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER) return true + cause = cause.cause } + return false + } - /** - * Get the real root cause of a [Throwable] by traversing until known wrapping exceptions are no - * longer found. - */ - private fun rootCauseException(throwable: Throwable): Throwable { - var rootCause = throwable - // JGit's InvalidRemoteException and TransportException hide the more helpful SSHJ exceptions. - // Also, SSHJ's UserAuthException about exhausting available authentication methods hides - // more useful exceptions. - while ((rootCause is org.eclipse.jgit.errors.TransportException || - rootCause is org.eclipse.jgit.api.errors.TransportException || - rootCause is org.eclipse.jgit.api.errors.InvalidRemoteException || - (rootCause is UserAuthException && - rootCause.message == "Exhausted available authentication methods"))) { - rootCause = rootCause.cause ?: break - } - return rootCause + /** + * Get the real root cause of a [Throwable] by traversing until known wrapping exceptions are no + * longer found. + */ + private fun rootCauseException(throwable: Throwable): Throwable { + var rootCause = throwable + // JGit's InvalidRemoteException and TransportException hide the more helpful SSHJ + // exceptions. + // Also, SSHJ's UserAuthException about exhausting available authentication methods hides + // more useful exceptions. + while ((rootCause is org.eclipse.jgit.errors.TransportException || + rootCause is org.eclipse.jgit.api.errors.TransportException || + rootCause is org.eclipse.jgit.api.errors.InvalidRemoteException || + (rootCause is UserAuthException && rootCause.message == "Exhausted available authentication methods"))) { + rootCause = rootCause.cause ?: break } + return rootCause + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/git/config/GitConfigActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/git/config/GitConfigActivity.kt index 75787148..a1125ef0 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/git/config/GitConfigActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/git/config/GitConfigActivity.kt @@ -33,122 +33,113 @@ import org.eclipse.jgit.lib.RepositoryState class GitConfigActivity : BaseGitActivity() { - private val binding by viewBinding(ActivityGitConfigBinding::inflate) + private val binding by viewBinding(ActivityGitConfigBinding::inflate) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + supportActionBar?.setDisplayHomeAsUpEnabled(true) - if (GitSettings.authorName.isEmpty()) - binding.gitUserName.requestFocus() - else - binding.gitUserName.setText(GitSettings.authorName) - binding.gitUserEmail.setText(GitSettings.authorEmail) - setupTools() - binding.saveButton.setOnClickListener { - val email = binding.gitUserEmail.text.toString().trim() - val name = binding.gitUserName.text.toString().trim() - if (!email.matches(Patterns.EMAIL_ADDRESS.toRegex())) { - MaterialAlertDialogBuilder(this) - .setMessage(getString(R.string.invalid_email_dialog_text)) - .setPositiveButton(getString(R.string.dialog_ok), null) - .show() - } else { - GitSettings.authorEmail = email - GitSettings.authorName = name - Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show() - Handler(Looper.getMainLooper()).postDelayed(500) { finish() } - } - } + if (GitSettings.authorName.isEmpty()) binding.gitUserName.requestFocus() + else binding.gitUserName.setText(GitSettings.authorName) + binding.gitUserEmail.setText(GitSettings.authorEmail) + setupTools() + binding.saveButton.setOnClickListener { + val email = binding.gitUserEmail.text.toString().trim() + val name = binding.gitUserName.text.toString().trim() + if (!email.matches(Patterns.EMAIL_ADDRESS.toRegex())) { + MaterialAlertDialogBuilder(this) + .setMessage(getString(R.string.invalid_email_dialog_text)) + .setPositiveButton(getString(R.string.dialog_ok), null) + .show() + } else { + GitSettings.authorEmail = email + GitSettings.authorName = name + Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show() + Handler(Looper.getMainLooper()).postDelayed(500) { finish() } + } } + } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - onBackPressed() - true - } - else -> super.onOptionsItemSelected(item) - } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) } + } - /** - * Sets up the UI components of the tools section. - */ - private fun setupTools() { - val repo = PasswordRepository.getRepository(null) - if (repo != null) { - binding.gitHeadStatus.text = headStatusMsg(repo) - // enable the abort button only if we're rebasing or merging - val needsAbort = repo.repositoryState.isRebasing || repo.repositoryState == RepositoryState.MERGING - binding.gitAbortRebase.isEnabled = needsAbort - binding.gitAbortRebase.alpha = if (needsAbort) 1.0f else 0.5f - } - binding.gitLog.setOnClickListener { - runCatching { - startActivity(Intent(this, GitLogActivity::class.java)) - }.onFailure { ex -> - e(ex) { "Failed to start GitLogActivity" } - } - } - binding.gitAbortRebase.setOnClickListener { - lifecycleScope.launch { - launchGitOperation(GitOp.BREAK_OUT_OF_DETACHED).fold( - success = { - MaterialAlertDialogBuilder(this@GitConfigActivity).run { - setTitle(resources.getString(R.string.git_abort_and_push_title)) - setMessage(resources.getString( - R.string.git_break_out_of_detached_success, - GitSettings.branch, - "conflicting-${GitSettings.branch}-...", - )) - setOnDismissListener { finish() } - setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> } - show() - } - }, - failure = { err -> - promptOnErrorHandler(err) { - finish() - } - }, - ) - } - } - binding.gitResetToRemote.setOnClickListener { - lifecycleScope.launch { - launchGitOperation(GitOp.RESET).fold( - success = ::finishOnSuccessHandler, - failure = { err -> - promptOnErrorHandler(err) { - finish() - } - }, + /** Sets up the UI components of the tools section. */ + private fun setupTools() { + val repo = PasswordRepository.getRepository(null) + if (repo != null) { + binding.gitHeadStatus.text = headStatusMsg(repo) + // enable the abort button only if we're rebasing or merging + val needsAbort = repo.repositoryState.isRebasing || repo.repositoryState == RepositoryState.MERGING + binding.gitAbortRebase.isEnabled = needsAbort + binding.gitAbortRebase.alpha = if (needsAbort) 1.0f else 0.5f + } + binding.gitLog.setOnClickListener { + runCatching { startActivity(Intent(this, GitLogActivity::class.java)) }.onFailure { ex -> + e(ex) { "Failed to start GitLogActivity" } + } + } + binding.gitAbortRebase.setOnClickListener { + lifecycleScope.launch { + launchGitOperation(GitOp.BREAK_OUT_OF_DETACHED) + .fold( + success = { + MaterialAlertDialogBuilder(this@GitConfigActivity).run { + setTitle(resources.getString(R.string.git_abort_and_push_title)) + setMessage( + resources.getString( + R.string.git_break_out_of_detached_success, + GitSettings.branch, + "conflicting-${GitSettings.branch}-...", + ) ) - } - } + setOnDismissListener { finish() } + setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> } + show() + } + }, + failure = { err -> promptOnErrorHandler(err) { finish() } }, + ) + } + } + binding.gitResetToRemote.setOnClickListener { + lifecycleScope.launch { + launchGitOperation(GitOp.RESET) + .fold( + success = ::finishOnSuccessHandler, + failure = { err -> promptOnErrorHandler(err) { finish() } }, + ) + } } + } - /** - * Returns a user-friendly message about the current state of HEAD. - * - * The state is recognized to be either pointing to a branch or detached. - */ - private fun headStatusMsg(repo: Repository): String { - return runCatching { - val headRef = repo.getRef(Constants.HEAD) - if (headRef.isSymbolic) { - val branchName = headRef.target.name - val shortBranchName = Repository.shortenRefName(branchName) - getString(R.string.git_head_on_branch, shortBranchName) - } else { - val commitHash = headRef.objectId.abbreviate(8).name() - getString(R.string.git_head_detached, commitHash) - } - }.getOrElse { ex -> - e(ex) { "Error getting HEAD reference" } - getString(R.string.git_head_missing) - } + /** + * Returns a user-friendly message about the current state of HEAD. + * + * The state is recognized to be either pointing to a branch or detached. + */ + private fun headStatusMsg(repo: Repository): String { + return runCatching { + val headRef = repo.getRef(Constants.HEAD) + if (headRef.isSymbolic) { + val branchName = headRef.target.name + val shortBranchName = Repository.shortenRefName(branchName) + getString(R.string.git_head_on_branch, shortBranchName) + } else { + val commitHash = headRef.objectId.abbreviate(8).name() + getString(R.string.git_head_detached, commitHash) + } } + .getOrElse { ex -> + e(ex) { "Error getting HEAD reference" } + getString(R.string.git_head_missing) + } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/git/config/GitServerConfigActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/git/config/GitServerConfigActivity.kt index 7b34a686..56d9c043 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/git/config/GitServerConfigActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/git/config/GitServerConfigActivity.kt @@ -41,233 +41,226 @@ import kotlinx.coroutines.withContext */ class GitServerConfigActivity : BaseGitActivity() { - private val binding by viewBinding(ActivityGitCloneBinding::inflate) + private val binding by viewBinding(ActivityGitCloneBinding::inflate) - private lateinit var newAuthMode: AuthMode + private lateinit var newAuthMode: AuthMode - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val isClone = intent?.extras?.getBoolean("cloning") ?: false - if (isClone) { - binding.saveButton.text = getString(R.string.clone_button) - } - setContentView(binding.root) - supportActionBar?.setDisplayHomeAsUpEnabled(true) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val isClone = intent?.extras?.getBoolean("cloning") ?: false + if (isClone) { + binding.saveButton.text = getString(R.string.clone_button) + } + setContentView(binding.root) + supportActionBar?.setDisplayHomeAsUpEnabled(true) - newAuthMode = GitSettings.authMode + newAuthMode = GitSettings.authMode - binding.authModeGroup.apply { - when (newAuthMode) { - AuthMode.SshKey -> check(binding.authModeSshKey.id) - AuthMode.Password -> check(binding.authModePassword.id) - AuthMode.OpenKeychain -> check(binding.authModeOpenKeychain.id) - AuthMode.None -> check(View.NO_ID) - } - setOnCheckedChangeListener { _, checkedId -> - when (checkedId) { - binding.authModeSshKey.id -> newAuthMode = AuthMode.SshKey - binding.authModeOpenKeychain.id -> newAuthMode = AuthMode.OpenKeychain - binding.authModePassword.id -> newAuthMode = AuthMode.Password - View.NO_ID -> newAuthMode = AuthMode.None - } - } + binding.authModeGroup.apply { + when (newAuthMode) { + AuthMode.SshKey -> check(binding.authModeSshKey.id) + AuthMode.Password -> check(binding.authModePassword.id) + AuthMode.OpenKeychain -> check(binding.authModeOpenKeychain.id) + AuthMode.None -> check(View.NO_ID) + } + setOnCheckedChangeListener { _, checkedId -> + when (checkedId) { + binding.authModeSshKey.id -> newAuthMode = AuthMode.SshKey + binding.authModeOpenKeychain.id -> newAuthMode = AuthMode.OpenKeychain + binding.authModePassword.id -> newAuthMode = AuthMode.Password + View.NO_ID -> newAuthMode = AuthMode.None } + } + } - binding.serverUrl.setText(GitSettings.url.also { - if (it.isNullOrEmpty()) return@also - setAuthModes(it.startsWith("http://") || it.startsWith("https://")) - }) - binding.serverBranch.setText(GitSettings.branch) + binding.serverUrl.setText( + GitSettings.url.also { + if (it.isNullOrEmpty()) return@also + setAuthModes(it.startsWith("http://") || it.startsWith("https://")) + } + ) + binding.serverBranch.setText(GitSettings.branch) - binding.serverUrl.doOnTextChanged { text, _, _, _ -> - if (text.isNullOrEmpty()) return@doOnTextChanged - setAuthModes(text.startsWith("http://") || text.startsWith("https://")) - } + binding.serverUrl.doOnTextChanged { text, _, _, _ -> + if (text.isNullOrEmpty()) return@doOnTextChanged + setAuthModes(text.startsWith("http://") || text.startsWith("https://")) + } - binding.clearHostKeyButton.isVisible = GitSettings.hasSavedHostKey() - binding.clearHostKeyButton.setOnClickListener { - GitSettings.clearSavedHostKey() - Snackbar.make(binding.root, getString(R.string.clear_saved_host_key_success), Snackbar.LENGTH_LONG).show() - it.isVisible = false + binding.clearHostKeyButton.isVisible = GitSettings.hasSavedHostKey() + binding.clearHostKeyButton.setOnClickListener { + GitSettings.clearSavedHostKey() + Snackbar.make(binding.root, getString(R.string.clear_saved_host_key_success), Snackbar.LENGTH_LONG).show() + it.isVisible = false + } + binding.saveButton.setOnClickListener { + val newUrl = binding.serverUrl.text.toString().trim() + // If url is of type john_doe@example.org:12435/path/to/repo, then not adding `ssh://` + // in the beginning will cause the port to be seen as part of the path. Let users know + // about it and offer a quickfix. + if (newUrl.contains(PORT_REGEX)) { + if (newUrl.startsWith("https://")) { + BasicBottomSheet.Builder(this) + .setTitleRes(R.string.https_scheme_with_port_title) + .setMessageRes(R.string.https_scheme_with_port_message) + .setPositiveButtonClickListener { binding.serverUrl.setText(newUrl.replace(PORT_REGEX, "/")) } + .build() + .show(supportFragmentManager, "SSH_SCHEME_WARNING") + return@setOnClickListener + } else if (!newUrl.startsWith("ssh://")) { + BasicBottomSheet.Builder(this) + .setTitleRes(R.string.ssh_scheme_needed_title) + .setMessageRes(R.string.ssh_scheme_needed_message) + .setPositiveButtonClickListener { @Suppress("SetTextI18n") binding.serverUrl.setText("ssh://$newUrl") } + .build() + .show(supportFragmentManager, "SSH_SCHEME_WARNING") + return@setOnClickListener } - binding.saveButton.setOnClickListener { - val newUrl = binding.serverUrl.text.toString().trim() - // If url is of type john_doe@example.org:12435/path/to/repo, then not adding `ssh://` - // in the beginning will cause the port to be seen as part of the path. Let users know - // about it and offer a quickfix. - if (newUrl.contains(PORT_REGEX)) { - if (newUrl.startsWith("https://")) { - BasicBottomSheet.Builder(this) - .setTitleRes(R.string.https_scheme_with_port_title) - .setMessageRes(R.string.https_scheme_with_port_message) - .setPositiveButtonClickListener { - binding.serverUrl.setText(newUrl.replace(PORT_REGEX, "/")) - } - .build() - .show(supportFragmentManager, "SSH_SCHEME_WARNING") - return@setOnClickListener - } else if (!newUrl.startsWith("ssh://")) { - BasicBottomSheet.Builder(this) - .setTitleRes(R.string.ssh_scheme_needed_title) - .setMessageRes(R.string.ssh_scheme_needed_message) - .setPositiveButtonClickListener { - @Suppress("SetTextI18n") - binding.serverUrl.setText("ssh://$newUrl") - } - .build() - .show(supportFragmentManager, "SSH_SCHEME_WARNING") - return@setOnClickListener - } - } - when (val updateResult = GitSettings.updateConnectionSettingsIfValid( - newAuthMode = newAuthMode, - newUrl = binding.serverUrl.text.toString().trim(), - newBranch = binding.serverBranch.text.toString().trim())) { - GitSettings.UpdateConnectionSettingsResult.FailedToParseUrl -> { - Snackbar.make(binding.root, getString(R.string.git_server_config_save_error), Snackbar.LENGTH_LONG).show() - } - - is GitSettings.UpdateConnectionSettingsResult.MissingUsername -> { - when (updateResult.newProtocol) { - Protocol.Https -> - BasicBottomSheet.Builder(this) - .setTitleRes(R.string.ssh_scheme_needed_title) - .setMessageRes(R.string.git_server_config_save_missing_username_https) - .setPositiveButtonClickListener { - } - .build() - .show(supportFragmentManager, "HTTPS_MISSING_USERNAME") - Protocol.Ssh -> - BasicBottomSheet.Builder(this) - .setTitleRes(R.string.ssh_scheme_needed_title) - .setMessageRes(R.string.git_server_config_save_missing_username_ssh) - .setPositiveButtonClickListener { - } - .build() - .show(supportFragmentManager, "SSH_MISSING_USERNAME") - } - } - GitSettings.UpdateConnectionSettingsResult.Valid -> { - if (isClone && PasswordRepository.getRepository(null) == null) - PasswordRepository.initialize() - if (!isClone) { - Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show() - Handler(Looper.getMainLooper()).postDelayed(500) { finish() } - } else { - cloneRepository() - } - } - is GitSettings.UpdateConnectionSettingsResult.AuthModeMismatch -> { - val message = getString( - R.string.git_server_config_save_auth_mode_mismatch, - updateResult.newProtocol, - updateResult.validModes.joinToString(", "), - ) - Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() - } - } + } + when (val updateResult = + GitSettings.updateConnectionSettingsIfValid( + newAuthMode = newAuthMode, + newUrl = binding.serverUrl.text.toString().trim(), + newBranch = binding.serverBranch.text.toString().trim() + ) + ) { + GitSettings.UpdateConnectionSettingsResult.FailedToParseUrl -> { + Snackbar.make(binding.root, getString(R.string.git_server_config_save_error), Snackbar.LENGTH_LONG).show() + } + is GitSettings.UpdateConnectionSettingsResult.MissingUsername -> { + when (updateResult.newProtocol) { + Protocol.Https -> + BasicBottomSheet.Builder(this) + .setTitleRes(R.string.ssh_scheme_needed_title) + .setMessageRes(R.string.git_server_config_save_missing_username_https) + .setPositiveButtonClickListener {} + .build() + .show(supportFragmentManager, "HTTPS_MISSING_USERNAME") + Protocol.Ssh -> + BasicBottomSheet.Builder(this) + .setTitleRes(R.string.ssh_scheme_needed_title) + .setMessageRes(R.string.git_server_config_save_missing_username_ssh) + .setPositiveButtonClickListener {} + .build() + .show(supportFragmentManager, "SSH_MISSING_USERNAME") + } } + GitSettings.UpdateConnectionSettingsResult.Valid -> { + if (isClone && PasswordRepository.getRepository(null) == null) PasswordRepository.initialize() + if (!isClone) { + Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT) + .show() + Handler(Looper.getMainLooper()).postDelayed(500) { finish() } + } else { + cloneRepository() + } + } + is GitSettings.UpdateConnectionSettingsResult.AuthModeMismatch -> { + val message = + getString( + R.string.git_server_config_save_auth_mode_mismatch, + updateResult.newProtocol, + updateResult.validModes.joinToString(", "), + ) + Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() + } + } } + } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - onBackPressed() - true - } - else -> super.onOptionsItemSelected(item) - } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) } + } - private fun setAuthModes(isHttps: Boolean) = with(binding) { - if (isHttps) { - authModeSshKey.isVisible = false - authModeOpenKeychain.isVisible = false - authModePassword.isVisible = true - if (authModeGroup.checkedChipId != authModePassword.id) - authModeGroup.check(View.NO_ID) - } else { - authModeSshKey.isVisible = true - authModeOpenKeychain.isVisible = true - authModePassword.isVisible = true - if (authModeGroup.checkedChipId == View.NO_ID) - authModeGroup.check(authModeSshKey.id) - } + private fun setAuthModes(isHttps: Boolean) = + with(binding) { + if (isHttps) { + authModeSshKey.isVisible = false + authModeOpenKeychain.isVisible = false + authModePassword.isVisible = true + if (authModeGroup.checkedChipId != authModePassword.id) authModeGroup.check(View.NO_ID) + } else { + authModeSshKey.isVisible = true + authModeOpenKeychain.isVisible = true + authModePassword.isVisible = true + if (authModeGroup.checkedChipId == View.NO_ID) authModeGroup.check(authModeSshKey.id) + } } - /** - * Clones the repository, the directory exists, deletes it - */ - private fun cloneRepository() { - val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory()) - val localDirFiles = localDir.listFiles() ?: emptyArray() - // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder - if (localDir.exists() && localDirFiles.isNotEmpty() && - !(localDirFiles.size == 1 && localDirFiles[0].name == ".git")) { - MaterialAlertDialogBuilder(this) - .setTitle(R.string.dialog_delete_title) - .setMessage(resources.getString(R.string.dialog_delete_msg, localDir.toString())) - .setCancelable(false) - .setPositiveButton(R.string.dialog_delete) { dialog, _ -> - runCatching { - lifecycleScope.launch { - val snackbar = snackbar(message = getString(R.string.delete_directory_progress_text), length = Snackbar.LENGTH_INDEFINITE) - withContext(Dispatchers.IO) { - localDir.deleteRecursively() - } - snackbar.dismiss() - launchGitOperation(GitOp.CLONE).fold( - success = { - setResult(RESULT_OK) - finish() - }, - failure = { err -> - promptOnErrorHandler(err) { - finish() - } - } - ) - } - }.onFailure { e -> - e.printStackTrace() - MaterialAlertDialogBuilder(this).setMessage(e.message).show() - } - dialog.cancel() - } - .setNegativeButton(R.string.dialog_do_not_delete) { dialog, _ -> - dialog.cancel() - } - .show() - } else { - runCatching { - // Silently delete & replace the lone .git folder if it exists - if (localDir.exists() && localDirFiles.size == 1 && localDirFiles[0].name == ".git") { - localDir.deleteRecursively() - } - }.onFailure { e -> - e(e) - MaterialAlertDialogBuilder(this).setMessage(e.message).show() - } + /** Clones the repository, the directory exists, deletes it */ + private fun cloneRepository() { + val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory()) + val localDirFiles = localDir.listFiles() ?: emptyArray() + // Warn if non-empty folder unless it's a just-initialized store that has just a .git folder + if (localDir.exists() && localDirFiles.isNotEmpty() && !(localDirFiles.size == 1 && localDirFiles[0].name == ".git") + ) { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.dialog_delete_title) + .setMessage(resources.getString(R.string.dialog_delete_msg, localDir.toString())) + .setCancelable(false) + .setPositiveButton(R.string.dialog_delete) { dialog, _ -> + runCatching { lifecycleScope.launch { - launchGitOperation(GitOp.CLONE).fold( - success = { - setResult(RESULT_OK) - finish() - }, - failure = { promptOnErrorHandler(it) }, + val snackbar = + snackbar( + message = getString(R.string.delete_directory_progress_text), + length = Snackbar.LENGTH_INDEFINITE ) + withContext(Dispatchers.IO) { localDir.deleteRecursively() } + snackbar.dismiss() + launchGitOperation(GitOp.CLONE) + .fold( + success = { + setResult(RESULT_OK) + finish() + }, + failure = { err -> promptOnErrorHandler(err) { finish() } } + ) + } + } + .onFailure { e -> + e.printStackTrace() + MaterialAlertDialogBuilder(this).setMessage(e.message).show() } + dialog.cancel() + } + .setNegativeButton(R.string.dialog_do_not_delete) { dialog, _ -> dialog.cancel() } + .show() + } else { + runCatching { + // Silently delete & replace the lone .git folder if it exists + if (localDir.exists() && localDirFiles.size == 1 && localDirFiles[0].name == ".git") { + localDir.deleteRecursively() } + } + .onFailure { e -> + e(e) + MaterialAlertDialogBuilder(this).setMessage(e.message).show() + } + lifecycleScope.launch { + launchGitOperation(GitOp.CLONE) + .fold( + success = { + setResult(RESULT_OK) + finish() + }, + failure = { promptOnErrorHandler(it) }, + ) + } } + } - companion object { + companion object { - private val PORT_REGEX = ":[0-9]{1,5}/".toRegex() + private val PORT_REGEX = ":[0-9]{1,5}/".toRegex() - fun createCloneIntent(context: Context): Intent { - return Intent(context, GitServerConfigActivity::class.java).apply { - putExtra("cloning", true) - } - } + fun createCloneIntent(context: Context): Intent { + return Intent(context, GitServerConfigActivity::class.java).apply { putExtra("cloning", true) } } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/git/log/GitLogActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/git/log/GitLogActivity.kt index a3415805..4265717d 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/git/log/GitLogActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/git/log/GitLogActivity.kt @@ -20,30 +20,30 @@ import dev.msfjarvis.aps.util.extensions.viewBinding */ class GitLogActivity : BaseGitActivity() { - private val binding by viewBinding(ActivityGitLogBinding::inflate) + private val binding by viewBinding(ActivityGitLogBinding::inflate) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - createRecyclerView() - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + createRecyclerView() + } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - finish() - true - } - else -> super.onOptionsItemSelected(item) - } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + finish() + true + } + else -> super.onOptionsItemSelected(item) } + } - private fun createRecyclerView() { - binding.gitLogRecyclerView.apply { - setHasFixedSize(true) - addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL)) - adapter = GitLogAdapter() - } + private fun createRecyclerView() { + binding.gitLogRecyclerView.apply { + setHasFixedSize(true) + addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL)) + adapter = GitLogAdapter() } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/git/log/GitLogAdapter.kt b/app/src/main/java/dev/msfjarvis/aps/ui/git/log/GitLogAdapter.kt index 123c2af7..4b29542e 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/git/log/GitLogAdapter.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/git/log/GitLogAdapter.kt @@ -16,43 +16,42 @@ import java.text.DateFormat import java.util.Date private fun shortHash(hash: String): String { - return hash.substring(0 until 8) + return hash.substring(0 until 8) } private fun stringFrom(date: Date): String { - return DateFormat.getDateTimeInstance().format(date) + return DateFormat.getDateTimeInstance().format(date) } -/** - * @see GitLogActivity - */ +/** @see GitLogActivity */ class GitLogAdapter : RecyclerView.Adapter() { - private val model = GitLogModel() + private val model = GitLogModel() - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(parent.context) - val binding = GitLogRowLayoutBinding.inflate(inflater, parent, false) - return ViewHolder(binding) - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = GitLogRowLayoutBinding.inflate(inflater, parent, false) + return ViewHolder(binding) + } - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - val commit = model.get(position) - if (commit == null) { - e { "There is no git commit for view holder at position $position." } - return - } - viewHolder.bind(commit) + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + val commit = model.get(position) + if (commit == null) { + e { "There is no git commit for view holder at position $position." } + return } + viewHolder.bind(commit) + } - override fun getItemCount() = model.size + override fun getItemCount() = model.size - class ViewHolder(private val binding: GitLogRowLayoutBinding) : RecyclerView.ViewHolder(binding.root) { + class ViewHolder(private val binding: GitLogRowLayoutBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(commit: GitCommit) = with(binding) { - gitLogRowMessage.text = commit.shortMessage - gitLogRowHash.text = shortHash(commit.hash) - gitLogRowTime.text = stringFrom(commit.time) - } - } + fun bind(commit: GitCommit) = + with(binding) { + gitLogRowMessage.text = commit.shortMessage + gitLogRowHash.text = shortHash(commit.hash) + gitLogRowTime.text = stringFrom(commit.time) + } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/main/LaunchActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/main/LaunchActivity.kt index 2748e9c8..e3c59a50 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/main/LaunchActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/main/LaunchActivity.kt @@ -18,46 +18,46 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys class LaunchActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val prefs = sharedPrefs - if (prefs.getBoolean(PreferenceKeys.BIOMETRIC_AUTH, false)) { - BiometricAuthenticator.authenticate(this) { - when (it) { - is BiometricAuthenticator.Result.Success -> { - startTargetActivity(false) - } - is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> { - prefs.edit { remove(PreferenceKeys.BIOMETRIC_AUTH) } - startTargetActivity(false) - } - is BiometricAuthenticator.Result.Failure, BiometricAuthenticator.Result.Cancelled -> { - finish() - } - } - } - } else { - startTargetActivity(true) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val prefs = sharedPrefs + if (prefs.getBoolean(PreferenceKeys.BIOMETRIC_AUTH, false)) { + BiometricAuthenticator.authenticate(this) { + when (it) { + is BiometricAuthenticator.Result.Success -> { + startTargetActivity(false) + } + is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> { + prefs.edit { remove(PreferenceKeys.BIOMETRIC_AUTH) } + startTargetActivity(false) + } + is BiometricAuthenticator.Result.Failure, BiometricAuthenticator.Result.Cancelled -> { + finish() + } } + } + } else { + startTargetActivity(true) } + } - private fun startTargetActivity(noAuth: Boolean) { - val intentToStart = if (intent.action == ACTION_DECRYPT_PASS) - Intent(this, DecryptActivity::class.java).apply { - putExtra("NAME", intent.getStringExtra("NAME")) - putExtra("FILE_PATH", intent.getStringExtra("FILE_PATH")) - putExtra("REPO_PATH", intent.getStringExtra("REPO_PATH")) - putExtra("LAST_CHANGED_TIMESTAMP", intent.getLongExtra("LAST_CHANGED_TIMESTAMP", 0L)) - } - else - Intent(this, PasswordStore::class.java) - startActivity(intentToStart) + private fun startTargetActivity(noAuth: Boolean) { + val intentToStart = + if (intent.action == ACTION_DECRYPT_PASS) + Intent(this, DecryptActivity::class.java).apply { + putExtra("NAME", intent.getStringExtra("NAME")) + putExtra("FILE_PATH", intent.getStringExtra("FILE_PATH")) + putExtra("REPO_PATH", intent.getStringExtra("REPO_PATH")) + putExtra("LAST_CHANGED_TIMESTAMP", intent.getLongExtra("LAST_CHANGED_TIMESTAMP", 0L)) + } + else Intent(this, PasswordStore::class.java) + startActivity(intentToStart) - Handler(Looper.getMainLooper()).postDelayed({ finish() }, if (noAuth) 0L else 500L) - } + Handler(Looper.getMainLooper()).postDelayed({ finish() }, if (noAuth) 0L else 500L) + } - companion object { + companion object { - const val ACTION_DECRYPT_PASS = "DECRYPT_PASS" - } + const val ACTION_DECRYPT_PASS = "DECRYPT_PASS" + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/activity/OnboardingActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/activity/OnboardingActivity.kt index ec1d4e62..31ca362c 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/activity/OnboardingActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/activity/OnboardingActivity.kt @@ -11,16 +11,16 @@ import dev.msfjarvis.aps.R class OnboardingActivity : AppCompatActivity(R.layout.activity_onboarding) { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.hide() - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar?.hide() + } - override fun onBackPressed() { - if (supportFragmentManager.backStackEntryCount == 0) { - finishAffinity() - } else { - super.onBackPressed() - } + override fun onBackPressed() { + if (supportFragmentManager.backStackEntryCount == 0) { + finishAffinity() + } else { + super.onBackPressed() } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/CloneFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/CloneFragment.kt index b2092ab0..12a97d45 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/CloneFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/CloneFragment.kt @@ -22,37 +22,34 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys class CloneFragment : Fragment(R.layout.fragment_clone) { - private val binding by viewBinding(FragmentCloneBinding::bind) + private val binding by viewBinding(FragmentCloneBinding::bind) - private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } + private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } - private val cloneAction = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK) { - settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } - finish() - } + private val cloneAction = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK) { + settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } + finish() + } } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.cloneRemote.setOnClickListener { - cloneToHiddenDir() - } - binding.createLocal.setOnClickListener { - parentFragmentManager.performTransactionWithBackStack(RepoLocationFragment.newInstance()) - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.cloneRemote.setOnClickListener { cloneToHiddenDir() } + binding.createLocal.setOnClickListener { + parentFragmentManager.performTransactionWithBackStack(RepoLocationFragment.newInstance()) } + } - /** - * Clones a remote Git repository to the app's private directory - */ - private fun cloneToHiddenDir() { - settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, false) } - cloneAction.launch(GitServerConfigActivity.createCloneIntent(requireContext())) - } + /** Clones a remote Git repository to the app's private directory */ + private fun cloneToHiddenDir() { + settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, false) } + cloneAction.launch(GitServerConfigActivity.createCloneIntent(requireContext())) + } - companion object { + companion object { - fun newInstance(): CloneFragment = CloneFragment() - } + fun newInstance(): CloneFragment = CloneFragment() + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/KeySelectionFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/KeySelectionFragment.kt index 6ebde7c1..44a6d6c5 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/KeySelectionFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/KeySelectionFragment.kt @@ -30,37 +30,37 @@ import me.msfjarvis.openpgpktx.util.OpenPgpApi class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) { - private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } - private val binding by viewBinding(FragmentKeySelectionBinding::bind) + private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } + private val binding by viewBinding(FragmentKeySelectionBinding::bind) - private val gpgKeySelectAction = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK) { - result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> - lifecycleScope.launch { - withContext(Dispatchers.IO) { - val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id") - gpgIdentifierFile.writeText((keyIds + "").joinToString("\n")) - } - settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } - requireActivity().commitChange(getString( - R.string.git_commit_gpg_id, - getString(R.string.app_name) - )) - } + private val gpgKeySelectAction = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK) { + result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds -> + lifecycleScope.launch { + withContext(Dispatchers.IO) { + val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id") + gpgIdentifierFile.writeText((keyIds + "").joinToString("\n")) } - } else { - throw IllegalStateException("Failed to initialize repository state.") + settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) } + requireActivity().commitChange(getString(R.string.git_commit_gpg_id, getString(R.string.app_name))) + } } - finish() + } else { + throw IllegalStateException("Failed to initialize repository state.") + } + finish() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.selectKey.setOnClickListener { gpgKeySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java)) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.selectKey.setOnClickListener { + gpgKeySelectAction.launch(Intent(requireContext(), GetKeyIdsActivity::class.java)) } + } - companion object { + companion object { - fun newInstance() = KeySelectionFragment() - } + fun newInstance() = KeySelectionFragment() + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt index 159e0664..9adbc3e8 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/RepoLocationFragment.kt @@ -35,159 +35,155 @@ import java.io.File class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) { - private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } - private val directorySelectIntent by lazy(LazyThreadSafetyMode.NONE) { Intent(requireContext(), DirectorySelectionActivity::class.java) } - private val binding by viewBinding(FragmentRepoLocationBinding::bind) - private val sortOrder: PasswordSortOrder - get() = PasswordSortOrder.getSortOrder(settings) - - private val repositoryInitAction = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK) { - initializeRepositoryInfo() - } + private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs } + private val directorySelectIntent by lazy(LazyThreadSafetyMode.NONE) { + Intent(requireContext(), DirectorySelectionActivity::class.java) + } + private val binding by viewBinding(FragmentRepoLocationBinding::bind) + private val sortOrder: PasswordSortOrder + get() = PasswordSortOrder.getSortOrder(settings) + + private val repositoryInitAction = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK) { + initializeRepositoryInfo() + } } - private val externalDirectorySelectAction = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK) { - if (checkExternalDirectory()) { - finish() - } else { - createRepository() - } + private val externalDirectorySelectAction = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK) { + if (checkExternalDirectory()) { + finish() + } else { + createRepository() } + } } - private val externalDirPermGrantedAction = createPermGrantedAction { - externalDirectorySelectAction.launch(directorySelectIntent) - } + private val externalDirPermGrantedAction = createPermGrantedAction { + externalDirectorySelectAction.launch(directorySelectIntent) + } - private val repositoryUsePermGrantedAction = createPermGrantedAction { - initializeRepositoryInfo() - } + private val repositoryUsePermGrantedAction = createPermGrantedAction { initializeRepositoryInfo() } - private val repositoryChangePermGrantedAction = createPermGrantedAction { - repositoryInitAction.launch(directorySelectIntent) - } + private val repositoryChangePermGrantedAction = createPermGrantedAction { + repositoryInitAction.launch(directorySelectIntent) + } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.hidden.setOnClickListener { - createRepoInHiddenDir() - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.hidden.setOnClickListener { createRepoInHiddenDir() } - binding.sdcard.setOnClickListener { - createRepoFromExternalDir() - } - } + binding.sdcard.setOnClickListener { createRepoFromExternalDir() } + } - /** - * Initializes an empty repository in the app's private directory - */ - private fun createRepoInHiddenDir() { - settings.edit { - putBoolean(PreferenceKeys.GIT_EXTERNAL, false) - remove(PreferenceKeys.GIT_EXTERNAL_REPO) - } - initializeRepositoryInfo() + /** Initializes an empty repository in the app's private directory */ + private fun createRepoInHiddenDir() { + settings.edit { + putBoolean(PreferenceKeys.GIT_EXTERNAL, false) + remove(PreferenceKeys.GIT_EXTERNAL_REPO) } - - /** - * Initializes an empty repository in a selected directory if one does not already exist - */ - private fun createRepoFromExternalDir() { - settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, true) } - val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) - if (externalRepo == null) { - if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - externalDirPermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } else { - // Unlikely we have storage permissions without user ever selecting a directory, - // but let's not assume. - externalDirectorySelectAction.launch(directorySelectIntent) - } - } else { - MaterialAlertDialogBuilder(requireActivity()) - .setTitle(resources.getString(R.string.directory_selected_title)) - .setMessage(resources.getString(R.string.directory_selected_message, externalRepo)) - .setPositiveButton(resources.getString(R.string.use)) { _, _ -> - if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - repositoryUsePermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } else { - initializeRepositoryInfo() - } - } - .setNegativeButton(resources.getString(R.string.change)) { _, _ -> - if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - repositoryChangePermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } else { - repositoryInitAction.launch(directorySelectIntent) - } - } - .show() + initializeRepositoryInfo() + } + + /** Initializes an empty repository in a selected directory if one does not already exist */ + private fun createRepoFromExternalDir() { + settings.edit { putBoolean(PreferenceKeys.GIT_EXTERNAL, true) } + val externalRepo = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) + if (externalRepo == null) { + if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + externalDirPermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } else { + // Unlikely we have storage permissions without user ever selecting a directory, + // but let's not assume. + externalDirectorySelectAction.launch(directorySelectIntent) + } + } else { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(resources.getString(R.string.directory_selected_title)) + .setMessage(resources.getString(R.string.directory_selected_message, externalRepo)) + .setPositiveButton(resources.getString(R.string.use)) { _, _ -> + if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + repositoryUsePermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } else { + initializeRepositoryInfo() + } } - } - - private fun checkExternalDirectory(): Boolean { - if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) && - settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) != null) { - val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) - val dir = externalRepoPath?.let { File(it) } - if (dir != null && // The directory could be opened - dir.exists() && // The directory exists - dir.isDirectory && // The directory, is really a directory - dir.listFilesRecursively().isNotEmpty() && // The directory contains files - // The directory contains a non-zero number of password files - PasswordRepository.getPasswords(dir, PasswordRepository.getRepositoryDirectory(), sortOrder).isNotEmpty() - ) { - PasswordRepository.closeRepository() - return true - } + .setNegativeButton(resources.getString(R.string.change)) { _, _ -> + if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + repositoryChangePermGrantedAction.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } else { + repositoryInitAction.launch(directorySelectIntent) + } } - return false + .show() } - - private fun createRepository() { - val localDir = PasswordRepository.getRepositoryDirectory() - runCatching { - check(localDir.exists() || localDir.mkdir()) { "Failed to create directory!" } - PasswordRepository.createRepository(localDir) - if (!PasswordRepository.isInitialized) { - PasswordRepository.initialize() - } - parentFragmentManager.performTransactionWithBackStack(KeySelectionFragment.newInstance()) - }.onFailure { e -> - e(e) - if (!localDir.delete()) { - d { "Failed to delete local repository: $localDir" } - } - finish() - } + } + + private fun checkExternalDirectory(): Boolean { + if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) && + settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) != null + ) { + val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) + val dir = externalRepoPath?.let { File(it) } + if (dir != null && // The directory could be opened + dir.exists() && // The directory exists + dir.isDirectory && // The directory, is really a directory + dir.listFilesRecursively().isNotEmpty() && // The directory contains files + // The directory contains a non-zero number of password files + PasswordRepository.getPasswords(dir, PasswordRepository.getRepositoryDirectory(), sortOrder).isNotEmpty() + ) { + PasswordRepository.closeRepository() + return true + } } - - private fun initializeRepositoryInfo() { - val externalRepo = settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) - val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) - if (externalRepo && !isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - return - } - if (externalRepo && externalRepoPath != null) { - if (checkExternalDirectory()) { - finish() - return - } - } - createRepository() + return false + } + + private fun createRepository() { + val localDir = PasswordRepository.getRepositoryDirectory() + runCatching { + check(localDir.exists() || localDir.mkdir()) { "Failed to create directory!" } + PasswordRepository.createRepository(localDir) + if (!PasswordRepository.isInitialized) { + PasswordRepository.initialize() + } + parentFragmentManager.performTransactionWithBackStack(KeySelectionFragment.newInstance()) } - - private fun createPermGrantedAction(block: () -> Unit) = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> - if (granted) { - block.invoke() - } + .onFailure { e -> + e(e) + if (!localDir.delete()) { + d { "Failed to delete local repository: $localDir" } } + finish() + } + } + + private fun initializeRepositoryInfo() { + val externalRepo = settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) + val externalRepoPath = settings.getString(PreferenceKeys.GIT_EXTERNAL_REPO) + if (externalRepo && !isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + return + } + if (externalRepo && externalRepoPath != null) { + if (checkExternalDirectory()) { + finish() + return + } + } + createRepository() + } + + private fun createPermGrantedAction(block: () -> Unit) = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + if (granted) { + block.invoke() + } + } - companion object { + companion object { - fun newInstance(): RepoLocationFragment = RepoLocationFragment() - } + fun newInstance(): RepoLocationFragment = RepoLocationFragment() + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/WelcomeFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/WelcomeFragment.kt index 6b7e089f..7c3788c7 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/WelcomeFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/onboarding/fragments/WelcomeFragment.kt @@ -20,11 +20,13 @@ import dev.msfjarvis.aps.util.extensions.viewBinding @Suppress("unused") class WelcomeFragment : Fragment(R.layout.fragment_welcome) { - private val binding by viewBinding(FragmentWelcomeBinding::bind) + private val binding by viewBinding(FragmentWelcomeBinding::bind) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.letsGo.setOnClickListener { parentFragmentManager.performTransactionWithBackStack(CloneFragment.newInstance()) } - binding.settingsButton.setOnClickListener { startActivity(Intent(requireContext(), SettingsActivity::class.java)) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.letsGo.setOnClickListener { + parentFragmentManager.performTransactionWithBackStack(CloneFragment.newInstance()) } + binding.settingsButton.setOnClickListener { startActivity(Intent(requireContext(), SettingsActivity::class.java)) } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordFragment.kt index cc93b77f..c5712353 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordFragment.kt @@ -49,296 +49,278 @@ import me.zhanghai.android.fastscroll.FastScrollerBuilder class PasswordFragment : Fragment(R.layout.password_recycler_view) { - private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter - private lateinit var listener: OnFragmentInteractionListener - private lateinit var settings: SharedPreferences + private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter + private lateinit var listener: OnFragmentInteractionListener + private lateinit var settings: SharedPreferences + + private var recyclerViewStateToRestore: Parcelable? = null + private var actionMode: ActionMode? = null + private var scrollTarget: File? = null + + private val model: SearchableRepositoryViewModel by activityViewModels() + private val binding by viewBinding(PasswordRecyclerViewBinding::bind) + private val swipeResult = + registerForActivityResult(StartActivityForResult()) { + binding.swipeRefresher.isRefreshing = false + requireStore().refreshPasswordList() + } - private var recyclerViewStateToRestore: Parcelable? = null - private var actionMode: ActionMode? = null - private var scrollTarget: File? = null + val currentDir: File + get() = model.currentDir.value!! + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + settings = requireContext().sharedPrefs + initializePasswordList() + binding.fab.setOnClickListener { ItemCreationBottomSheet().show(childFragmentManager, "BOTTOM_SHEET") } + childFragmentManager.setFragmentResultListener(ITEM_CREATION_REQUEST_KEY, viewLifecycleOwner) { _, bundle -> + when (bundle.getString(ACTION_KEY)) { + ACTION_FOLDER -> requireStore().createFolder() + ACTION_PASSWORD -> requireStore().createPassword() + } + } + } - private val model: SearchableRepositoryViewModel by activityViewModels() - private val binding by viewBinding(PasswordRecyclerViewBinding::bind) - private val swipeResult = registerForActivityResult(StartActivityForResult()) { - binding.swipeRefresher.isRefreshing = false + private fun initializePasswordList() { + val gitDir = File(PasswordRepository.getRepositoryDirectory(), ".git") + val hasGitDir = gitDir.exists() && gitDir.isDirectory && (gitDir.listFiles()?.isNotEmpty() == true) + binding.swipeRefresher.setOnRefreshListener { + if (!hasGitDir) { requireStore().refreshPasswordList() + binding.swipeRefresher.isRefreshing = false + } else if (!PasswordRepository.isGitRepo()) { + BasicBottomSheet.Builder(requireContext()) + .setMessageRes(R.string.clone_git_repo) + .setPositiveButtonClickListener(getString(R.string.clone_button)) { + swipeResult.launch(GitServerConfigActivity.createCloneIntent(requireContext())) + } + .build() + .show(requireActivity().supportFragmentManager, "NOT_A_GIT_REPO") + binding.swipeRefresher.isRefreshing = false + } else { + // When authentication is set to AuthMode.None then the only git operation we can + // run is a pull, so automatically fallback to that. + val operationId = + when (GitSettings.authMode) { + AuthMode.None -> BaseGitActivity.GitOp.PULL + else -> BaseGitActivity.GitOp.SYNC + } + requireStore().apply { + lifecycleScope.launch { + launchGitOperation(operationId) + .fold( + success = { + binding.swipeRefresher.isRefreshing = false + refreshPasswordList() + }, + failure = { err -> promptOnErrorHandler(err) { binding.swipeRefresher.isRefreshing = false } }, + ) + } + } + } } - val currentDir: File - get() = model.currentDir.value!! - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - settings = requireContext().sharedPrefs - initializePasswordList() - binding.fab.setOnClickListener { - ItemCreationBottomSheet().show(childFragmentManager, "BOTTOM_SHEET") - } - childFragmentManager.setFragmentResultListener(ITEM_CREATION_REQUEST_KEY, viewLifecycleOwner) { _, bundle -> - when (bundle.getString(ACTION_KEY)) { - ACTION_FOLDER -> requireStore().createFolder() - ACTION_PASSWORD -> requireStore().createPassword() - } + recyclerAdapter = + PasswordItemRecyclerAdapter() + .onItemClicked { _, item -> listener.onFragmentInteraction(item) } + .onSelectionChanged { selection -> + // In order to not interfere with drag selection, we disable the + // SwipeRefreshLayout + // once an item is selected. + binding.swipeRefresher.isEnabled = selection.isEmpty + + if (actionMode == null) + actionMode = requireStore().startSupportActionMode(actionModeCallback) ?: return@onSelectionChanged + + if (!selection.isEmpty) { + actionMode!!.title = resources.getQuantityString(R.plurals.delete_title, selection.size(), selection.size()) + actionMode!!.invalidate() + } else { + actionMode!!.finish() + } } + val recyclerView = binding.passRecycler + recyclerView.apply { + addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)) + layoutManager = LinearLayoutManager(requireContext()) + itemAnimator = OnOffItemAnimator() + adapter = recyclerAdapter } - private fun initializePasswordList() { - val gitDir = File(PasswordRepository.getRepositoryDirectory(), ".git") - val hasGitDir = gitDir.exists() && gitDir.isDirectory && (gitDir.listFiles()?.isNotEmpty() == true) - binding.swipeRefresher.setOnRefreshListener { - if (!hasGitDir) { - requireStore().refreshPasswordList() - binding.swipeRefresher.isRefreshing = false - } else if (!PasswordRepository.isGitRepo()) { - BasicBottomSheet.Builder(requireContext()) - .setMessageRes(R.string.clone_git_repo) - .setPositiveButtonClickListener(getString(R.string.clone_button)) { - swipeResult.launch(GitServerConfigActivity.createCloneIntent(requireContext())) - } - .build() - .show(requireActivity().supportFragmentManager, "NOT_A_GIT_REPO") - binding.swipeRefresher.isRefreshing = false - } else { - // When authentication is set to AuthMode.None then the only git operation we can - // run is a pull, so automatically fallback to that. - val operationId = when (GitSettings.authMode) { - AuthMode.None -> BaseGitActivity.GitOp.PULL - else -> BaseGitActivity.GitOp.SYNC - } - requireStore().apply { - lifecycleScope.launch { - launchGitOperation(operationId).fold( - success = { - binding.swipeRefresher.isRefreshing = false - refreshPasswordList() - }, - failure = { err -> - promptOnErrorHandler(err) { - binding.swipeRefresher.isRefreshing = false - } - }, - ) - } - } - } + FastScrollerBuilder(recyclerView).build() + recyclerAdapter.makeSelectable(recyclerView) + registerForContextMenu(recyclerView) + + val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH)) + model.navigateTo(File(path), pushPreviousLocation = false) + model.searchResult.observe(viewLifecycleOwner) { result -> + // Only run animations when the new list is filtered, i.e., the user submitted a search, + // and not on folder navigations since the latter leads to too many removal animations. + (recyclerView.itemAnimator as OnOffItemAnimator).isEnabled = result.isFiltered + recyclerAdapter.submitList(result.passwordItems) { + when { + result.isFiltered -> { + // When the result is filtered, we always scroll to the top since that is + // where + // the best fuzzy match appears. + recyclerView.scrollToPosition(0) + } + scrollTarget != null -> { + scrollTarget?.let { recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it)) } + scrollTarget = null + } + else -> { + // When the result is not filtered and there is a saved scroll position for + // it, + // we try to restore it. + recyclerViewStateToRestore?.let { recyclerView.layoutManager!!.onRestoreInstanceState(it) } + recyclerViewStateToRestore = null + } } - - recyclerAdapter = PasswordItemRecyclerAdapter() - .onItemClicked { _, item -> - listener.onFragmentInteraction(item) - } - .onSelectionChanged { selection -> - // In order to not interfere with drag selection, we disable the SwipeRefreshLayout - // once an item is selected. - binding.swipeRefresher.isEnabled = selection.isEmpty - - if (actionMode == null) - actionMode = requireStore().startSupportActionMode(actionModeCallback) - ?: return@onSelectionChanged - - if (!selection.isEmpty) { - actionMode!!.title = resources.getQuantityString(R.plurals.delete_title, selection.size(), selection.size()) - actionMode!!.invalidate() - } else { - actionMode!!.finish() - } - } - val recyclerView = binding.passRecycler - recyclerView.apply { - addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)) - layoutManager = LinearLayoutManager(requireContext()) - itemAnimator = OnOffItemAnimator() - adapter = recyclerAdapter + } + } + } + + private val actionModeCallback = + object : ActionMode.Callback { + // Called when the action mode is created; startActionMode() was called + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + // Inflate a menu resource providing context menu items + mode.menuInflater.inflate(R.menu.context_pass, menu) + // hide the fab + animateFab(false) + return true + } + + // Called each time the action mode is shown. Always called after onCreateActionMode, + // but + // may be called multiple times if the mode is invalidated. + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + menu.findItem(R.id.menu_edit_password).isVisible = + recyclerAdapter.getSelectedItems().all { it.type == PasswordItem.TYPE_CATEGORY } + return true + } + + // Called when the user selects a contextual menu item + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_delete_password -> { + requireStore().deletePasswords(recyclerAdapter.getSelectedItems()) + // Action picked, so close the CAB + mode.finish() + true + } + R.id.menu_move_password -> { + requireStore().movePasswords(recyclerAdapter.getSelectedItems()) + false + } + R.id.menu_edit_password -> { + requireStore().renameCategory(recyclerAdapter.getSelectedItems()) + mode.finish() + false + } + else -> false } - - FastScrollerBuilder(recyclerView).build() - recyclerAdapter.makeSelectable(recyclerView) - registerForContextMenu(recyclerView) - - val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH)) - model.navigateTo(File(path), pushPreviousLocation = false) - model.searchResult.observe(viewLifecycleOwner) { result -> - // Only run animations when the new list is filtered, i.e., the user submitted a search, - // and not on folder navigations since the latter leads to too many removal animations. - (recyclerView.itemAnimator as OnOffItemAnimator).isEnabled = result.isFiltered - recyclerAdapter.submitList(result.passwordItems) { - when { - result.isFiltered -> { - // When the result is filtered, we always scroll to the top since that is where - // the best fuzzy match appears. - recyclerView.scrollToPosition(0) - } - scrollTarget != null -> { - scrollTarget?.let { - recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it)) - } - scrollTarget = null - } - else -> { - // When the result is not filtered and there is a saved scroll position for it, - // we try to restore it. - recyclerViewStateToRestore?.let { - recyclerView.layoutManager!!.onRestoreInstanceState(it) - } - recyclerViewStateToRestore = null - } - } + } + + // Called when the user exits the action mode + override fun onDestroyActionMode(mode: ActionMode) { + recyclerAdapter.requireSelectionTracker().clearSelection() + actionMode = null + // show the fab + animateFab(true) + } + + private fun animateFab(show: Boolean) = + with(binding.fab) { + val animation = AnimationUtils.loadAnimation(context, if (show) R.anim.scale_up else R.anim.scale_down) + animation.setAnimationListener( + object : Animation.AnimationListener { + override fun onAnimationRepeat(animation: Animation?) {} + + override fun onAnimationEnd(animation: Animation?) { + if (!show) visibility = View.GONE + } + + override fun onAnimationStart(animation: Animation?) { + if (show) visibility = View.VISIBLE + } } + ) + animate().rotationBy(if (show) -90f else 90f).setStartDelay(if (show) 100 else 0).setDuration(100).start() + startAnimation(animation) } } - private val actionModeCallback = object : ActionMode.Callback { - // Called when the action mode is created; startActionMode() was called - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - // Inflate a menu resource providing context menu items - mode.menuInflater.inflate(R.menu.context_pass, menu) - // hide the fab - animateFab(false) - return true - } - - // Called each time the action mode is shown. Always called after onCreateActionMode, but - // may be called multiple times if the mode is invalidated. - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - menu.findItem(R.id.menu_edit_password).isVisible = - recyclerAdapter.getSelectedItems() - .all { it.type == PasswordItem.TYPE_CATEGORY } - return true - } - - // Called when the user selects a contextual menu item - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - return when (item.itemId) { - R.id.menu_delete_password -> { - requireStore().deletePasswords(recyclerAdapter.getSelectedItems()) - // Action picked, so close the CAB - mode.finish() - true - } - R.id.menu_move_password -> { - requireStore().movePasswords(recyclerAdapter.getSelectedItems()) - false - } - R.id.menu_edit_password -> { - requireStore().renameCategory(recyclerAdapter.getSelectedItems()) - mode.finish() - false - } - else -> false + override fun onAttach(context: Context) { + super.onAttach(context) + runCatching { + listener = + object : OnFragmentInteractionListener { + override fun onFragmentInteraction(item: PasswordItem) { + if (settings.getString(PreferenceKeys.SORT_ORDER) == PasswordSortOrder.RECENTLY_USED.name) { + // save the time when password was used + val preferences = context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + preferences.edit { putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString()) } } - } - - // Called when the user exits the action mode - override fun onDestroyActionMode(mode: ActionMode) { - recyclerAdapter.requireSelectionTracker().clearSelection() - actionMode = null - // show the fab - animateFab(true) - } - private fun animateFab(show: Boolean) = with(binding.fab) { - val animation = AnimationUtils.loadAnimation( - context, if (show) R.anim.scale_up else R.anim.scale_down - ) - animation.setAnimationListener(object : Animation.AnimationListener { - override fun onAnimationRepeat(animation: Animation?) { - } - - override fun onAnimationEnd(animation: Animation?) { - if (!show) visibility = View.GONE - } - - override fun onAnimationStart(animation: Animation?) { - if (show) visibility = View.VISIBLE - } - }) - animate().rotationBy(if (show) -90f else 90f) - .setStartDelay(if (show) 100 else 0) - .setDuration(100) - .start() - startAnimation(animation) - } - } - - override fun onAttach(context: Context) { - super.onAttach(context) - runCatching { - listener = object : OnFragmentInteractionListener { - override fun onFragmentInteraction(item: PasswordItem) { - if (settings.getString(PreferenceKeys.SORT_ORDER) == PasswordSortOrder.RECENTLY_USED.name) { - //save the time when password was used - val preferences = context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) - preferences.edit { - putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString()) - } - } - - if (item.type == PasswordItem.TYPE_CATEGORY) { - navigateTo(item.file) - } else { - if (requireArguments().getBoolean("matchWith", false)) { - requireStore().matchPasswordWithApp(item) - } else { - requireStore().decryptPassword(item) - } - } - } + if (item.type == PasswordItem.TYPE_CATEGORY) { + navigateTo(item.file) + } else { + if (requireArguments().getBoolean("matchWith", false)) { + requireStore().matchPasswordWithApp(item) + } else { + requireStore().decryptPassword(item) + } } - }.onFailure { - throw ClassCastException("$context must implement OnFragmentInteractionListener") + } } } - - private fun requireStore() = requireActivity() as PasswordStore - - /** - * Returns true if the back press was handled by the [Fragment]. - */ - fun onBackPressedInActivity(): Boolean { - if (!model.canNavigateBack) - return false - // The RecyclerView state is restored when the asynchronous update operation on the - // adapter is completed. - recyclerViewStateToRestore = model.navigateBack() - if (!model.canNavigateBack) - requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(false) - return true + .onFailure { throw ClassCastException("$context must implement OnFragmentInteractionListener") } + } + + private fun requireStore() = requireActivity() as PasswordStore + + /** Returns true if the back press was handled by the [Fragment]. */ + fun onBackPressedInActivity(): Boolean { + if (!model.canNavigateBack) return false + // The RecyclerView state is restored when the asynchronous update operation on the + // adapter is completed. + recyclerViewStateToRestore = model.navigateBack() + if (!model.canNavigateBack) requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(false) + return true + } + + fun dismissActionMode() { + actionMode?.finish() + } + + companion object { + + const val ITEM_CREATION_REQUEST_KEY = "creation_key" + const val ACTION_KEY = "action" + const val ACTION_FOLDER = "folder" + const val ACTION_PASSWORD = "password" + + fun newInstance(args: Bundle): PasswordFragment { + val fragment = PasswordFragment() + fragment.arguments = args + return fragment } + } - fun dismissActionMode() { - actionMode?.finish() - } + fun navigateTo(file: File) { + requireStore().clearSearch() + model.navigateTo(file, recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState()) + requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true) + } - companion object { + fun scrollToOnNextRefresh(file: File) { + scrollTarget = file + } - const val ITEM_CREATION_REQUEST_KEY = "creation_key" - const val ACTION_KEY = "action" - const val ACTION_FOLDER = "folder" - const val ACTION_PASSWORD = "password" + interface OnFragmentInteractionListener { - fun newInstance(args: Bundle): PasswordFragment { - val fragment = PasswordFragment() - fragment.arguments = args - return fragment - } - } - - - fun navigateTo(file: File) { - requireStore().clearSearch() - model.navigateTo( - file, - recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState() - ) - requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - fun scrollToOnNextRefresh(file: File) { - scrollTarget = file - } - - interface OnFragmentInteractionListener { - - fun onFragmentInteraction(item: PasswordItem) - } + fun onFragmentInteraction(item: PasswordItem) + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt index a337a189..96fce533 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt @@ -78,625 +78,612 @@ const val PASSWORD_FRAGMENT_TAG = "PasswordsList" class PasswordStore : BaseGitActivity() { - private lateinit var searchItem: MenuItem - private val settings by lazy { sharedPrefs } + private lateinit var searchItem: MenuItem + private val settings by lazy { sharedPrefs } - private val model: SearchableRepositoryViewModel by viewModels { - ViewModelProvider.AndroidViewModelFactory(application) - } + private val model: SearchableRepositoryViewModel by viewModels { + ViewModelProvider.AndroidViewModelFactory(application) + } - private val storagePermissionRequest = registerForActivityResult(RequestPermission()) { granted -> - if (granted) checkLocalRepository() - } + private val storagePermissionRequest = + registerForActivityResult(RequestPermission()) { granted -> if (granted) checkLocalRepository() } - private val directorySelectAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - checkLocalRepository() - } + private val directorySelectAction = + registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + checkLocalRepository() + } } - private val listRefreshAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - refreshPasswordList() - } - } - - private val passwordMoveAction = registerForActivityResult(StartActivityForResult()) { result -> - val intentData = result.data ?: return@registerForActivityResult - val filesToMove = requireNotNull(intentData.getStringArrayExtra("Files")) - val target = File(requireNotNull(intentData.getStringExtra("SELECTED_FOLDER_PATH"))) - val repositoryPath = PasswordRepository.getRepositoryDirectory().absolutePath - if (!target.isDirectory) { - e { "Tried moving passwords to a non-existing folder." } - return@registerForActivityResult - } - - d { "Moving passwords to ${intentData.getStringExtra("SELECTED_FOLDER_PATH")}" } - d { filesToMove.joinToString(", ") } - - lifecycleScope.launch(Dispatchers.IO) { - for (file in filesToMove) { - val source = File(file) - if (!source.exists()) { - e { "Tried moving something that appears non-existent." } - continue - } - val destinationFile = File(target.absolutePath + "/" + source.name) - val basename = source.nameWithoutExtension - val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename) - val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename) - if (destinationFile.exists()) { - e { "Trying to move a file that already exists." } - withContext(Dispatchers.Main) { - MaterialAlertDialogBuilder(this@PasswordStore) - .setTitle(resources.getString(R.string.password_exists_title)) - .setMessage(resources.getString( - R.string.password_exists_message, - destinationLongName, - sourceLongName) - ) - .setPositiveButton(R.string.dialog_ok) { _, _ -> - launch(Dispatchers.IO) { - moveFile(source, destinationFile) - } - } - .setNegativeButton(R.string.dialog_cancel, null) - .show() - } - } else { - launch(Dispatchers.IO) { - moveFile(source, destinationFile) - } - } - } - when (filesToMove.size) { - 1 -> { - val source = File(filesToMove[0]) - val basename = source.nameWithoutExtension - val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename) - val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename) - withContext(Dispatchers.Main) { - commitChange( - resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName), - ) - } - } - else -> { - val repoDir = PasswordRepository.getRepositoryDirectory().absolutePath - val relativePath = getRelativePath("${target.absolutePath}/", repoDir) - withContext(Dispatchers.Main) { - commitChange( - resources.getString(R.string.git_commit_move_multiple_text, relativePath), - ) - } + private val listRefreshAction = + registerForActivityResult(StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + refreshPasswordList() + } + } + + private val passwordMoveAction = + registerForActivityResult(StartActivityForResult()) { result -> + val intentData = result.data ?: return@registerForActivityResult + val filesToMove = requireNotNull(intentData.getStringArrayExtra("Files")) + val target = File(requireNotNull(intentData.getStringExtra("SELECTED_FOLDER_PATH"))) + val repositoryPath = PasswordRepository.getRepositoryDirectory().absolutePath + if (!target.isDirectory) { + e { "Tried moving passwords to a non-existing folder." } + return@registerForActivityResult + } + + d { "Moving passwords to ${intentData.getStringExtra("SELECTED_FOLDER_PATH")}" } + d { filesToMove.joinToString(", ") } + + lifecycleScope.launch(Dispatchers.IO) { + for (file in filesToMove) { + val source = File(file) + if (!source.exists()) { + e { "Tried moving something that appears non-existent." } + continue + } + val destinationFile = File(target.absolutePath + "/" + source.name) + val basename = source.nameWithoutExtension + val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename) + val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename) + if (destinationFile.exists()) { + e { "Trying to move a file that already exists." } + withContext(Dispatchers.Main) { + MaterialAlertDialogBuilder(this@PasswordStore) + .setTitle(resources.getString(R.string.password_exists_title)) + .setMessage(resources.getString(R.string.password_exists_message, destinationLongName, sourceLongName)) + .setPositiveButton(R.string.dialog_ok) { _, _ -> + launch(Dispatchers.IO) { moveFile(source, destinationFile) } } + .setNegativeButton(R.string.dialog_cancel, null) + .show() } + } else { + launch(Dispatchers.IO) { moveFile(source, destinationFile) } + } } - refreshPasswordList() - getPasswordFragment()?.dismissActionMode() - } - - override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - // open search view on search key, or Ctr+F - if ((keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) && - !searchItem.isActionViewExpanded) { - searchItem.expandActionView() - return true - } - - // open search view on any printable character and query for it - val c = event.unicodeChar.toChar() - val printable = isPrintable(c) - if (printable && !searchItem.isActionViewExpanded) { - searchItem.expandActionView() - (searchItem.actionView as SearchView).setQuery(c.toString(), true) - return true - } - return super.onKeyDown(keyCode, event) - } - - @SuppressLint("NewApi") - override fun onCreate(savedInstanceState: Bundle?) { - // If user opens app with permission granted then revokes and returns, - // prevent attempt to create password list fragment - var savedInstance = savedInstanceState - if (savedInstanceState != null && (!settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) || - !isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE))) { - savedInstance = null - } - super.onCreate(savedInstance) - setContentView(R.layout.activity_pwdstore) - - model.currentDir.observe(this) { dir -> - val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile - supportActionBar!!.apply { - if (dir != basePath) - title = dir.name - else - setTitle(R.string.app_name) + when (filesToMove.size) { + 1 -> { + val source = File(filesToMove[0]) + val basename = source.nameWithoutExtension + val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename) + val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename) + withContext(Dispatchers.Main) { + commitChange( + resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName), + ) } - } - } - - override fun onStart() { - super.onStart() - refreshPasswordList() - } - - override fun onResume() { - super.onResume() - if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) { - hasRequiredStoragePermissions() - } else { - checkLocalRepository() - } - if (settings.getBoolean(PreferenceKeys.SEARCH_ON_START, false) && ::searchItem.isInitialized) { - if (!searchItem.isActionViewExpanded) { - searchItem.expandActionView() + } + else -> { + val repoDir = PasswordRepository.getRepositoryDirectory().absolutePath + val relativePath = getRelativePath("${target.absolutePath}/", repoDir) + withContext(Dispatchers.Main) { + commitChange( + resources.getString(R.string.git_commit_move_multiple_text, relativePath), + ) } + } } - } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - val menuRes = when { - GitSettings.authMode == AuthMode.None -> R.menu.main_menu_no_auth - PasswordRepository.isGitRepo() -> R.menu.main_menu_git - else -> R.menu.main_menu_non_git + } + refreshPasswordList() + getPasswordFragment()?.dismissActionMode() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + // open search view on search key, or Ctr+F + if ((keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) && + !searchItem.isActionViewExpanded + ) { + searchItem.expandActionView() + return true + } + + // open search view on any printable character and query for it + val c = event.unicodeChar.toChar() + val printable = isPrintable(c) + if (printable && !searchItem.isActionViewExpanded) { + searchItem.expandActionView() + (searchItem.actionView as SearchView).setQuery(c.toString(), true) + return true + } + return super.onKeyDown(keyCode, event) + } + + @SuppressLint("NewApi") + override fun onCreate(savedInstanceState: Bundle?) { + // If user opens app with permission granted then revokes and returns, + // prevent attempt to create password list fragment + var savedInstance = savedInstanceState + if (savedInstanceState != null && + (!settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) || + !isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) + ) { + savedInstance = null + } + super.onCreate(savedInstance) + setContentView(R.layout.activity_pwdstore) + + model.currentDir.observe(this) { dir -> + val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile + supportActionBar!!.apply { if (dir != basePath) title = dir.name else setTitle(R.string.app_name) } + } + } + + override fun onStart() { + super.onStart() + refreshPasswordList() + } + + override fun onResume() { + super.onResume() + if (settings.getBoolean(PreferenceKeys.GIT_EXTERNAL, false)) { + hasRequiredStoragePermissions() + } else { + checkLocalRepository() + } + if (settings.getBoolean(PreferenceKeys.SEARCH_ON_START, false) && ::searchItem.isInitialized) { + if (!searchItem.isActionViewExpanded) { + searchItem.expandActionView() + } + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + val menuRes = + when { + GitSettings.authMode == AuthMode.None -> R.menu.main_menu_no_auth + PasswordRepository.isGitRepo() -> R.menu.main_menu_git + else -> R.menu.main_menu_non_git + } + menuInflater.inflate(menuRes, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + // Invalidation forces onCreateOptionsMenu to be called again. This is cheap and quick so + // we can get by without any noticeable difference in performance. + invalidateOptionsMenu() + searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + searchView.setOnQueryTextListener( + object : OnQueryTextListener { + override fun onQueryTextSubmit(s: String): Boolean { + searchView.clearFocus() + return true } - menuInflater.inflate(menuRes, menu) - return super.onCreateOptionsMenu(menu) - } - override fun onPrepareOptionsMenu(menu: Menu): Boolean { - // Invalidation forces onCreateOptionsMenu to be called again. This is cheap and quick so - // we can get by without any noticeable difference in performance. - invalidateOptionsMenu() - searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView - searchView.setOnQueryTextListener( - object : OnQueryTextListener { - override fun onQueryTextSubmit(s: String): Boolean { - searchView.clearFocus() - return true - } - - override fun onQueryTextChange(s: String): Boolean { - val filter = s.trim() - // List the contents of the current directory if the user enters a blank - // search term. - if (filter.isEmpty()) - model.navigateTo( - newDirectory = model.currentDir.value!!, - pushPreviousLocation = false - ) - else - model.search(filter) - return true - } - }) - - // When using the support library, the setOnActionExpandListener() method is - // static and accepts the MenuItem object as an argument - searchItem.setOnActionExpandListener( - object : OnActionExpandListener { - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - refreshPasswordList() - return true - } - - override fun onMenuItemActionExpand(item: MenuItem): Boolean { - return true - } - }) - if (settings.getBoolean(PreferenceKeys.SEARCH_ON_START, false)) { - searchItem.expandActionView() + override fun onQueryTextChange(s: String): Boolean { + val filter = s.trim() + // List the contents of the current directory if the user enters a blank + // search term. + if (filter.isEmpty()) model.navigateTo(newDirectory = model.currentDir.value!!, pushPreviousLocation = false) + else model.search(filter) + return true } - return super.onPrepareOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - val id = item.itemId - val initBefore = MaterialAlertDialogBuilder(this) - .setMessage(resources.getString(R.string.creation_dialog_text)) - .setPositiveButton(resources.getString(R.string.dialog_ok), null) - when (id) { - R.id.user_pref -> { - runCatching { - startActivity(Intent(this, SettingsActivity::class.java)) - }.onFailure { e -> - e.printStackTrace() - } - } - R.id.git_push -> { - if (!PasswordRepository.isInitialized) { - initBefore.show() - } else { - runGitOperation(GitOp.PUSH) - } - } - R.id.git_pull -> { - if (!PasswordRepository.isInitialized) { - initBefore.show() - } else { - runGitOperation(GitOp.PULL) - } - } - R.id.git_sync -> { - if (!PasswordRepository.isInitialized) { - initBefore.show() - } else { - runGitOperation(GitOp.SYNC) - } - } - R.id.refresh -> refreshPasswordList() - android.R.id.home -> onBackPressed() - else -> return super.onOptionsItemSelected(item) + } + ) + + // When using the support library, the setOnActionExpandListener() method is + // static and accepts the MenuItem object as an argument + searchItem.setOnActionExpandListener( + object : OnActionExpandListener { + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + refreshPasswordList() + return true } - return true - } - - override fun onBackPressed() { - if (getPasswordFragment()?.onBackPressedInActivity() != true) - super.onBackPressed() - } - - private fun getPasswordFragment(): PasswordFragment? { - return supportFragmentManager.findFragmentByTag(PASSWORD_FRAGMENT_TAG) as? PasswordFragment - } - - fun clearSearch() { - if (searchItem.isActionViewExpanded) - searchItem.collapseActionView() - } - - private fun runGitOperation(operation: GitOp) = lifecycleScope.launch { - launchGitOperation(operation).fold( - success = { refreshPasswordList() }, - failure = { promptOnErrorHandler(it) }, - ) - } - /** - * Validates if storage permission is granted, and requests for it if not. The return value - * is true if the permission has been granted. - */ - private fun hasRequiredStoragePermissions(): Boolean { - return if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - BasicBottomSheet.Builder(this) - .setMessageRes(R.string.access_sdcard_text) - .setPositiveButtonClickListener(getString(R.string.snackbar_action_grant)) { - storagePermissionRequest.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } - .build() - .show(supportFragmentManager, "STORAGE_PERMISSION_MISSING") - false + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + return true + } + } + ) + if (settings.getBoolean(PreferenceKeys.SEARCH_ON_START, false)) { + searchItem.expandActionView() + } + return super.onPrepareOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val id = item.itemId + val initBefore = + MaterialAlertDialogBuilder(this) + .setMessage(resources.getString(R.string.creation_dialog_text)) + .setPositiveButton(resources.getString(R.string.dialog_ok), null) + when (id) { + R.id.user_pref -> { + runCatching { startActivity(Intent(this, SettingsActivity::class.java)) }.onFailure { e -> e.printStackTrace() } + } + R.id.git_push -> { + if (!PasswordRepository.isInitialized) { + initBefore.show() } else { - checkLocalRepository() - true + runGitOperation(GitOp.PUSH) } - } - - private fun checkLocalRepository() { - val repo = PasswordRepository.initialize() - if (repo == null) { - directorySelectAction.launch(Intent(this, DirectorySelectionActivity::class.java)) + } + R.id.git_pull -> { + if (!PasswordRepository.isInitialized) { + initBefore.show() } else { - checkLocalRepository(PasswordRepository.getRepositoryDirectory()) + runGitOperation(GitOp.PULL) } - } - - private fun checkLocalRepository(localDir: File?) { - if (localDir != null && settings.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)) { - d { "Check, dir: ${localDir.absolutePath}" } - // do not push the fragment if we already have it - if (getPasswordFragment() == null || - settings.getBoolean(PreferenceKeys.REPO_CHANGED, false)) { - settings.edit { putBoolean(PreferenceKeys.REPO_CHANGED, false) } - val args = Bundle() - args.putString(REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath) - - // if the activity was started from the autofill settings, the - // intent is to match a clicked pwd with app. pass this to fragment - if (intent.getBooleanExtra("matchWith", false)) { - args.putBoolean("matchWith", true) - } - supportActionBar?.apply { - show() - setDisplayHomeAsUpEnabled(false) - } - supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - supportFragmentManager.commit { - replace(R.id.main_layout, PasswordFragment.newInstance(args), PASSWORD_FRAGMENT_TAG) - } - } + } + R.id.git_sync -> { + if (!PasswordRepository.isInitialized) { + initBefore.show() } else { - startActivity(Intent(this, OnboardingActivity::class.java)) + runGitOperation(GitOp.SYNC) } + } + R.id.refresh -> refreshPasswordList() + android.R.id.home -> onBackPressed() + else -> return super.onOptionsItemSelected(item) + } + return true + } + + override fun onBackPressed() { + if (getPasswordFragment()?.onBackPressedInActivity() != true) super.onBackPressed() + } + + private fun getPasswordFragment(): PasswordFragment? { + return supportFragmentManager.findFragmentByTag(PASSWORD_FRAGMENT_TAG) as? PasswordFragment + } + + fun clearSearch() { + if (searchItem.isActionViewExpanded) searchItem.collapseActionView() + } + + private fun runGitOperation(operation: GitOp) = + lifecycleScope.launch { + launchGitOperation(operation) + .fold( + success = { refreshPasswordList() }, + failure = { promptOnErrorHandler(it) }, + ) } - private fun getRelativePath(fullPath: String, repositoryPath: String): String { - return fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") - } - - private fun getLastChangedTimestamp(fullPath: String): Long { - val repoPath = PasswordRepository.getRepositoryDirectory() - val repository = PasswordRepository.getRepository(repoPath) - if (repository == null) { - d { "getLastChangedTimestamp: No git repository" } - return File(fullPath).lastModified() + /** + * Validates if storage permission is granted, and requests for it if not. The return value is + * true if the permission has been granted. + */ + private fun hasRequiredStoragePermissions(): Boolean { + return if (!isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + BasicBottomSheet.Builder(this) + .setMessageRes(R.string.access_sdcard_text) + .setPositiveButtonClickListener(getString(R.string.snackbar_action_grant)) { + storagePermissionRequest.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) } - val git = Git(repository) - val relativePath = getRelativePath(fullPath, repoPath.absolutePath).substring(1) // Removes leading '/' - return runCatching { - val iterator = git.log().addPath(relativePath).call().iterator() - if (!iterator.hasNext()) { - w { "getLastChangedTimestamp: No commits for file: $relativePath" } - return -1 - } - iterator.next().commitTime.toLong() * 1000 - }.getOr(-1) - } - - fun decryptPassword(item: PasswordItem) { - val decryptIntent = Intent(this, DecryptActivity::class.java) - val authDecryptIntent = Intent(this, LaunchActivity::class.java) - for (intent in arrayOf(decryptIntent, authDecryptIntent)) { - intent.putExtra("NAME", item.toString()) - intent.putExtra("FILE_PATH", item.file.absolutePath) - intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory().absolutePath) - intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.file.absolutePath)) + .build() + .show(supportFragmentManager, "STORAGE_PERMISSION_MISSING") + false + } else { + checkLocalRepository() + true + } + } + + private fun checkLocalRepository() { + val repo = PasswordRepository.initialize() + if (repo == null) { + directorySelectAction.launch(Intent(this, DirectorySelectionActivity::class.java)) + } else { + checkLocalRepository(PasswordRepository.getRepositoryDirectory()) + } + } + + private fun checkLocalRepository(localDir: File?) { + if (localDir != null && settings.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)) { + d { "Check, dir: ${localDir.absolutePath}" } + // do not push the fragment if we already have it + if (getPasswordFragment() == null || settings.getBoolean(PreferenceKeys.REPO_CHANGED, false)) { + settings.edit { putBoolean(PreferenceKeys.REPO_CHANGED, false) } + val args = Bundle() + args.putString(REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath) + + // if the activity was started from the autofill settings, the + // intent is to match a clicked pwd with app. pass this to fragment + if (intent.getBooleanExtra("matchWith", false)) { + args.putBoolean("matchWith", true) } - // Needs an action to be a shortcut intent - authDecryptIntent.action = LaunchActivity.ACTION_DECRYPT_PASS - - startActivity(decryptIntent) - - // Adds shortcut - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - addShortcut(item, authDecryptIntent) + supportActionBar?.apply { + show() + setDisplayHomeAsUpEnabled(false) } - } - - @RequiresApi(Build.VERSION_CODES.N_MR1) - private fun addShortcut(item: PasswordItem, intent: Intent) { - val shortcutManager: ShortcutManager = getSystemService() ?: return - val shortcut = Builder(this, item.fullPathToParent) - .setShortLabel(item.toString()) - .setLongLabel(item.fullPathToParent + item.toString()) - .setIcon(Icon.createWithResource(this, R.drawable.ic_lock_open_24px)) - .setIntent(intent) - .build() - val shortcuts = shortcutManager.dynamicShortcuts - // If we're above or equal to the maximum shortcuts allowed, drop the last item. - if (shortcuts.size >= MAX_SHORTCUT_COUNT) { - shortcuts.removeLast() + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + supportFragmentManager.commit { + replace(R.id.main_layout, PasswordFragment.newInstance(args), PASSWORD_FRAGMENT_TAG) } - // Reverse the list so we can append our new shortcut at the 'end'. - shortcuts.reverse() - shortcuts.add(shortcut) - // Reverse it again, so the previous items are now in the correct order and our new item - // is at the front like it's supposed to. - shortcuts.reverse() - // Write back the new shortcuts. - shortcutManager.dynamicShortcuts = shortcuts - } - - private fun validateState(): Boolean { - if (!PasswordRepository.isInitialized) { - MaterialAlertDialogBuilder(this) - .setMessage(resources.getString(R.string.creation_dialog_text)) - .setPositiveButton(resources.getString(R.string.dialog_ok), null) - .show() - return false + } + } else { + startActivity(Intent(this, OnboardingActivity::class.java)) + } + } + + private fun getRelativePath(fullPath: String, repositoryPath: String): String { + return fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") + } + + private fun getLastChangedTimestamp(fullPath: String): Long { + val repoPath = PasswordRepository.getRepositoryDirectory() + val repository = PasswordRepository.getRepository(repoPath) + if (repository == null) { + d { "getLastChangedTimestamp: No git repository" } + return File(fullPath).lastModified() + } + val git = Git(repository) + val relativePath = getRelativePath(fullPath, repoPath.absolutePath).substring(1) // Removes leading '/' + return runCatching { + val iterator = git.log().addPath(relativePath).call().iterator() + if (!iterator.hasNext()) { + w { "getLastChangedTimestamp: No commits for file: $relativePath" } + return -1 } - return true - } - - fun createPassword() { - if (!validateState()) return - val currentDir = currentDir - i { "Adding file to : ${currentDir.absolutePath}" } - val intent = Intent(this, PasswordCreationActivity::class.java) - intent.putExtra("FILE_PATH", currentDir.absolutePath) - intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory().absolutePath) - listRefreshAction.launch(intent) - } - - fun createFolder() { - if (!validateState()) return - FolderCreationDialogFragment.newInstance(currentDir.path).show(supportFragmentManager, null) - } - - fun deletePasswords(selectedItems: List) { - var size = 0 - selectedItems.forEach { - if (it.file.isFile) - size++ - else - size += it.file.listFilesRecursively().size + iterator.next().commitTime.toLong() * 1000 + } + .getOr(-1) + } + + fun decryptPassword(item: PasswordItem) { + val decryptIntent = Intent(this, DecryptActivity::class.java) + val authDecryptIntent = Intent(this, LaunchActivity::class.java) + for (intent in arrayOf(decryptIntent, authDecryptIntent)) { + intent.putExtra("NAME", item.toString()) + intent.putExtra("FILE_PATH", item.file.absolutePath) + intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory().absolutePath) + intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.file.absolutePath)) + } + // Needs an action to be a shortcut intent + authDecryptIntent.action = LaunchActivity.ACTION_DECRYPT_PASS + + startActivity(decryptIntent) + + // Adds shortcut + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + addShortcut(item, authDecryptIntent) + } + } + + @RequiresApi(Build.VERSION_CODES.N_MR1) + private fun addShortcut(item: PasswordItem, intent: Intent) { + val shortcutManager: ShortcutManager = getSystemService() ?: return + val shortcut = + Builder(this, item.fullPathToParent) + .setShortLabel(item.toString()) + .setLongLabel(item.fullPathToParent + item.toString()) + .setIcon(Icon.createWithResource(this, R.drawable.ic_lock_open_24px)) + .setIntent(intent) + .build() + val shortcuts = shortcutManager.dynamicShortcuts + // If we're above or equal to the maximum shortcuts allowed, drop the last item. + if (shortcuts.size >= MAX_SHORTCUT_COUNT) { + shortcuts.removeLast() + } + // Reverse the list so we can append our new shortcut at the 'end'. + shortcuts.reverse() + shortcuts.add(shortcut) + // Reverse it again, so the previous items are now in the correct order and our new item + // is at the front like it's supposed to. + shortcuts.reverse() + // Write back the new shortcuts. + shortcutManager.dynamicShortcuts = shortcuts + } + + private fun validateState(): Boolean { + if (!PasswordRepository.isInitialized) { + MaterialAlertDialogBuilder(this) + .setMessage(resources.getString(R.string.creation_dialog_text)) + .setPositiveButton(resources.getString(R.string.dialog_ok), null) + .show() + return false + } + return true + } + + fun createPassword() { + if (!validateState()) return + val currentDir = currentDir + i { "Adding file to : ${currentDir.absolutePath}" } + val intent = Intent(this, PasswordCreationActivity::class.java) + intent.putExtra("FILE_PATH", currentDir.absolutePath) + intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory().absolutePath) + listRefreshAction.launch(intent) + } + + fun createFolder() { + if (!validateState()) return + FolderCreationDialogFragment.newInstance(currentDir.path).show(supportFragmentManager, null) + } + + fun deletePasswords(selectedItems: List) { + var size = 0 + selectedItems.forEach { if (it.file.isFile) size++ else size += it.file.listFilesRecursively().size } + if (size == 0) { + selectedItems.map { item -> item.file.deleteRecursively() } + refreshPasswordList() + return + } + MaterialAlertDialogBuilder(this) + .setMessage(resources.getQuantityString(R.plurals.delete_dialog_text, size, size)) + .setPositiveButton(resources.getString(R.string.dialog_yes)) { _, _ -> + val filesToDelete = arrayListOf() + selectedItems.forEach { item -> + if (item.file.isDirectory) filesToDelete.addAll(item.file.listFilesRecursively()) + else filesToDelete.add(item.file) } - if (size == 0) { - selectedItems.map { item -> item.file.deleteRecursively() } - refreshPasswordList() - return + selectedItems.map { item -> item.file.deleteRecursively() } + refreshPasswordList() + AutofillMatcher.updateMatches(applicationContext, delete = filesToDelete) + val fmt = + selectedItems.joinToString(separator = ", ") { item -> + item.file.toRelativeString(PasswordRepository.getRepositoryDirectory()) + } + lifecycleScope.launch { + commitChange( + resources.getString(R.string.git_commit_remove_text, fmt), + ) } - MaterialAlertDialogBuilder(this) - .setMessage(resources.getQuantityString(R.plurals.delete_dialog_text, size, size)) - .setPositiveButton(resources.getString(R.string.dialog_yes)) { _, _ -> - val filesToDelete = arrayListOf() - selectedItems.forEach { item -> - if (item.file.isDirectory) - filesToDelete.addAll(item.file.listFilesRecursively()) - else - filesToDelete.add(item.file) - } - selectedItems.map { item -> item.file.deleteRecursively() } - refreshPasswordList() - AutofillMatcher.updateMatches(applicationContext, delete = filesToDelete) - val fmt = selectedItems.joinToString(separator = ", ") { item -> - item.file.toRelativeString(PasswordRepository.getRepositoryDirectory()) + } + .setNegativeButton(resources.getString(R.string.dialog_no), null) + .show() + } + + fun movePasswords(values: List) { + val intent = Intent(this, SelectFolderActivity::class.java) + val fileLocations = values.map { it.file.absolutePath }.toTypedArray() + intent.putExtra("Files", fileLocations) + passwordMoveAction.launch(intent) + } + + enum class CategoryRenameError(val resource: Int) { + None(0), + EmptyField(R.string.message_category_error_empty_field), + CategoryExists(R.string.message_category_error_category_exists), + DestinationOutsideRepo(R.string.message_error_destination_outside_repo), + } + + /** + * Prompt the user with a new category name to assign, if the new category forms/leads a path + * (i.e. contains "/"), intermediate directories will be created and new category will be placed + * inside. + * + * @param oldCategory The category to change its name + * @param error Determines whether to show an error to the user in the alert dialog, this error + * may be due to the new category the user entered already exists or the field was empty or the + * destination path is outside the repository + * + * @see [CategoryRenameError] + * @see [isInsideRepository] + */ + private fun renameCategory(oldCategory: PasswordItem, error: CategoryRenameError = CategoryRenameError.None) { + val view = layoutInflater.inflate(R.layout.folder_dialog_fragment, null) + val newCategoryEditText = view.findViewById(R.id.folder_name_text) + + if (error != CategoryRenameError.None) { + newCategoryEditText.error = getString(error.resource) + } + + val dialog = + MaterialAlertDialogBuilder(this) + .setTitle(R.string.title_rename_folder) + .setView(view) + .setMessage(getString(R.string.message_rename_folder, oldCategory.name)) + .setPositiveButton(R.string.dialog_ok) { _, _ -> + val newCategory = File("${oldCategory.file.parent}/${newCategoryEditText.text}") + when { + newCategoryEditText.text.isNullOrBlank() -> renameCategory(oldCategory, CategoryRenameError.EmptyField) + newCategory.exists() -> renameCategory(oldCategory, CategoryRenameError.CategoryExists) + !newCategory.isInsideRepository() -> renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo) + else -> + lifecycleScope.launch(Dispatchers.IO) { + moveFile(oldCategory.file, newCategory) + + // associate the new category with the last category's timestamp in + // history + val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) + val timestamp = preference.getString(oldCategory.file.absolutePath.base64()) + if (timestamp != null) { + preference.edit { + remove(oldCategory.file.absolutePath.base64()) + putString(newCategory.absolutePath.base64(), timestamp) + } } - lifecycleScope.launch { - commitChange( - resources.getString(R.string.git_commit_remove_text, fmt), - ) - } - } - .setNegativeButton(resources.getString(R.string.dialog_no), null) - .show() - } - - fun movePasswords(values: List) { - val intent = Intent(this, SelectFolderActivity::class.java) - val fileLocations = values.map { it.file.absolutePath }.toTypedArray() - intent.putExtra("Files", fileLocations) - passwordMoveAction.launch(intent) - } - - enum class CategoryRenameError(val resource: Int) { - None(0), - EmptyField(R.string.message_category_error_empty_field), - CategoryExists(R.string.message_category_error_category_exists), - DestinationOutsideRepo(R.string.message_error_destination_outside_repo), - } - - /** - * Prompt the user with a new category name to assign, - * if the new category forms/leads a path (i.e. contains "/"), intermediate directories will be created - * and new category will be placed inside. - * - * @param oldCategory The category to change its name - * @param error Determines whether to show an error to the user in the alert dialog, - * this error may be due to the new category the user entered already exists or the field was empty or the - * destination path is outside the repository - * - * @see [CategoryRenameError] - * @see [isInsideRepository] - */ - private fun renameCategory(oldCategory: PasswordItem, error: CategoryRenameError = CategoryRenameError.None) { - val view = layoutInflater.inflate(R.layout.folder_dialog_fragment, null) - val newCategoryEditText = view.findViewById(R.id.folder_name_text) - - if (error != CategoryRenameError.None) { - newCategoryEditText.error = getString(error.resource) - } - val dialog = MaterialAlertDialogBuilder(this) - .setTitle(R.string.title_rename_folder) - .setView(view) - .setMessage(getString(R.string.message_rename_folder, oldCategory.name)) - .setPositiveButton(R.string.dialog_ok) { _, _ -> - val newCategory = File("${oldCategory.file.parent}/${newCategoryEditText.text}") - when { - newCategoryEditText.text.isNullOrBlank() -> renameCategory(oldCategory, CategoryRenameError.EmptyField) - newCategory.exists() -> renameCategory(oldCategory, CategoryRenameError.CategoryExists) - !newCategory.isInsideRepository() -> renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo) - else -> lifecycleScope.launch(Dispatchers.IO) { - moveFile(oldCategory.file, newCategory) - - //associate the new category with the last category's timestamp in history - val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE) - val timestamp = preference.getString(oldCategory.file.absolutePath.base64()) - if (timestamp != null) { - preference.edit { - remove(oldCategory.file.absolutePath.base64()) - putString(newCategory.absolutePath.base64(), timestamp) - } - } - - withContext(Dispatchers.Main) { - commitChange( - resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name), - ) - } - } + withContext(Dispatchers.Main) { + commitChange( + resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name), + ) } - } - .setNegativeButton(R.string.dialog_skip, null) - .create() - - dialog.requestInputFocusOnView(R.id.folder_name_text) - dialog.show() - } - - fun renameCategory(categories: List) { - for (oldCategory in categories) { - renameCategory(oldCategory) - } - } - - /** - * Refreshes the password list by re-executing the last navigation or search action, preserving - * the navigation stack and scroll position. If the current directory no longer exists, - * navigation is reset to the repository root. If the optional [target] argument is provided, - * it will be entered if it is a directory or scrolled into view if it is a file (both inside - * the current directory). - */ - fun refreshPasswordList(target: File? = null) { - val plist = getPasswordFragment() - if (target?.isDirectory == true && model.currentDir.value?.contains(target) == true) { - plist?.navigateTo(target) - } else if (target?.isFile == true && model.currentDir.value?.contains(target) == true) { - // Creating new passwords is handled by an activity, so we will refresh in onStart. - plist?.scrollToOnNextRefresh(target) - } else if (model.currentDir.value?.isDirectory == true) { - model.forceRefresh() - } else { - model.reset() - supportActionBar!!.setDisplayHomeAsUpEnabled(false) - } - } - - private val currentDir: File - get() = getPasswordFragment()?.currentDir ?: PasswordRepository.getRepositoryDirectory() - - private suspend fun moveFile(source: File, destinationFile: File) { - val sourceDestinationMap = if (source.isDirectory) { - destinationFile.mkdirs() - // Recursively list all files (not directories) below `source`, then - // obtain the corresponding target file by resolving the relative path - // starting at the destination folder. - source.listFilesRecursively().associateWith { destinationFile.resolve(it.relativeTo(source)) } - } else { - mapOf(source to destinationFile) - } - if (!source.renameTo(destinationFile)) { - e { "Something went wrong while moving $source to $destinationFile." } - withContext(Dispatchers.Main) { - MaterialAlertDialogBuilder(this@PasswordStore) - .setTitle(R.string.password_move_error_title) - .setMessage(getString(R.string.password_move_error_message, source, destinationFile)) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, null) - .show() - } - } else { - AutofillMatcher.updateMatches(this, sourceDestinationMap) - } - } - - fun matchPasswordWithApp(item: PasswordItem) { - val path = item.file - .absolutePath - .replace(PasswordRepository.getRepositoryDirectory().toString() + "/", "") - .replace(".gpg", "") - val data = Intent() - data.putExtra("path", path) - setResult(RESULT_OK, data) - finish() - } - - companion object { - - // The max shortcut count from the system is set to 15 for some godforsaken reason, which - // makes zero sense and is why our update logic just never worked. Capping it at 4 which is - // what most launchers seem to have agreed upon is the only reasonable solution. - private const val MAX_SHORTCUT_COUNT = 4 - const val REQUEST_ARG_PATH = "PATH" - private fun isPrintable(c: Char): Boolean { - val block = UnicodeBlock.of(c) - return (!Character.isISOControl(c) && - block != null && block !== UnicodeBlock.SPECIALS) + } + } } - } + .setNegativeButton(R.string.dialog_skip, null) + .create() + + dialog.requestInputFocusOnView(R.id.folder_name_text) + dialog.show() + } + + fun renameCategory(categories: List) { + for (oldCategory in categories) { + renameCategory(oldCategory) + } + } + + /** + * Refreshes the password list by re-executing the last navigation or search action, preserving + * the navigation stack and scroll position. If the current directory no longer exists, navigation + * is reset to the repository root. If the optional [target] argument is provided, it will be + * entered if it is a directory or scrolled into view if it is a file (both inside the current + * directory). + */ + fun refreshPasswordList(target: File? = null) { + val plist = getPasswordFragment() + if (target?.isDirectory == true && model.currentDir.value?.contains(target) == true) { + plist?.navigateTo(target) + } else if (target?.isFile == true && model.currentDir.value?.contains(target) == true) { + // Creating new passwords is handled by an activity, so we will refresh in onStart. + plist?.scrollToOnNextRefresh(target) + } else if (model.currentDir.value?.isDirectory == true) { + model.forceRefresh() + } else { + model.reset() + supportActionBar!!.setDisplayHomeAsUpEnabled(false) + } + } + + private val currentDir: File + get() = getPasswordFragment()?.currentDir ?: PasswordRepository.getRepositoryDirectory() + + private suspend fun moveFile(source: File, destinationFile: File) { + val sourceDestinationMap = + if (source.isDirectory) { + destinationFile.mkdirs() + // Recursively list all files (not directories) below `source`, then + // obtain the corresponding target file by resolving the relative path + // starting at the destination folder. + source.listFilesRecursively().associateWith { destinationFile.resolve(it.relativeTo(source)) } + } else { + mapOf(source to destinationFile) + } + if (!source.renameTo(destinationFile)) { + e { "Something went wrong while moving $source to $destinationFile." } + withContext(Dispatchers.Main) { + MaterialAlertDialogBuilder(this@PasswordStore) + .setTitle(R.string.password_move_error_title) + .setMessage(getString(R.string.password_move_error_message, source, destinationFile)) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } else { + AutofillMatcher.updateMatches(this, sourceDestinationMap) + } + } + + fun matchPasswordWithApp(item: PasswordItem) { + val path = + item + .file + .absolutePath + .replace(PasswordRepository.getRepositoryDirectory().toString() + "/", "") + .replace(".gpg", "") + val data = Intent() + data.putExtra("path", path) + setResult(RESULT_OK, data) + finish() + } + + companion object { + + // The max shortcut count from the system is set to 15 for some godforsaken reason, which + // makes zero sense and is why our update logic just never worked. Capping it at 4 which is + // what most launchers seem to have agreed upon is the only reasonable solution. + private const val MAX_SHORTCUT_COUNT = 4 + const val REQUEST_ARG_PATH = "PATH" + private fun isPrintable(c: Char): Boolean { + val block = UnicodeBlock.of(c) + return (!Character.isISOControl(c) && block != null && block !== UnicodeBlock.SPECIALS) + } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/proxy/ProxySelectorActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/proxy/ProxySelectorActivity.kt index 72e1d873..3841438e 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/proxy/ProxySelectorActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/proxy/ProxySelectorActivity.kt @@ -27,49 +27,39 @@ private val WEB_ADDRESS_REGEX = Patterns.WEB_URL.toRegex() class ProxySelectorActivity : AppCompatActivity() { - private val binding by viewBinding(ActivityProxySelectorBinding::inflate) - private val proxyPrefs by lazy(LazyThreadSafetyMode.NONE) { applicationContext.getEncryptedProxyPrefs() } + private val binding by viewBinding(ActivityProxySelectorBinding::inflate) + private val proxyPrefs by lazy(LazyThreadSafetyMode.NONE) { applicationContext.getEncryptedProxyPrefs() } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - with(binding) { - proxyHost.setText(proxyPrefs.getString(PreferenceKeys.PROXY_HOST)) - proxyUser.setText(proxyPrefs.getString(PreferenceKeys.PROXY_USERNAME)) - proxyPrefs.getInt(PreferenceKeys.PROXY_PORT, -1).takeIf { it != -1 }?.let { - proxyPort.setText("$it") - } - proxyPassword.setText(proxyPrefs.getString(PreferenceKeys.PROXY_PASSWORD)) - save.setOnClickListener { saveSettings() } - proxyHost.doOnTextChanged { text, _, _, _ -> - if (text != null) { - proxyHost.error = if (text.matches(IP_ADDRESS_REGEX) || text.matches(WEB_ADDRESS_REGEX)) { - null - } else { - getString(R.string.invalid_proxy_url) - } - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + with(binding) { + proxyHost.setText(proxyPrefs.getString(PreferenceKeys.PROXY_HOST)) + proxyUser.setText(proxyPrefs.getString(PreferenceKeys.PROXY_USERNAME)) + proxyPrefs.getInt(PreferenceKeys.PROXY_PORT, -1).takeIf { it != -1 }?.let { proxyPort.setText("$it") } + proxyPassword.setText(proxyPrefs.getString(PreferenceKeys.PROXY_PASSWORD)) + save.setOnClickListener { saveSettings() } + proxyHost.doOnTextChanged { text, _, _, _ -> + if (text != null) { + proxyHost.error = + if (text.matches(IP_ADDRESS_REGEX) || text.matches(WEB_ADDRESS_REGEX)) { + null + } else { + getString(R.string.invalid_proxy_url) } } - + } } + } - private fun saveSettings() { - proxyPrefs.edit { - binding.proxyHost.text?.toString()?.takeIf { it.isNotEmpty() }.let { - GitSettings.proxyHost = it - } - binding.proxyUser.text?.toString()?.takeIf { it.isNotEmpty() }.let { - GitSettings.proxyUsername = it - } - binding.proxyPort.text?.toString()?.takeIf { it.isNotEmpty() }?.let { - GitSettings.proxyPort = it.toInt() - } - binding.proxyPassword.text?.toString()?.takeIf { it.isNotEmpty() }.let { - GitSettings.proxyPassword = it - } - } - ProxyUtils.setDefaultProxy() - Handler(Looper.getMainLooper()).postDelayed(500) { finish() } + private fun saveSettings() { + proxyPrefs.edit { + binding.proxyHost.text?.toString()?.takeIf { it.isNotEmpty() }.let { GitSettings.proxyHost = it } + binding.proxyUser.text?.toString()?.takeIf { it.isNotEmpty() }.let { GitSettings.proxyUsername = it } + binding.proxyPort.text?.toString()?.takeIf { it.isNotEmpty() }?.let { GitSettings.proxyPort = it.toInt() } + binding.proxyPassword.text?.toString()?.takeIf { it.isNotEmpty() }.let { GitSettings.proxyPassword = it } } + ProxyUtils.setDefaultProxy() + Handler(Looper.getMainLooper()).postDelayed(500) { finish() } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/AutofillSettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/AutofillSettings.kt index 20ca403c..ee6b468b 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/AutofillSettings.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/AutofillSettings.kt @@ -33,94 +33,94 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys class AutofillSettings(private val activity: FragmentActivity) : SettingsProvider { - private val isAutofillServiceEnabled: Boolean - get() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false - return activity.autofillManager?.hasEnabledAutofillServices() == true - } + private val isAutofillServiceEnabled: Boolean + get() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false + return activity.autofillManager?.hasEnabledAutofillServices() == true + } - @RequiresApi(Build.VERSION_CODES.O) - private fun showAutofillDialog(pref: SwitchPreference) { - val observer = LifecycleEventObserver { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> { - pref.checked = isAutofillServiceEnabled - } - else -> { - } - } + @RequiresApi(Build.VERSION_CODES.O) + private fun showAutofillDialog(pref: SwitchPreference) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + pref.checked = isAutofillServiceEnabled } - MaterialAlertDialogBuilder(activity).run { - setTitle(R.string.pref_autofill_enable_title) - @SuppressLint("InflateParams") - val layout = - activity.layoutInflater.inflate(R.layout.oreo_autofill_instructions, null) - val supportedBrowsersTextView = - layout.findViewById(R.id.supportedBrowsers) - supportedBrowsersTextView.text = - getInstalledBrowsersWithAutofillSupportLevel(context).joinToString( - separator = "\n" - ) { - val appLabel = it.first - val supportDescription = when (it.second) { - BrowserAutofillSupportLevel.None -> activity.getString(R.string.oreo_autofill_no_support) - BrowserAutofillSupportLevel.FlakyFill -> activity.getString(R.string.oreo_autofill_flaky_fill_support) - BrowserAutofillSupportLevel.PasswordFill -> activity.getString(R.string.oreo_autofill_password_fill_support) - BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility -> activity.getString(R.string.oreo_autofill_password_fill_and_conditional_save_support) - BrowserAutofillSupportLevel.GeneralFill -> activity.getString(R.string.oreo_autofill_general_fill_support) - BrowserAutofillSupportLevel.GeneralFillAndSave -> activity.getString(R.string.oreo_autofill_general_fill_and_save_support) - } - "$appLabel: $supportDescription" - } - setView(layout) - setPositiveButton(R.string.dialog_ok) { _, _ -> - val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE).apply { - data = Uri.parse("package:${BuildConfig.APPLICATION_ID}") - } - activity.startActivity(intent) + else -> {} + } + } + MaterialAlertDialogBuilder(activity).run { + setTitle(R.string.pref_autofill_enable_title) + @SuppressLint("InflateParams") + val layout = activity.layoutInflater.inflate(R.layout.oreo_autofill_instructions, null) + val supportedBrowsersTextView = layout.findViewById(R.id.supportedBrowsers) + supportedBrowsersTextView.text = + getInstalledBrowsersWithAutofillSupportLevel(context).joinToString(separator = "\n") { + val appLabel = it.first + val supportDescription = + when (it.second) { + BrowserAutofillSupportLevel.None -> activity.getString(R.string.oreo_autofill_no_support) + BrowserAutofillSupportLevel.FlakyFill -> activity.getString(R.string.oreo_autofill_flaky_fill_support) + BrowserAutofillSupportLevel.PasswordFill -> + activity.getString(R.string.oreo_autofill_password_fill_support) + BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility -> + activity.getString(R.string.oreo_autofill_password_fill_and_conditional_save_support) + BrowserAutofillSupportLevel.GeneralFill -> activity.getString(R.string.oreo_autofill_general_fill_support) + BrowserAutofillSupportLevel.GeneralFillAndSave -> + activity.getString(R.string.oreo_autofill_general_fill_and_save_support) } - setNegativeButton(R.string.dialog_cancel, null) - setOnDismissListener { pref.checked = isAutofillServiceEnabled } - activity.lifecycle.addObserver(observer) - show() + "$appLabel: $supportDescription" } + setView(layout) + setPositiveButton(R.string.dialog_ok) { _, _ -> + val intent = + Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE).apply { + data = Uri.parse("package:${BuildConfig.APPLICATION_ID}") + } + activity.startActivity(intent) + } + setNegativeButton(R.string.dialog_cancel, null) + setOnDismissListener { pref.checked = isAutofillServiceEnabled } + activity.lifecycle.addObserver(observer) + show() } + } - override fun provideSettings(builder: PreferenceScreen.Builder) { - builder.apply { - switch(PreferenceKeys.AUTOFILL_ENABLE) { - titleRes = R.string.pref_autofill_enable_title - visible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - defaultValue = isAutofillServiceEnabled - onClick { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return@onClick true - if (isAutofillServiceEnabled) { - activity.autofillManager?.disableAutofillServices() - } else { - showAutofillDialog(this) - } - false - } - } - val values = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_values) - val titles = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_entries) - val items = values.zip(titles).map { SelectionItem(it.first, it.second, null) } - singleChoice(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE, items) { - initialSelection = DirectoryStructure.DEFAULT.value - dependency = PreferenceKeys.AUTOFILL_ENABLE - titleRes = R.string.oreo_autofill_preference_directory_structure - } - editText(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME) { - dependency = PreferenceKeys.AUTOFILL_ENABLE - titleRes = R.string.preference_default_username_title - summaryProvider = { activity.getString(R.string.preference_default_username_summary) } - } - editText(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES) { - dependency = PreferenceKeys.AUTOFILL_ENABLE - titleRes = R.string.preference_custom_public_suffixes_title - summaryProvider = { activity.getString(R.string.preference_custom_public_suffixes_summary) } - textInputHintRes = R.string.preference_custom_public_suffixes_hint - } + override fun provideSettings(builder: PreferenceScreen.Builder) { + builder.apply { + switch(PreferenceKeys.AUTOFILL_ENABLE) { + titleRes = R.string.pref_autofill_enable_title + visible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + defaultValue = isAutofillServiceEnabled + onClick { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return@onClick true + if (isAutofillServiceEnabled) { + activity.autofillManager?.disableAutofillServices() + } else { + showAutofillDialog(this) + } + false } + } + val values = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_values) + val titles = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_entries) + val items = values.zip(titles).map { SelectionItem(it.first, it.second, null) } + singleChoice(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE, items) { + initialSelection = DirectoryStructure.DEFAULT.value + dependency = PreferenceKeys.AUTOFILL_ENABLE + titleRes = R.string.oreo_autofill_preference_directory_structure + } + editText(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME) { + dependency = PreferenceKeys.AUTOFILL_ENABLE + titleRes = R.string.preference_default_username_title + summaryProvider = { activity.getString(R.string.preference_default_username_summary) } + } + editText(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES) { + dependency = PreferenceKeys.AUTOFILL_ENABLE + titleRes = R.string.preference_custom_public_suffixes_title + summaryProvider = { activity.getString(R.string.preference_custom_public_suffixes_summary) } + textInputHintRes = R.string.preference_custom_public_suffixes_hint + } } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt index a2d246f1..475d3b5e 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/DirectorySelectionActivity.kt @@ -20,37 +20,38 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys class DirectorySelectionActivity : AppCompatActivity() { - @Suppress("DEPRECATION") - private val directorySelectAction = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? -> - if (uri == null) return@registerForActivityResult - - d { "Selected repository URI is $uri" } - // TODO: This is fragile. Workaround until PasswordItem is backed by DocumentFile - val docId = DocumentsContract.getTreeDocumentId(uri) - val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - val path = if (split.size > 1) split[1] else split[0] - val repoPath = "${Environment.getExternalStorageDirectory()}/$path" - val prefs = sharedPrefs - - d { "Selected repository path is $repoPath" } - - if (Environment.getExternalStorageDirectory().path == repoPath) { - MaterialAlertDialogBuilder(this) - .setTitle(resources.getString(R.string.sdcard_root_warning_title)) - .setMessage(resources.getString(R.string.sdcard_root_warning_message)) - .setPositiveButton(resources.getString(R.string.sdcard_root_warning_remove_everything)) { _, _ -> - prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, uri.path) } - } - .setNegativeButton(R.string.dialog_cancel, null) - .show() - } - prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, repoPath) } - setResult(RESULT_OK) - finish() + @Suppress("DEPRECATION") + private val directorySelectAction = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? -> + if (uri == null) return@registerForActivityResult + + d { "Selected repository URI is $uri" } + // TODO: This is fragile. Workaround until PasswordItem is backed by DocumentFile + val docId = DocumentsContract.getTreeDocumentId(uri) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val path = if (split.size > 1) split[1] else split[0] + val repoPath = "${Environment.getExternalStorageDirectory()}/$path" + val prefs = sharedPrefs + + d { "Selected repository path is $repoPath" } + + if (Environment.getExternalStorageDirectory().path == repoPath) { + MaterialAlertDialogBuilder(this) + .setTitle(resources.getString(R.string.sdcard_root_warning_title)) + .setMessage(resources.getString(R.string.sdcard_root_warning_message)) + .setPositiveButton(resources.getString(R.string.sdcard_root_warning_remove_everything)) { _, _ -> + prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, uri.path) } + } + .setNegativeButton(R.string.dialog_cancel, null) + .show() + } + prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, repoPath) } + setResult(RESULT_OK) + finish() } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - directorySelectAction.launch(null) - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + directorySelectAction.launch(null) + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/GeneralSettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/GeneralSettings.kt index 64b9c3f1..39501d52 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/GeneralSettings.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/GeneralSettings.kt @@ -22,83 +22,85 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys class GeneralSettings(private val activity: FragmentActivity) : SettingsProvider { - override fun provideSettings(builder: PreferenceScreen.Builder) { - builder.apply { - val themeValues = activity.resources.getStringArray(R.array.app_theme_values) - val themeOptions = activity.resources.getStringArray(R.array.app_theme_options) - val themeItems = themeValues.zip(themeOptions).map { SelectionItem(it.first, it.second, null) } - singleChoice(PreferenceKeys.APP_THEME, themeItems) { - initialSelection = activity.resources.getString(R.string.app_theme_def) - titleRes = R.string.pref_app_theme_title - } + override fun provideSettings(builder: PreferenceScreen.Builder) { + builder.apply { + val themeValues = activity.resources.getStringArray(R.array.app_theme_values) + val themeOptions = activity.resources.getStringArray(R.array.app_theme_options) + val themeItems = themeValues.zip(themeOptions).map { SelectionItem(it.first, it.second, null) } + singleChoice(PreferenceKeys.APP_THEME, themeItems) { + initialSelection = activity.resources.getString(R.string.app_theme_def) + titleRes = R.string.pref_app_theme_title + } - val sortValues = activity.resources.getStringArray(R.array.sort_order_values) - val sortOptions = activity.resources.getStringArray(R.array.sort_order_entries) - val sortItems = sortValues.zip(sortOptions).map { SelectionItem(it.first, it.second, null) } - singleChoice(PreferenceKeys.SORT_ORDER, sortItems) { - initialSelection = sortValues[0] - titleRes = R.string.pref_sort_order_title - } + val sortValues = activity.resources.getStringArray(R.array.sort_order_values) + val sortOptions = activity.resources.getStringArray(R.array.sort_order_entries) + val sortItems = sortValues.zip(sortOptions).map { SelectionItem(it.first, it.second, null) } + singleChoice(PreferenceKeys.SORT_ORDER, sortItems) { + initialSelection = sortValues[0] + titleRes = R.string.pref_sort_order_title + } - checkBox(PreferenceKeys.FILTER_RECURSIVELY) { - titleRes = R.string.pref_recursive_filter_title - summaryRes = R.string.pref_recursive_filter_summary - defaultValue = true - } + checkBox(PreferenceKeys.FILTER_RECURSIVELY) { + titleRes = R.string.pref_recursive_filter_title + summaryRes = R.string.pref_recursive_filter_summary + defaultValue = true + } - checkBox(PreferenceKeys.SEARCH_ON_START) { - titleRes = R.string.pref_search_on_start_title - summaryRes = R.string.pref_search_on_start_summary - defaultValue = false - } + checkBox(PreferenceKeys.SEARCH_ON_START) { + titleRes = R.string.pref_search_on_start_title + summaryRes = R.string.pref_search_on_start_summary + defaultValue = false + } - checkBox(PreferenceKeys.SHOW_HIDDEN_CONTENTS) { - titleRes = R.string.pref_show_hidden_title - summaryRes = R.string.pref_show_hidden_summary - defaultValue = false - } + checkBox(PreferenceKeys.SHOW_HIDDEN_CONTENTS) { + titleRes = R.string.pref_show_hidden_title + summaryRes = R.string.pref_show_hidden_summary + defaultValue = false + } - checkBox(PreferenceKeys.BIOMETRIC_AUTH) { - titleRes = R.string.pref_biometric_auth_title - defaultValue = false - }.apply { - val canAuthenticate = BiometricAuthenticator.canAuthenticate(activity) - if (!canAuthenticate) { - enabled = false - checked = false - summaryRes = R.string.pref_biometric_auth_summary_error - } else { - summaryRes = R.string.pref_biometric_auth_summary - onClick { - enabled = false - val isChecked = checked - activity.sharedPrefs.edit { - BiometricAuthenticator.authenticate(activity) { result -> - when (result) { - is BiometricAuthenticator.Result.Success -> { - // Apply the changes - putBoolean(PreferenceKeys.BIOMETRIC_AUTH, checked) - enabled = true - } - else -> { - // If any error occurs, revert back to the previous state. This - // catch-all clause includes the cancellation case. - putBoolean(PreferenceKeys.BIOMETRIC_AUTH, !checked) - checked = !isChecked - enabled = true - } - } - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - activity.getSystemService()?.apply { - removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) - } - } - false + checkBox(PreferenceKeys.BIOMETRIC_AUTH) { + titleRes = R.string.pref_biometric_auth_title + defaultValue = false + } + .apply { + val canAuthenticate = BiometricAuthenticator.canAuthenticate(activity) + if (!canAuthenticate) { + enabled = false + checked = false + summaryRes = R.string.pref_biometric_auth_summary_error + } else { + summaryRes = R.string.pref_biometric_auth_summary + onClick { + enabled = false + val isChecked = checked + activity.sharedPrefs.edit { + BiometricAuthenticator.authenticate(activity) { result -> + when (result) { + is BiometricAuthenticator.Result.Success -> { + // Apply the changes + putBoolean(PreferenceKeys.BIOMETRIC_AUTH, checked) + enabled = true + } + else -> { + // If any error occurs, revert back to the previous + // state. This + // catch-all clause includes the cancellation case. + putBoolean(PreferenceKeys.BIOMETRIC_AUTH, !checked) + checked = !isChecked + enabled = true } + } + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + activity.getSystemService()?.apply { + removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) } + } + false } + } } } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/MiscSettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/MiscSettings.kt index faed1c3c..8921317b 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/MiscSettings.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/MiscSettings.kt @@ -23,54 +23,59 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys class MiscSettings(activity: FragmentActivity) : SettingsProvider { - private val storeExportAction = activity.registerForActivityResult(object : ActivityResultContracts.OpenDocumentTree() { + private val storeExportAction = + activity.registerForActivityResult( + object : ActivityResultContracts.OpenDocumentTree() { override fun createIntent(context: Context, input: Uri?): Intent { - return super.createIntent(context, input).apply { - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or - Intent.FLAG_GRANT_PREFIX_URI_PERMISSION - } + return super.createIntent(context, input).apply { + flags = + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + } } - }) { uri: Uri? -> - if (uri == null) return@registerForActivityResult - val targetDirectory = DocumentFile.fromTreeUri(activity.applicationContext, uri) + } + ) { uri: Uri? -> + if (uri == null) return@registerForActivityResult + val targetDirectory = DocumentFile.fromTreeUri(activity.applicationContext, uri) - if (targetDirectory != null) { - val service = Intent(activity.applicationContext, PasswordExportService::class.java).apply { - action = PasswordExportService.ACTION_EXPORT_PASSWORD - putExtra("uri", uri) - } + if (targetDirectory != null) { + val service = + Intent(activity.applicationContext, PasswordExportService::class.java).apply { + action = PasswordExportService.ACTION_EXPORT_PASSWORD + putExtra("uri", uri) + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity.startForegroundService(service) - } else { - activity.startService(service) - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activity.startForegroundService(service) + } else { + activity.startService(service) } + } } - override fun provideSettings(builder: PreferenceScreen.Builder) { - builder.apply { - pref(PreferenceKeys.EXPORT_PASSWORDS) { - titleRes = R.string.prefs_export_passwords_title - summaryRes = R.string.prefs_export_passwords_summary - onClick { - storeExportAction.launch(null) - true - } - } - checkBox(PreferenceKeys.CLEAR_CLIPBOARD_HISTORY) { - defaultValue = false - titleRes = R.string.pref_clear_clipboard_title - summaryRes = R.string.pref_clear_clipboard_summary - } - checkBox(PreferenceKeys.ENABLE_DEBUG_LOGGING) { - defaultValue = false - titleRes = R.string.pref_debug_logging_title - summaryRes = R.string.pref_debug_logging_summary - visible = !BuildConfig.DEBUG - } + override fun provideSettings(builder: PreferenceScreen.Builder) { + builder.apply { + pref(PreferenceKeys.EXPORT_PASSWORDS) { + titleRes = R.string.prefs_export_passwords_title + summaryRes = R.string.prefs_export_passwords_summary + onClick { + storeExportAction.launch(null) + true } + } + checkBox(PreferenceKeys.CLEAR_CLIPBOARD_HISTORY) { + defaultValue = false + titleRes = R.string.pref_clear_clipboard_title + summaryRes = R.string.pref_clear_clipboard_summary + } + checkBox(PreferenceKeys.ENABLE_DEBUG_LOGGING) { + defaultValue = false + titleRes = R.string.pref_debug_logging_title + summaryRes = R.string.pref_debug_logging_summary + visible = !BuildConfig.DEBUG + } } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt index 98748584..ca72e4b1 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/PasswordSettings.kt @@ -29,85 +29,90 @@ import java.io.File class PasswordSettings(private val activity: FragmentActivity) : SettingsProvider { - private val sharedPrefs by lazy(LazyThreadSafetyMode.NONE) { activity.sharedPrefs } - private val storeCustomXkpwdDictionaryAction = activity.registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> - if (uri == null) return@registerForActivityResult + private val sharedPrefs by lazy(LazyThreadSafetyMode.NONE) { activity.sharedPrefs } + private val storeCustomXkpwdDictionaryAction = + activity.registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + if (uri == null) return@registerForActivityResult - Toast.makeText( - activity, - activity.resources.getString(R.string.xkpwgen_custom_dict_imported, uri.path), - Toast.LENGTH_SHORT - ).show() + Toast.makeText( + activity, + activity.resources.getString(R.string.xkpwgen_custom_dict_imported, uri.path), + Toast.LENGTH_SHORT + ) + .show() - sharedPrefs.edit { putString(PreferenceKeys.PREF_KEY_CUSTOM_DICT, uri.toString()) } + sharedPrefs.edit { putString(PreferenceKeys.PREF_KEY_CUSTOM_DICT, uri.toString()) } - val inputStream = activity.contentResolver.openInputStream(uri) - val customDictFile = File(activity.filesDir.toString(), XkpwdDictionary.XKPWD_CUSTOM_DICT_FILE).outputStream() - inputStream?.copyTo(customDictFile, 1024) - inputStream?.close() - customDictFile.close() + val inputStream = activity.contentResolver.openInputStream(uri) + val customDictFile = File(activity.filesDir.toString(), XkpwdDictionary.XKPWD_CUSTOM_DICT_FILE).outputStream() + inputStream?.copyTo(customDictFile, 1024) + inputStream?.close() + customDictFile.close() } - override fun provideSettings(builder: PreferenceScreen.Builder) { - builder.apply { - val customDictPref = CheckBoxPreference(PreferenceKeys.PREF_KEY_IS_CUSTOM_DICT).apply { - titleRes = R.string.pref_xkpwgen_custom_wordlist_enabled_title - summaryRes = R.string.pref_xkpwgen_custom_dict_summary_off - summaryOnRes = R.string.pref_xkpwgen_custom_dict_summary_on - visible = sharedPrefs.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) == "xkpasswd" - onCheckedChange { - requestRebind() - true - } - } - val customDictPathPref = Preference(PreferenceKeys.PREF_KEY_CUSTOM_DICT).apply { - dependency = PreferenceKeys.PREF_KEY_IS_CUSTOM_DICT - titleRes = R.string.pref_xkpwgen_custom_dict_picker_title - summary = sharedPrefs.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT) - ?: activity.resources.getString(R.string.pref_xkpwgen_custom_dict_picker_summary) - visible = sharedPrefs.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) == "xkpasswd" - onClick { - storeCustomXkpwdDictionaryAction.launch(arrayOf("*/*")) - true - } - } - val values = activity.resources.getStringArray(R.array.pwgen_provider_values) - val labels = activity.resources.getStringArray(R.array.pwgen_provider_labels) - val items = values.zip(labels).map { SelectionItem(it.first, it.second, null) } - singleChoice( - PreferenceKeys.PREF_KEY_PWGEN_TYPE, - items, - ) { - initialSelection = "classic" - titleRes = R.string.pref_password_generator_type_title - onSelectionChange { selection -> - val xkpasswdEnabled = selection == "xkpasswd" - customDictPathPref.visible = xkpasswdEnabled - customDictPref.visible = xkpasswdEnabled - customDictPref.requestRebind() - customDictPathPref.requestRebind() - true - } - } - // We initialize them early and add them manually to be able to manually force a rebind - // when the password generator type is changed. - addPreferenceItem(customDictPref) - addPreferenceItem(customDictPathPref) - editText(PreferenceKeys.GENERAL_SHOW_TIME) { - titleRes = R.string.pref_clipboard_timeout_title - summaryProvider = { activity.getString(R.string.pref_clipboard_timeout_summary) } - textInputType = InputType.TYPE_CLASS_NUMBER - } - checkBox(PreferenceKeys.SHOW_PASSWORD) { - titleRes = R.string.show_password_pref_title - summaryRes = R.string.show_password_pref_summary - defaultValue = true - } - checkBox(PreferenceKeys.COPY_ON_DECRYPT) { - titleRes = R.string.pref_copy_title - summaryRes = R.string.pref_copy_summary - defaultValue = false - } + override fun provideSettings(builder: PreferenceScreen.Builder) { + builder.apply { + val customDictPref = + CheckBoxPreference(PreferenceKeys.PREF_KEY_IS_CUSTOM_DICT).apply { + titleRes = R.string.pref_xkpwgen_custom_wordlist_enabled_title + summaryRes = R.string.pref_xkpwgen_custom_dict_summary_off + summaryOnRes = R.string.pref_xkpwgen_custom_dict_summary_on + visible = sharedPrefs.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) == "xkpasswd" + onCheckedChange { + requestRebind() + true + } } + val customDictPathPref = + Preference(PreferenceKeys.PREF_KEY_CUSTOM_DICT).apply { + dependency = PreferenceKeys.PREF_KEY_IS_CUSTOM_DICT + titleRes = R.string.pref_xkpwgen_custom_dict_picker_title + summary = + sharedPrefs.getString(PreferenceKeys.PREF_KEY_CUSTOM_DICT) + ?: activity.resources.getString(R.string.pref_xkpwgen_custom_dict_picker_summary) + visible = sharedPrefs.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) == "xkpasswd" + onClick { + storeCustomXkpwdDictionaryAction.launch(arrayOf("*/*")) + true + } + } + val values = activity.resources.getStringArray(R.array.pwgen_provider_values) + val labels = activity.resources.getStringArray(R.array.pwgen_provider_labels) + val items = values.zip(labels).map { SelectionItem(it.first, it.second, null) } + singleChoice( + PreferenceKeys.PREF_KEY_PWGEN_TYPE, + items, + ) { + initialSelection = "classic" + titleRes = R.string.pref_password_generator_type_title + onSelectionChange { selection -> + val xkpasswdEnabled = selection == "xkpasswd" + customDictPathPref.visible = xkpasswdEnabled + customDictPref.visible = xkpasswdEnabled + customDictPref.requestRebind() + customDictPathPref.requestRebind() + true + } + } + // We initialize them early and add them manually to be able to manually force a rebind + // when the password generator type is changed. + addPreferenceItem(customDictPref) + addPreferenceItem(customDictPathPref) + editText(PreferenceKeys.GENERAL_SHOW_TIME) { + titleRes = R.string.pref_clipboard_timeout_title + summaryProvider = { activity.getString(R.string.pref_clipboard_timeout_summary) } + textInputType = InputType.TYPE_CLASS_NUMBER + } + checkBox(PreferenceKeys.SHOW_PASSWORD) { + titleRes = R.string.show_password_pref_title + summaryRes = R.string.show_password_pref_summary + defaultValue = true + } + checkBox(PreferenceKeys.COPY_ON_DECRYPT) { + titleRes = R.string.pref_copy_title + summaryRes = R.string.pref_copy_summary + defaultValue = false + } } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt index afd54298..3e99c890 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/RepositorySettings.kt @@ -37,168 +37,165 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys class RepositorySettings(private val activity: FragmentActivity) : SettingsProvider { - private val encryptedPreferences by lazy(LazyThreadSafetyMode.NONE) { activity.getEncryptedGitPrefs() } + private val encryptedPreferences by lazy(LazyThreadSafetyMode.NONE) { activity.getEncryptedGitPrefs() } - private fun launchActivity(clazz: Class) { - activity.startActivity(Intent(activity, clazz)) - } + private fun launchActivity(clazz: Class) { + activity.startActivity(Intent(activity, clazz)) + } - private fun selectExternalGitRepository() { - MaterialAlertDialogBuilder(activity) - .setTitle(activity.resources.getString(R.string.external_repository_dialog_title)) - .setMessage(activity.resources.getString(R.string.external_repository_dialog_text)) - .setPositiveButton(R.string.dialog_ok) { _, _ -> - launchActivity(DirectorySelectionActivity::class.java) - } - .setNegativeButton(R.string.dialog_cancel, null) - .show() - } + private fun selectExternalGitRepository() { + MaterialAlertDialogBuilder(activity) + .setTitle(activity.resources.getString(R.string.external_repository_dialog_title)) + .setMessage(activity.resources.getString(R.string.external_repository_dialog_text)) + .setPositiveButton(R.string.dialog_ok) { _, _ -> launchActivity(DirectorySelectionActivity::class.java) } + .setNegativeButton(R.string.dialog_cancel, null) + .show() + } - override fun provideSettings(builder: PreferenceScreen.Builder) { - builder.apply { - checkBox(PreferenceKeys.REBASE_ON_PULL) { - titleRes = R.string.pref_rebase_on_pull_title - summaryRes = R.string.pref_rebase_on_pull_summary - summaryOnRes = R.string.pref_rebase_on_pull_summary_on - defaultValue = true - } - pref(PreferenceKeys.GIT_SERVER_INFO) { - titleRes = R.string.pref_edit_git_server_settings - visible = PasswordRepository.isGitRepo() - onClick { - launchActivity(GitServerConfigActivity::class.java) - true - } - } - pref(PreferenceKeys.PROXY_SETTINGS) { - titleRes = R.string.pref_edit_proxy_settings - visible = GitSettings.url?.startsWith("https") == true && PasswordRepository.isGitRepo() - onClick { - launchActivity(ProxySelectorActivity::class.java) - true - } - } - pref(PreferenceKeys.GIT_CONFIG) { - titleRes = R.string.pref_edit_git_config - visible = PasswordRepository.isGitRepo() - onClick { - launchActivity(GitConfigActivity::class.java) - true - } - } - pref(PreferenceKeys.SSH_KEY) { - titleRes = R.string.pref_import_ssh_key_title - visible = PasswordRepository.isGitRepo() - onClick { - launchActivity(SshKeyImportActivity::class.java) - true - } - } - pref(PreferenceKeys.SSH_KEYGEN) { - titleRes = R.string.pref_ssh_keygen_title - onClick { - launchActivity(SshKeyGenActivity::class.java) - true - } - } - pref(PreferenceKeys.SSH_SEE_KEY) { - titleRes = R.string.pref_ssh_see_key_title - visible = PasswordRepository.isGitRepo() - onClick { - ShowSshKeyFragment().show(activity.supportFragmentManager, "public_key") - true - } - } - pref(PreferenceKeys.CLEAR_SAVED_PASS) { - fun Preference.updatePref() { - val sshPass = encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) - val httpsPass = encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD) - if (sshPass == null && httpsPass == null) { - visible = false - return - } - when { - httpsPass != null -> titleRes = R.string.clear_saved_passphrase_https - sshPass != null -> titleRes = R.string.clear_saved_passphrase_ssh - } - visible = true - requestRebind() - } - onClick { - updatePref() - true - } - updatePref() - } - pref(PreferenceKeys.SSH_OPENKEYSTORE_CLEAR_KEY_ID) { - titleRes = R.string.pref_title_openkeystore_clear_keyid - visible = activity.sharedPrefs.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID)?.isNotEmpty() - ?: false - onClick { - activity.sharedPrefs.edit { putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null) } - visible = false - true + override fun provideSettings(builder: PreferenceScreen.Builder) { + builder.apply { + checkBox(PreferenceKeys.REBASE_ON_PULL) { + titleRes = R.string.pref_rebase_on_pull_title + summaryRes = R.string.pref_rebase_on_pull_summary + summaryOnRes = R.string.pref_rebase_on_pull_summary_on + defaultValue = true + } + pref(PreferenceKeys.GIT_SERVER_INFO) { + titleRes = R.string.pref_edit_git_server_settings + visible = PasswordRepository.isGitRepo() + onClick { + launchActivity(GitServerConfigActivity::class.java) + true + } + } + pref(PreferenceKeys.PROXY_SETTINGS) { + titleRes = R.string.pref_edit_proxy_settings + visible = GitSettings.url?.startsWith("https") == true && PasswordRepository.isGitRepo() + onClick { + launchActivity(ProxySelectorActivity::class.java) + true + } + } + pref(PreferenceKeys.GIT_CONFIG) { + titleRes = R.string.pref_edit_git_config + visible = PasswordRepository.isGitRepo() + onClick { + launchActivity(GitConfigActivity::class.java) + true + } + } + pref(PreferenceKeys.SSH_KEY) { + titleRes = R.string.pref_import_ssh_key_title + visible = PasswordRepository.isGitRepo() + onClick { + launchActivity(SshKeyImportActivity::class.java) + true + } + } + pref(PreferenceKeys.SSH_KEYGEN) { + titleRes = R.string.pref_ssh_keygen_title + onClick { + launchActivity(SshKeyGenActivity::class.java) + true + } + } + pref(PreferenceKeys.SSH_SEE_KEY) { + titleRes = R.string.pref_ssh_see_key_title + visible = PasswordRepository.isGitRepo() + onClick { + ShowSshKeyFragment().show(activity.supportFragmentManager, "public_key") + true + } + } + pref(PreferenceKeys.CLEAR_SAVED_PASS) { + fun Preference.updatePref() { + val sshPass = encryptedPreferences.getString(PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE) + val httpsPass = encryptedPreferences.getString(PreferenceKeys.HTTPS_PASSWORD) + if (sshPass == null && httpsPass == null) { + visible = false + return + } + when { + httpsPass != null -> titleRes = R.string.clear_saved_passphrase_https + sshPass != null -> titleRes = R.string.clear_saved_passphrase_ssh + } + visible = true + requestRebind() + } + onClick { + updatePref() + true + } + updatePref() + } + pref(PreferenceKeys.SSH_OPENKEYSTORE_CLEAR_KEY_ID) { + titleRes = R.string.pref_title_openkeystore_clear_keyid + visible = activity.sharedPrefs.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID)?.isNotEmpty() ?: false + onClick { + activity.sharedPrefs.edit { putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null) } + visible = false + true + } + } + val deleteRepoPref = + pref(PreferenceKeys.GIT_DELETE_REPO) { + titleRes = R.string.pref_git_delete_repo_title + summaryRes = R.string.pref_git_delete_repo_summary + visible = !activity.sharedPrefs.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) + onClick { + val repoDir = PasswordRepository.getRepositoryDirectory() + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.pref_dialog_delete_title) + .setMessage(activity.getString(R.string.dialog_delete_msg, repoDir)) + .setCancelable(false) + .setPositiveButton(R.string.dialog_delete) { dialogInterface, _ -> + runCatching { + PasswordRepository.getRepositoryDirectory().deleteRecursively() + PasswordRepository.closeRepository() } - } - val deleteRepoPref = pref(PreferenceKeys.GIT_DELETE_REPO) { - titleRes = R.string.pref_git_delete_repo_title - summaryRes = R.string.pref_git_delete_repo_summary - visible = !activity.sharedPrefs.getBoolean(PreferenceKeys.GIT_EXTERNAL, false) - onClick { - val repoDir = PasswordRepository.getRepositoryDirectory() - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.pref_dialog_delete_title) - .setMessage(activity.getString(R.string.dialog_delete_msg, repoDir)) - .setCancelable(false) - .setPositiveButton(R.string.dialog_delete) { dialogInterface, _ -> - runCatching { - PasswordRepository.getRepositoryDirectory().deleteRecursively() - PasswordRepository.closeRepository() - }.onFailure { - it.message?.let { message -> - activity.snackbar(message = message) - } - } + .onFailure { it.message?.let { message -> activity.snackbar(message = message) } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - activity.getSystemService()?.apply { - removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) - } - } - activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) } - dialogInterface.cancel() - activity.finish() - } - .setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ -> run { dialogInterface.cancel() } } - .show() - true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + activity.getSystemService()?.apply { + removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) + } } - } - checkBox(PreferenceKeys.GIT_EXTERNAL) { - titleRes = R.string.pref_external_repository_title - summaryRes = R.string.pref_external_repository_summary - onCheckedChange { checked -> - deleteRepoPref.visible = !checked - deleteRepoPref.requestRebind() - PasswordRepository.closeRepository() - activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPO_CHANGED, true) } - true - } - } - pref(PreferenceKeys.GIT_EXTERNAL_REPO) { - val externalRepo = activity.sharedPrefs.getString(PreferenceKeys.GIT_EXTERNAL_REPO) - if (externalRepo != null) { - summary = externalRepo - } else { - summaryRes = R.string.pref_select_external_repository_summary_no_repo_selected - } - titleRes = R.string.pref_select_external_repository_title - dependency = PreferenceKeys.GIT_EXTERNAL - onClick { - selectExternalGitRepository() - true - } - } + activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) } + dialogInterface.cancel() + activity.finish() + } + .setNegativeButton(R.string.dialog_do_not_delete) { dialogInterface, _ -> + run { dialogInterface.cancel() } + } + .show() + true + } + } + checkBox(PreferenceKeys.GIT_EXTERNAL) { + titleRes = R.string.pref_external_repository_title + summaryRes = R.string.pref_external_repository_summary + onCheckedChange { checked -> + deleteRepoPref.visible = !checked + deleteRepoPref.requestRebind() + PasswordRepository.closeRepository() + activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPO_CHANGED, true) } + true + } + } + pref(PreferenceKeys.GIT_EXTERNAL_REPO) { + val externalRepo = activity.sharedPrefs.getString(PreferenceKeys.GIT_EXTERNAL_REPO) + if (externalRepo != null) { + summary = externalRepo + } else { + summaryRes = R.string.pref_select_external_repository_summary_no_repo_selected + } + titleRes = R.string.pref_select_external_repository_title + dependency = PreferenceKeys.GIT_EXTERNAL + onClick { + selectExternalGitRepository() + true } + } } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt index d593bd21..ceb6599b 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsActivity.kt @@ -17,77 +17,79 @@ import dev.msfjarvis.aps.util.extensions.viewBinding class SettingsActivity : AppCompatActivity() { - private val miscSettings = MiscSettings(this) - private val autofillSettings = AutofillSettings(this) - private val passwordSettings = PasswordSettings(this) - private val repositorySettings = RepositorySettings(this) - private val generalSettings = GeneralSettings(this) + private val miscSettings = MiscSettings(this) + private val autofillSettings = AutofillSettings(this) + private val passwordSettings = PasswordSettings(this) + private val repositorySettings = RepositorySettings(this) + private val generalSettings = GeneralSettings(this) - private val binding by viewBinding(ActivityPreferenceRecyclerviewBinding::inflate) - private val preferencesAdapter: PreferencesAdapter - get() = binding.preferenceRecyclerView.adapter as PreferencesAdapter + private val binding by viewBinding(ActivityPreferenceRecyclerviewBinding::inflate) + private val preferencesAdapter: PreferencesAdapter + get() = binding.preferenceRecyclerView.adapter as PreferencesAdapter - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - val screen = screen(this) { - subScreen { - titleRes = R.string.pref_category_general_title - iconRes = R.drawable.app_settings_alt_24px - generalSettings.provideSettings(this) - } - subScreen { - titleRes = R.string.pref_category_autofill_title - iconRes = R.drawable.ic_wysiwyg_24px - autofillSettings.provideSettings(this) - } - subScreen { - titleRes = R.string.pref_category_passwords_title - iconRes = R.drawable.ic_lock_open_24px - passwordSettings.provideSettings(this) - } - subScreen { - titleRes = R.string.pref_category_repository_title - iconRes = R.drawable.ic_call_merge_24px - repositorySettings.provideSettings(this) - } - subScreen { - titleRes = R.string.pref_category_misc_title - iconRes = R.drawable.ic_miscellaneous_services_24px - miscSettings.provideSettings(this) - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + val screen = + screen(this) { + subScreen { + titleRes = R.string.pref_category_general_title + iconRes = R.drawable.app_settings_alt_24px + generalSettings.provideSettings(this) } - val adapter = PreferencesAdapter(screen) - adapter.onScreenChangeListener = PreferencesAdapter.OnScreenChangeListener { subScreen, entering -> - supportActionBar?.title = if (!entering) { - getString(R.string.action_settings) - } else { - getString(subScreen.titleRes) - } + subScreen { + titleRes = R.string.pref_category_autofill_title + iconRes = R.drawable.ic_wysiwyg_24px + autofillSettings.provideSettings(this) } - savedInstanceState?.getParcelable("adapter") - ?.let(adapter::loadSavedState) - binding.preferenceRecyclerView.adapter = adapter - } + subScreen { + titleRes = R.string.pref_category_passwords_title + iconRes = R.drawable.ic_lock_open_24px + passwordSettings.provideSettings(this) + } + subScreen { + titleRes = R.string.pref_category_repository_title + iconRes = R.drawable.ic_call_merge_24px + repositorySettings.provideSettings(this) + } + subScreen { + titleRes = R.string.pref_category_misc_title + iconRes = R.drawable.ic_miscellaneous_services_24px + miscSettings.provideSettings(this) + } + } + val adapter = PreferencesAdapter(screen) + adapter.onScreenChangeListener = + PreferencesAdapter.OnScreenChangeListener { subScreen, entering -> + supportActionBar?.title = + if (!entering) { + getString(R.string.action_settings) + } else { + getString(subScreen.titleRes) + } + } + savedInstanceState?.getParcelable("adapter")?.let(adapter::loadSavedState) + binding.preferenceRecyclerView.adapter = adapter + } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelable("adapter", preferencesAdapter.getSavedState()) - } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelable("adapter", preferencesAdapter.getSavedState()) + } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> if (!preferencesAdapter.goBack()) { - super.onOptionsItemSelected(item) - } else { - true - } - else -> super.onOptionsItemSelected(item) + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> + if (!preferencesAdapter.goBack()) { + super.onOptionsItemSelected(item) + } else { + true } + else -> super.onOptionsItemSelected(item) } + } - override fun onBackPressed() { - if (!preferencesAdapter.goBack()) - super.onBackPressed() - } + override fun onBackPressed() { + if (!preferencesAdapter.goBack()) super.onBackPressed() + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsProvider.kt b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsProvider.kt index 3599703e..61b11064 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsProvider.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/settings/SettingsProvider.kt @@ -7,13 +7,9 @@ package dev.msfjarvis.aps.ui.settings import de.Maxr1998.modernpreferences.PreferenceScreen -/** - * Used to generate a uniform API for all settings UI classes. - */ +/** Used to generate a uniform API for all settings UI classes. */ interface SettingsProvider { - /** - * Inserts the settings items for the class into the given [builder]. - */ - fun provideSettings(builder: PreferenceScreen.Builder) + /** Inserts the settings items for the class into the given [builder]. */ + fun provideSettings(builder: PreferenceScreen.Builder) } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/ShowSshKeyFragment.kt b/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/ShowSshKeyFragment.kt index 42346ccc..ee54febe 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/ShowSshKeyFragment.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/ShowSshKeyFragment.kt @@ -14,25 +14,24 @@ import dev.msfjarvis.aps.util.git.sshj.SshKey class ShowSshKeyFragment : DialogFragment() { - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val activity = requireActivity() - val publicKey = SshKey.sshPublicKey - return MaterialAlertDialogBuilder(requireActivity()).run { - setMessage(getString(R.string.ssh_keygen_message, publicKey)) - setTitle(R.string.your_public_key) - setNegativeButton(R.string.ssh_keygen_later) { _, _ -> - (activity as? SshKeyGenActivity)?.finish() - } - setPositiveButton(R.string.ssh_keygen_share) { _, _ -> - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, publicKey) - } - startActivity(Intent.createChooser(sendIntent, null)) - (activity as? SshKeyGenActivity)?.finish() - } - create() - } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val activity = requireActivity() + val publicKey = SshKey.sshPublicKey + return MaterialAlertDialogBuilder(requireActivity()).run { + setMessage(getString(R.string.ssh_keygen_message, publicKey)) + setTitle(R.string.your_public_key) + setNegativeButton(R.string.ssh_keygen_later) { _, _ -> (activity as? SshKeyGenActivity)?.finish() } + setPositiveButton(R.string.ssh_keygen_share) { _, _ -> + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, publicKey) + } + startActivity(Intent.createChooser(sendIntent, null)) + (activity as? SshKeyGenActivity)?.finish() + } + create() } + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyGenActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyGenActivity.kt index fb977cd1..c2025538 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyGenActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyGenActivity.kt @@ -30,135 +30,122 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext private enum class KeyGenType(val generateKey: suspend (requireAuthentication: Boolean) -> Unit) { - Rsa({ requireAuthentication -> - SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication) - }), - Ecdsa({ requireAuthentication -> - SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Ecdsa, requireAuthentication) - }), - Ed25519({ requireAuthentication -> - SshKey.generateKeystoreWrappedEd25519Key(requireAuthentication) - }), + Rsa({ requireAuthentication -> SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication) }), + Ecdsa({ requireAuthentication -> SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Ecdsa, requireAuthentication) }), + Ed25519({ requireAuthentication -> SshKey.generateKeystoreWrappedEd25519Key(requireAuthentication) }), } class SshKeyGenActivity : AppCompatActivity() { - private var keyGenType = KeyGenType.Ecdsa - private val binding by viewBinding(ActivitySshKeygenBinding::inflate) + private var keyGenType = KeyGenType.Ecdsa + private val binding by viewBinding(ActivitySshKeygenBinding::inflate) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - with(binding) { - generate.setOnClickListener { - if (SshKey.exists) { - MaterialAlertDialogBuilder(this@SshKeyGenActivity).run { - setTitle(R.string.ssh_keygen_existing_title) - setMessage(R.string.ssh_keygen_existing_message) - setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> - lifecycleScope.launch { - generate() - } - } - setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> - finish() - } - show() - } - } else { - lifecycleScope.launch { - generate() - } - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + with(binding) { + generate.setOnClickListener { + if (SshKey.exists) { + MaterialAlertDialogBuilder(this@SshKeyGenActivity).run { + setTitle(R.string.ssh_keygen_existing_title) + setMessage(R.string.ssh_keygen_existing_message) + setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> lifecycleScope.launch { generate() } } + setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> finish() } + show() + } + } else { + lifecycleScope.launch { generate() } + } + } + keyTypeGroup.check(R.id.key_type_ecdsa) + keyTypeExplanation.setText(R.string.ssh_keygen_explanation_ecdsa) + keyTypeGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (isChecked) { + keyGenType = + when (checkedId) { + R.id.key_type_ed25519 -> KeyGenType.Ed25519 + R.id.key_type_ecdsa -> KeyGenType.Ecdsa + R.id.key_type_rsa -> KeyGenType.Rsa + else -> throw IllegalStateException("Impossible key type selection") } - keyTypeGroup.check(R.id.key_type_ecdsa) - keyTypeExplanation.setText(R.string.ssh_keygen_explanation_ecdsa) - keyTypeGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> - if (isChecked) { - keyGenType = when (checkedId) { - R.id.key_type_ed25519 -> KeyGenType.Ed25519 - R.id.key_type_ecdsa -> KeyGenType.Ecdsa - R.id.key_type_rsa -> KeyGenType.Rsa - else -> throw IllegalStateException("Impossible key type selection") - } - keyTypeExplanation.setText(when (keyGenType) { - KeyGenType.Ed25519 -> R.string.ssh_keygen_explanation_ed25519 - KeyGenType.Ecdsa -> R.string.ssh_keygen_explanation_ecdsa - KeyGenType.Rsa -> R.string.ssh_keygen_explanation_rsa - }) - } + keyTypeExplanation.setText( + when (keyGenType) { + KeyGenType.Ed25519 -> R.string.ssh_keygen_explanation_ed25519 + KeyGenType.Ecdsa -> R.string.ssh_keygen_explanation_ecdsa + KeyGenType.Rsa -> R.string.ssh_keygen_explanation_rsa } - keyRequireAuthentication.isEnabled = keyguardManager.isDeviceSecure - keyRequireAuthentication.isChecked = keyRequireAuthentication.isEnabled + ) } + } + keyRequireAuthentication.isEnabled = keyguardManager.isDeviceSecure + keyRequireAuthentication.isChecked = keyRequireAuthentication.isEnabled } + } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - onBackPressed() - true - } - else -> super.onOptionsItemSelected(item) - } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) } + } - private suspend fun generate() { - binding.generate.apply { - text = getString(R.string.ssh_key_gen_generating_progress) - isEnabled = false - } - binding.generate.text = getString(R.string.ssh_key_gen_generating_progress) - val result = runCatching { - withContext(Dispatchers.IO) { - val requireAuthentication = binding.keyRequireAuthentication.isChecked - if (requireAuthentication) { - val result = withContext(Dispatchers.Main) { - suspendCoroutine { cont -> - BiometricAuthenticator.authenticate(this@SshKeyGenActivity, R.string.biometric_prompt_title_ssh_keygen) { - cont.resume(it) - } - } - } - if (result !is BiometricAuthenticator.Result.Success) - throw UserNotAuthenticatedException(getString(R.string.biometric_auth_generic_failure)) - } - keyGenType.generateKey(requireAuthentication) + private suspend fun generate() { + binding.generate.apply { + text = getString(R.string.ssh_key_gen_generating_progress) + isEnabled = false + } + binding.generate.text = getString(R.string.ssh_key_gen_generating_progress) + val result = runCatching { + withContext(Dispatchers.IO) { + val requireAuthentication = binding.keyRequireAuthentication.isChecked + if (requireAuthentication) { + val result = + withContext(Dispatchers.Main) { + suspendCoroutine { cont -> + BiometricAuthenticator.authenticate( + this@SshKeyGenActivity, + R.string.biometric_prompt_title_ssh_keygen + ) { cont.resume(it) } + } } + if (result !is BiometricAuthenticator.Result.Success) + throw UserNotAuthenticatedException(getString(R.string.biometric_auth_generic_failure)) } - getEncryptedGitPrefs().edit { - remove("ssh_key_local_passphrase") - } - binding.generate.apply { - text = getString(R.string.ssh_keygen_generate) - isEnabled = true - } - result.fold( - success = { - ShowSshKeyFragment().show(supportFragmentManager, "public_key") - }, - failure = { e -> - e.printStackTrace() - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.error_generate_ssh_key)) - .setMessage(getString(R.string.ssh_key_error_dialog_text) + e.message) - .setPositiveButton(getString(R.string.dialog_ok)) { _, _ -> - setResult(RESULT_OK) - finish() - } - .show() - }, - ) - hideKeyboard() + keyGenType.generateKey(requireAuthentication) + } } + getEncryptedGitPrefs().edit { remove("ssh_key_local_passphrase") } + binding.generate.apply { + text = getString(R.string.ssh_keygen_generate) + isEnabled = true + } + result.fold( + success = { ShowSshKeyFragment().show(supportFragmentManager, "public_key") }, + failure = { e -> + e.printStackTrace() + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.error_generate_ssh_key)) + .setMessage(getString(R.string.ssh_key_error_dialog_text) + e.message) + .setPositiveButton(getString(R.string.dialog_ok)) { _, _ -> + setResult(RESULT_OK) + finish() + } + .show() + }, + ) + hideKeyboard() + } - private fun hideKeyboard() { - val imm = getSystemService() ?: return - var view = currentFocus - if (view == null) { - view = View(this) - } - imm.hideSoftInputFromWindow(view.windowToken, 0) + private fun hideKeyboard() { + val imm = getSystemService() ?: return + var view = currentFocus + if (view == null) { + view = View(this) } + imm.hideSoftInputFromWindow(view.windowToken, 0) + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyImportActivity.kt b/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyImportActivity.kt index 2d482d3c..bf9f6eda 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyImportActivity.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/sshkeygen/SshKeyImportActivity.kt @@ -18,44 +18,44 @@ import dev.msfjarvis.aps.util.git.sshj.SshKey class SshKeyImportActivity : AppCompatActivity() { - private val sshKeyImportAction = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> - if (uri == null) { - finish() - return@registerForActivityResult - } - runCatching { - SshKey.import(uri) - Toast.makeText(this, resources.getString(R.string.ssh_key_success_dialog_title), Toast.LENGTH_LONG).show() - setResult(RESULT_OK) - finish() - }.onFailure { e -> - MaterialAlertDialogBuilder(this) - .setTitle(resources.getString(R.string.ssh_key_error_dialog_title)) - .setMessage(e.message) - .setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> finish() } - .show() + private val sshKeyImportAction = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> + if (uri == null) { + finish() + return@registerForActivityResult + } + runCatching { + SshKey.import(uri) + Toast.makeText(this, resources.getString(R.string.ssh_key_success_dialog_title), Toast.LENGTH_LONG).show() + setResult(RESULT_OK) + finish() + } + .onFailure { e -> + MaterialAlertDialogBuilder(this) + .setTitle(resources.getString(R.string.ssh_key_error_dialog_title)) + .setMessage(e.message) + .setPositiveButton(resources.getString(R.string.dialog_ok)) { _, _ -> finish() } + .show() } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (SshKey.exists) { - MaterialAlertDialogBuilder(this).run { - setTitle(R.string.ssh_keygen_existing_title) - setMessage(R.string.ssh_keygen_existing_message) - setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> - importSshKey() - } - setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> finish() } - setOnCancelListener { finish() } - show() - } - } else { - importSshKey() - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (SshKey.exists) { + MaterialAlertDialogBuilder(this).run { + setTitle(R.string.ssh_keygen_existing_title) + setMessage(R.string.ssh_keygen_existing_message) + setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> importSshKey() } + setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> finish() } + setOnCancelListener { finish() } + show() + } + } else { + importSshKey() } + } - private fun importSshKey() { - sshKeyImportAction.launch(arrayOf("*/*")) - } + private fun importSshKey() { + sshKeyImportAction.launch(arrayOf("*/*")) + } } diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/util/OnOffItemAnimator.kt b/app/src/main/java/dev/msfjarvis/aps/ui/util/OnOffItemAnimator.kt index 590b376f..494a9ed7 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/util/OnOffItemAnimator.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/util/OnOffItemAnimator.kt @@ -9,63 +9,63 @@ import androidx.recyclerview.widget.RecyclerView class OnOffItemAnimator : DefaultItemAnimator() { - var isEnabled: Boolean = true - set(value) { - // Defer update until no animation is running anymore. - isRunning { field = value } - } - - private fun dontAnimate(viewHolder: RecyclerView.ViewHolder): Boolean { - dispatchAnimationFinished(viewHolder) - return false + var isEnabled: Boolean = true + set(value) { + // Defer update until no animation is running anymore. + isRunning { field = value } } - override fun animateAppearance( - viewHolder: RecyclerView.ViewHolder, - preLayoutInfo: ItemHolderInfo?, - postLayoutInfo: ItemHolderInfo - ): Boolean { - return if (isEnabled) { - super.animateAppearance(viewHolder, preLayoutInfo, postLayoutInfo) - } else { - dontAnimate(viewHolder) - } + private fun dontAnimate(viewHolder: RecyclerView.ViewHolder): Boolean { + dispatchAnimationFinished(viewHolder) + return false + } + + override fun animateAppearance( + viewHolder: RecyclerView.ViewHolder, + preLayoutInfo: ItemHolderInfo?, + postLayoutInfo: ItemHolderInfo + ): Boolean { + return if (isEnabled) { + super.animateAppearance(viewHolder, preLayoutInfo, postLayoutInfo) + } else { + dontAnimate(viewHolder) } + } - override fun animateChange( - oldHolder: RecyclerView.ViewHolder, - newHolder: RecyclerView.ViewHolder, - preInfo: ItemHolderInfo, - postInfo: ItemHolderInfo - ): Boolean { - return if (isEnabled) { - super.animateChange(oldHolder, newHolder, preInfo, postInfo) - } else { - dontAnimate(oldHolder) - } + override fun animateChange( + oldHolder: RecyclerView.ViewHolder, + newHolder: RecyclerView.ViewHolder, + preInfo: ItemHolderInfo, + postInfo: ItemHolderInfo + ): Boolean { + return if (isEnabled) { + super.animateChange(oldHolder, newHolder, preInfo, postInfo) + } else { + dontAnimate(oldHolder) } + } - override fun animateDisappearance( - viewHolder: RecyclerView.ViewHolder, - preLayoutInfo: ItemHolderInfo, - postLayoutInfo: ItemHolderInfo? - ): Boolean { - return if (isEnabled) { - super.animateDisappearance(viewHolder, preLayoutInfo, postLayoutInfo) - } else { - dontAnimate(viewHolder) - } + override fun animateDisappearance( + viewHolder: RecyclerView.ViewHolder, + preLayoutInfo: ItemHolderInfo, + postLayoutInfo: ItemHolderInfo? + ): Boolean { + return if (isEnabled) { + super.animateDisappearance(viewHolder, preLayoutInfo, postLayoutInfo) + } else { + dontAnimate(viewHolder) } + } - override fun animatePersistence( - viewHolder: RecyclerView.ViewHolder, - preInfo: ItemHolderInfo, - postInfo: ItemHolderInfo - ): Boolean { - return if (isEnabled) { - super.animatePersistence(viewHolder, preInfo, postInfo) - } else { - dontAnimate(viewHolder) - } + override fun animatePersistence( + viewHolder: RecyclerView.ViewHolder, + preInfo: ItemHolderInfo, + postInfo: ItemHolderInfo + ): Boolean { + return if (isEnabled) { + super.animatePersistence(viewHolder, preInfo, postInfo) + } else { + dontAnimate(viewHolder) } + } } -- cgit v1.2.3