aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/ui
diff options
context:
space:
mode:
authorFabian Henneke <FabianHenneke@users.noreply.github.com>2020-04-10 13:18:42 +0200
committerGitHub <noreply@github.com>2020-04-10 16:48:42 +0530
commit575ef8472654f49dd60f0e6909164dd8dbd098fb (patch)
treebd94860a1dcb4a8c630ff5954c262716ac307b7a /app/src/main/java/com/zeapo/pwdstore/ui
parent2738d7500fa0d4d925f698a04045d930c27a2d59 (diff)
downloadHardenedPass-575ef8472654f49dd60f0e6909164dd8dbd098fb.tar.gz
HardenedPass-575ef8472654f49dd60f0e6909164dd8dbd098fb.zip
Modernize legacy RecyclerView adapters (#694)
* Modernize legacy RecyclerView adapters Introduces new adapters based on the SearchableRepositoryViewModel and using androidx.recyclerview.selection for multiselection support. The following positive effects in behavior are observable to end-users: - Search and navigation actions are executed on IO threads. - RecyclerViews are now animated during searches (but not navigations). - Exact scroll position is restored when navigating back. - The ActionBar title is updated with the current folder name. The following negative effects may warrant attention: - Support for the "always search from root" setting has been removed. - Due to a limitation of the fast scroll dependency, using the scroller may result in unwanted multiselections. If this is not fixed in the library, native fast scroller capabilities could be used, but these are more limited in appearance and to not offer popups. * Fix lint * Fix FastScroller/SelectionTracker incompatibility * Immediately react to settings changes * List directory entries when search term is blank * Use isEmpty() instead of == "" * Replace adapter inheritance with builders and fix selection drags * Remove dividers in password lists * Run spotlessApply * Use a more logical string in action mode * Commonize and constify path bundle key * Make lambda parameter name explicit Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/ui')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/OnOffItemAnimator.kt71
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/adapters/EntryRecyclerAdapter.kt148
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/adapters/FolderRecyclerAdapter.kt23
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt88
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordRecyclerAdapter.kt120
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt2
6 files changed, 160 insertions, 292 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/OnOffItemAnimator.kt b/app/src/main/java/com/zeapo/pwdstore/ui/OnOffItemAnimator.kt
new file mode 100644
index 00000000..e394e656
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/ui/OnOffItemAnimator.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.ui
+
+import androidx.recyclerview.widget.DefaultItemAnimator
+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
+ }
+
+ 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 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)
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/adapters/EntryRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/ui/adapters/EntryRecyclerAdapter.kt
deleted file mode 100644
index 9cf477e2..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/ui/adapters/EntryRecyclerAdapter.kt
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package com.zeapo.pwdstore.ui.adapters
-
-import android.content.SharedPreferences
-import android.text.SpannableString
-import android.text.style.RelativeSizeSpan
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.appcompat.widget.AppCompatImageView
-import androidx.appcompat.widget.AppCompatTextView
-import androidx.preference.PreferenceManager
-import androidx.recyclerview.widget.RecyclerView
-import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.utils.PasswordItem
-import com.zeapo.pwdstore.widget.MultiselectableConstraintLayout
-import java.io.File
-import java.util.ArrayList
-import java.util.Locale
-import java.util.TreeSet
-import me.zhanghai.android.fastscroll.PopupTextProvider
-
-abstract class EntryRecyclerAdapter internal constructor(val values: ArrayList<PasswordItem>) : RecyclerView.Adapter<EntryRecyclerAdapter.ViewHolder>(), PopupTextProvider {
- internal val selectedItems: MutableSet<Int> = TreeSet()
- internal var settings: SharedPreferences? = null
-
- // Return the size of your dataset (invoked by the layout manager)
- override fun getItemCount(): Int {
- return values.size
- }
-
- override fun getPopupText(position: Int): String {
- return values[position].name[0].toString().toUpperCase(Locale.getDefault())
- }
-
- fun clear() {
- this.values.clear()
- this.notifyDataSetChanged()
- }
-
- fun addAll(list: ArrayList<PasswordItem>) {
- this.values.addAll(list)
- this.notifyDataSetChanged()
- }
-
- fun add(item: PasswordItem) {
- this.values.add(item)
- this.notifyItemInserted(itemCount)
- }
-
- internal fun toggleSelection(position: Int) {
- if (!selectedItems.remove(position)) {
- selectedItems.add(position)
- }
- }
-
- // use this after an item is removed to update the positions of items in set
- // that followed the removed position
- fun updateSelectedItems(position: Int, selectedItems: MutableSet<Int>) {
- val temp = TreeSet<Int>()
- for (selected in selectedItems) {
- if (selected > position) {
- temp.add(selected - 1)
- } else {
- temp.add(selected)
- }
- }
- selectedItems.clear()
- selectedItems.addAll(temp)
- }
-
- fun remove(position: Int) {
- this.values.removeAt(position)
- this.notifyItemRemoved(position)
-
- // keep selectedItems updated so we know what to notifyItemChanged
- // (instead of just using notifyDataSetChanged)
- updateSelectedItems(position, selectedItems)
- }
-
- internal open fun getOnLongClickListener(holder: ViewHolder, pass: PasswordItem): View.OnLongClickListener {
- return View.OnLongClickListener { false }
- }
-
- // Replace the contents of a view (invoked by the layout manager)
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- settings = settings
- ?: PreferenceManager.getDefaultSharedPreferences(holder.view.context.applicationContext)
- val pass = values[position]
- val showHidden = settings?.getBoolean("show_hidden_folders", false) ?: false
- holder.name.text = pass.toString()
- if (pass.type == PasswordItem.TYPE_CATEGORY) {
- holder.typeImage.setImageResource(R.drawable.ic_multiple_files_24dp)
- holder.folderIndicator.visibility = View.VISIBLE
- val children = pass.file.listFiles { pathname ->
- !(!showHidden && (pathname.isDirectory && pathname.isHidden))
- } ?: emptyArray<File>()
- val childCount = children.size
- holder.childCount.visibility = if (childCount > 0) View.VISIBLE else View.GONE
- holder.childCount.text = "$childCount"
- } else {
- holder.typeImage.setImageResource(R.drawable.ic_action_secure_24dp)
- val parentPath = pass.fullPathToParent.replace("(^/)|(/$)".toRegex(), "")
- val source = "$parentPath\n$pass"
- val spannable = SpannableString(source)
- spannable.setSpan(RelativeSizeSpan(0.7f), 0, parentPath.length, 0)
- holder.name.text = spannable
- holder.childCount.visibility = View.GONE
- holder.folderIndicator.visibility = View.GONE
- }
- holder.view.setOnClickListener(getOnClickListener(holder, pass))
- holder.view.setOnLongClickListener(getOnLongClickListener(holder, pass))
-
- // after removal, everything is rebound for some reason; views are shuffled?
- val selected = selectedItems.contains(position)
- holder.view.isSelected = selected
- (holder.itemView as MultiselectableConstraintLayout).setMultiSelected(selected)
- }
-
- protected abstract fun getOnClickListener(holder: ViewHolder, pass: PasswordItem): View.OnClickListener
-
- // Create new views (invoked by the layout manager)
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int
- ): ViewHolder {
- // create a new view
- val v = LayoutInflater.from(parent.context)
- .inflate(R.layout.password_row_layout, parent, false)
- return ViewHolder(v)
- }
-
- /*
- Provide a reference to the views for each data item
- Complex data items may need more than one view per item, and
- you provide access to all the views for a data item in a view holder
- each data item is just a string in this case
- */
- class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
- val name: AppCompatTextView = view.findViewById(R.id.label)
- val typeImage: AppCompatImageView = view.findViewById(R.id.type_image)
- val childCount: AppCompatTextView = view.findViewById(R.id.child_count)
- val folderIndicator: AppCompatImageView = view.findViewById(R.id.folder_indicator)
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/adapters/FolderRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/ui/adapters/FolderRecyclerAdapter.kt
deleted file mode 100644
index 18aef509..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/ui/adapters/FolderRecyclerAdapter.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package com.zeapo.pwdstore.ui.adapters
-
-import android.view.View
-import com.zeapo.pwdstore.SelectFolderFragment
-import com.zeapo.pwdstore.utils.PasswordItem
-import java.util.ArrayList
-
-class FolderRecyclerAdapter(
- private val listener: SelectFolderFragment.OnFragmentInteractionListener,
- values: ArrayList<PasswordItem>
-) : EntryRecyclerAdapter(values) {
-
- override fun getOnClickListener(holder: ViewHolder, pass: PasswordItem): View.OnClickListener {
- return View.OnClickListener {
- listener.onFragmentInteraction(pass)
- notifyItemChanged(holder.adapterPosition)
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt
new file mode 100644
index 00000000..9a914be9
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.ui.adapters
+
+import android.text.SpannableString
+import android.text.style.RelativeSizeSpan
+import android.view.MotionEvent
+import android.view.View
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.preference.PreferenceManager
+import androidx.recyclerview.selection.ItemDetailsLookup
+import androidx.recyclerview.selection.Selection
+import androidx.recyclerview.widget.RecyclerView
+import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.SearchableRepositoryAdapter
+import com.zeapo.pwdstore.stableId
+import com.zeapo.pwdstore.utils.PasswordItem
+import java.io.File
+
+open class PasswordItemRecyclerAdapter :
+ SearchableRepositoryAdapter<PasswordItemRecyclerAdapter.PasswordItemViewHolder>(
+ R.layout.password_row_layout,
+ ::PasswordItemViewHolder,
+ PasswordItemViewHolder::bind
+ ) {
+
+ 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 onSelectionChanged(listener: (selection: Selection<String>) -> Unit): PasswordItemRecyclerAdapter {
+ return super.onSelectionChanged(listener) as PasswordItemRecyclerAdapter
+ }
+
+ class PasswordItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ private val name: AppCompatTextView = itemView.findViewById(R.id.label)
+ private val typeImage: AppCompatImageView = itemView.findViewById(R.id.type_image)
+ 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<String>
+
+ fun bind(item: PasswordItem) {
+ val settings =
+ PreferenceManager.getDefaultSharedPreferences(itemView.context.applicationContext)
+ val showHidden = settings.getBoolean("show_hidden_folders", false)
+ name.text = item.toString()
+ if (item.type == PasswordItem.TYPE_CATEGORY) {
+ typeImage.setImageResource(R.drawable.ic_multiple_files_24dp)
+ folderIndicator.visibility = View.VISIBLE
+ val children = item.file.listFiles { pathname ->
+ !(!showHidden && (pathname.isDirectory && pathname.isHidden))
+ } ?: emptyArray<File>()
+ val count = children.size
+ childCount.visibility = if (count > 0) View.VISIBLE else View.GONE
+ childCount.text = "$count"
+ } else {
+ typeImage.setImageResource(R.drawable.ic_action_secure_24dp)
+ val parentPath = item.fullPathToParent.replace("(^/)|(/$)".toRegex(), "")
+ val source = "$parentPath\n$item"
+ val spannable = SpannableString(source)
+ spannable.setSpan(RelativeSizeSpan(0.7f), 0, parentPath.length, 0)
+ name.text = spannable
+ childCount.visibility = View.GONE
+ folderIndicator.visibility = View.GONE
+ }
+ itemDetails = object : ItemDetailsLookup.ItemDetails<String>() {
+ override fun getPosition() = absoluteAdapterPosition
+ override fun getSelectionKey() = item.stableId
+ }
+ }
+ }
+
+ class PasswordItemDetailsLookup(private val recyclerView: RecyclerView) :
+ ItemDetailsLookup<String>() {
+ override fun getItemDetails(event: MotionEvent): ItemDetails<String>? {
+ val view = recyclerView.findChildViewUnder(event.x, event.y) ?: return null
+ return (recyclerView.getChildViewHolder(view) as PasswordItemViewHolder).itemDetails
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordRecyclerAdapter.kt
deleted file mode 100644
index 39d73563..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordRecyclerAdapter.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package com.zeapo.pwdstore.ui.adapters
-
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import androidx.appcompat.view.ActionMode
-import com.zeapo.pwdstore.PasswordFragment
-import com.zeapo.pwdstore.PasswordStore
-import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.utils.PasswordItem
-import java.util.ArrayList
-import java.util.TreeSet
-
-class PasswordRecyclerAdapter(
- private val activity: PasswordStore,
- private val listener: PasswordFragment.OnFragmentInteractionListener,
- values: ArrayList<PasswordItem>
-) : EntryRecyclerAdapter(values) {
- var actionMode: ActionMode? = null
- private var canEdit: Boolean = false
- 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
- activity.findViewById<View>(R.id.fab).visibility = View.GONE
- 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 = canEdit
- return true // Return false if nothing is done
- }
-
- // Called when the user selects a contextual menu item
- override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.menu_delete_password -> {
- activity.deletePasswords(this@PasswordRecyclerAdapter, TreeSet(selectedItems))
- mode.finish() // Action picked, so close the CAB
- return true
- }
- R.id.menu_edit_password -> {
- activity.editPassword(values[selectedItems.iterator().next()])
- mode.finish()
- return true
- }
- R.id.menu_move_password -> {
- val selectedPasswords = ArrayList<PasswordItem>()
- for (id in selectedItems) {
- selectedPasswords.add(values[id])
- }
- activity.movePasswords(selectedPasswords)
- return false
- }
- else -> return false
- }
- }
-
- // Called when the user exits the action mode
- override fun onDestroyActionMode(mode: ActionMode) {
- val it = selectedItems.iterator()
- while (it.hasNext()) {
- // need the setSelected line in onBind
- notifyItemChanged(it.next())
- it.remove()
- }
- actionMode = null
- // show the fab
- activity.findViewById<View>(R.id.fab).visibility = View.VISIBLE
- }
- }
-
- override fun getOnLongClickListener(holder: ViewHolder, pass: PasswordItem): View.OnLongClickListener {
- return View.OnLongClickListener {
- if (actionMode != null) {
- return@OnLongClickListener false
- }
- toggleSelection(holder.adapterPosition)
- canEdit = pass.type == PasswordItem.TYPE_PASSWORD
- // Start the CAB using the ActionMode.Callback
- actionMode = activity.startSupportActionMode(actionModeCallback)
- actionMode?.title = "" + selectedItems.size
- actionMode?.invalidate()
- notifyItemChanged(holder.adapterPosition)
- true
- }
- }
-
- override fun getOnClickListener(holder: ViewHolder, pass: PasswordItem): View.OnClickListener {
- return View.OnClickListener {
- if (actionMode != null) {
- toggleSelection(holder.adapterPosition)
- actionMode?.title = "" + selectedItems.size
- if (selectedItems.isEmpty()) {
- actionMode?.finish()
- } else if (selectedItems.size == 1 && (canEdit.not())) {
- if (values[selectedItems.iterator().next()].type == PasswordItem.TYPE_PASSWORD) {
- canEdit = true
- actionMode?.invalidate()
- }
- } else if (selectedItems.size >= 1 && canEdit) {
- canEdit = false
- actionMode?.invalidate()
- }
- } else {
- listener.onFragmentInteraction(pass)
- }
- notifyItemChanged(holder.adapterPosition)
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt
index bff229b9..1426ef1e 100644
--- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt
@@ -48,7 +48,7 @@ class FolderCreationDialogFragment : DialogFragment() {
val materialTextView = dialog.findViewById<TextInputEditText>(R.id.folder_name_text)
val folderName = materialTextView.text.toString()
File("$currentDir/$folderName").mkdir()
- (requireActivity() as PasswordStore).updateListAdapter()
+ (requireActivity() as PasswordStore).refreshPasswordList()
dismiss()
}