Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file.
- Add support for better, more secure Keyex's and MACs with a brand new SSH backend
- Allow manually marking domains for subdomain-level association. This will allow you to keep separate passwords for `site1.example.com` and `site2.example.com` and have them show as such in Autofill.
- Provide better messages for OpenKeychain errors
- Rename passwords
- Rename passwords and categories

### Changed
- **BREAKING**: Remove support for HOTP/TOTP secrets - Please use FIDO keys or a dedicated app like [Aegis](https://github.com/beemdevelopment/Aegis) or [andOTP](https://github.com/andOTP/andOTP)
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
// 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(requireContext())
.all { it.type == PasswordItem.TYPE_CATEGORY }
return true
}

Expand All @@ -170,6 +173,11 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
requireStore().movePasswords(recyclerAdapter.getSelectedItems(requireContext()))
false
}
R.id.menu_edit_password -> {
requireStore().renameCategory(recyclerAdapter.getSelectedItems(requireContext()))
mode.finish()
false
}
else -> false
}
}
Expand Down
74 changes: 70 additions & 4 deletions app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import com.github.ajalt.timberkt.i
import com.github.ajalt.timberkt.w
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel
import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel
Expand All @@ -66,6 +67,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository.Companion.isInitialized
import com.zeapo.pwdstore.utils.PasswordRepository.PasswordSortOrder.Companion.getSortOrder
import com.zeapo.pwdstore.utils.commitChange
import com.zeapo.pwdstore.utils.listFilesRecursively
import com.zeapo.pwdstore.utils.requestInputFocusOnView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -628,15 +630,15 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
)
.setPositiveButton(R.string.dialog_ok) { _, _ ->
launch(Dispatchers.IO) {
movePassword(source, destinationFile)
moveFile(source, destinationFile)
}
}
.setNegativeButton(R.string.dialog_cancel, null)
.show()
}
} else {
launch(Dispatchers.IO) {
movePassword(source, destinationFile)
moveFile(source, destinationFile)
}
}
}
Expand Down Expand Up @@ -664,6 +666,69 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
}.launch(intent)
}

private fun isInsideRepository(file: File): Boolean {
return file.canonicalPath.contains(getRepositoryDirectory(this).canonicalPath)
}

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_category_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<TextInputEditText>(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)
!isInsideRepository(newCategory) -> renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo)
else -> lifecycleScope.launch(Dispatchers.IO) {
moveFile(oldCategory.file, newCategory)
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<TextInputEditText>(R.id.folder_name_text)
dialog.show()
}

fun renameCategory(categories: List<PasswordItem>) {
for (oldCategory in categories) {
renameCategory(oldCategory)
}
}

/**
* Resets navigation to the repository root and refreshes the password list accordingly.
*
Expand Down Expand Up @@ -736,8 +801,9 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
super.onActivityResult(requestCode, resultCode, data)
}

private suspend fun movePassword(source: File, destinationFile: File) {
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.
Expand All @@ -746,7 +812,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
mapOf(source to destinationFile)
}
if (!source.renameTo(destinationFile)) {
e { "Something went wrong while moving." }
e { "Something went wrong while moving $source to $destinationFile." }
withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(this@PasswordStore)
.setTitle(R.string.password_move_error_title)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class FolderCreationDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext())
alertDialogBuilder.setTitle(R.string.title_create_folder)
alertDialogBuilder.setView(R.layout.folder_creation_dialog_fragment)
alertDialogBuilder.setView(R.layout.folder_dialog_fragment)
alertDialogBuilder.setPositiveButton(getString(R.string.button_create)) { _, _ ->
createDirectory(requireArguments().getString(CURRENT_DIR_EXTRA)!!)
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/menu/context_pass.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@
android:title="@string/delete"
app:showAsAction="ifRoom" />

<item
android:id="@+id/menu_edit_password"
android:icon="@drawable/ic_edit_white_24dp"
android:title="@string/edit"
app:showAsAction="ifRoom" />
</menu>
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@
<string name="dialog_positive">Go to Settings</string>
<string name="dialog_negative">Go back</string>
<string name="dialog_cancel">Cancel</string>
<string name="dialog_skip">Skip</string>
<string name="git_sync">Synchronize repository</string>
<string name="git_pull">Pull from remote</string>
<string name="git_push">Push to remote</string>
Expand Down Expand Up @@ -323,6 +324,11 @@
<string name="pref_show_hidden_title">Show hidden folders</string>
<string name="pref_show_hidden_summary">Include hidden directories in the password list</string>
<string name="title_create_folder">Create folder</string>
<string name="title_rename_folder">Rename folder</string>
<string name="message_category_error_empty_field">Category name can\'t be empty</string>
<string name="message_category_error_category_exists">Category name already exists</string>
<string name="message_category_error_destination_outside_repo">Destination must be within the repository</string>
<string name="message_rename_folder">Enter destination for %1$s</string>
<string name="button_create">Create</string>
<string name="pref_search_on_start">Open search on start</string>
<string name="pref_search_on_start_hint">Open search bar when app is launched</string>
Expand Down