From 073346c157ec34b750d175c8543f346e6eae94de Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Tue, 1 Oct 2019 22:14:28 +0530 Subject: Redesign UI and introduce dark theme (#519) Caveats: - The openpgp preference had to be removed because the open-intents developers are too lazy to update their libraries. Over the coming weeks I will be reimplementing a local solution for this instead. - The autofill dialog is broken but I since it is being worked on in #410 already I'm not going to bother fixing it. --- .../zeapo/pwdstore/utils/EntryRecyclerAdapter.java | 155 ----------- .../zeapo/pwdstore/utils/EntryRecyclerAdapter.kt | 118 +++++++++ .../java/com/zeapo/pwdstore/utils/Extensions.kt | 9 + .../pwdstore/utils/FolderRecyclerAdapter.java | 29 --- .../zeapo/pwdstore/utils/FolderRecyclerAdapter.kt | 20 ++ .../com/zeapo/pwdstore/utils/PasswordItem.java | 110 -------- .../java/com/zeapo/pwdstore/utils/PasswordItem.kt | 81 ++++++ .../pwdstore/utils/PasswordRecyclerAdapter.java | 133 ---------- .../pwdstore/utils/PasswordRecyclerAdapter.kt | 116 +++++++++ .../zeapo/pwdstore/utils/PasswordRepository.java | 284 --------------------- .../com/zeapo/pwdstore/utils/PasswordRepository.kt | 282 ++++++++++++++++++++ 11 files changed, 626 insertions(+), 711 deletions(-) delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.kt delete mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt (limited to 'app/src/main/java/com/zeapo/pwdstore/utils') diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java deleted file mode 100644 index eba6a02e..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.zeapo.pwdstore.utils; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.graphics.Color; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.recyclerview.widget.RecyclerView; - -import com.zeapo.pwdstore.R; - -import java.util.ArrayList; -import java.util.Set; -import java.util.TreeSet; - -public abstract class EntryRecyclerAdapter extends RecyclerView.Adapter { - final Set selectedItems = new TreeSet<>(); - private final Activity activity; - private final ArrayList values; - - EntryRecyclerAdapter(Activity activity, ArrayList values) { - this.activity = activity; - this.values = values; - } - - // Return the size of your dataset (invoked by the layout manager) - @Override - public int getItemCount() { - return values.size(); - } - - public ArrayList getValues() { - return this.values; - } - - public void clear() { - this.values.clear(); - this.notifyDataSetChanged(); - } - - public void addAll(ArrayList list) { - this.values.addAll(list); - this.notifyDataSetChanged(); - } - - public void add(PasswordItem item) { - this.values.add(item); - this.notifyItemInserted(getItemCount()); - } - - void toggleSelection(int position) { - 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 - public void updateSelectedItems(int position, Set selectedItems) { - Set temp = new TreeSet<>(); - for (int selected : selectedItems) { - if (selected > position) { - temp.add(selected - 1); - } else { - temp.add(selected); - } - } - selectedItems.clear(); - selectedItems.addAll(temp); - } - - public void remove(int position) { - this.values.remove(position); - this.notifyItemRemoved(position); - - // keep selectedItems updated so we know what to notifyItemChanged - // (instead of just using notifyDataSetChanged) - updateSelectedItems(position, selectedItems); - } - - @NonNull - View.OnLongClickListener getOnLongClickListener(ViewHolder holder, PasswordItem pass) { - return v -> false; - } - - // Replace the contents of a view (invoked by the layout manager) - @SuppressLint("SetTextI18n") - @Override - public void onBindViewHolder(@NonNull final ViewHolder holder, int position) { - final PasswordItem pass = getValues().get(position); - holder.name.setText(pass.toString()); - if (pass.getType() == PasswordItem.TYPE_CATEGORY) { - holder.typeImage.setImageResource(R.drawable.ic_folder_grey600_24dp); - } else { - holder.typeImage.setImageResource(R.drawable.ic_action_secure); - holder.name.setText(pass.toString()); - } - - holder.type.setText(pass.getFullPathToParent().replaceAll("(^/)|(/$)", "")); - - holder.view.setOnClickListener(getOnClickListener(holder, pass)); - - holder.view.setOnLongClickListener(getOnLongClickListener(holder, pass)); - - // after removal, everything is rebound for some reason; views are shuffled? - boolean selected = selectedItems.contains(position); - holder.view.setSelected(selected); - if (selected) { - holder.itemView.setBackgroundResource(R.color.deep_orange_200); - holder.type.setTextColor(Color.BLACK); - } else { - holder.itemView.setBackgroundColor(Color.alpha(1)); - holder.type.setTextColor(ContextCompat.getColor(activity, R.color.grey_500)); - } - } - - @NonNull - protected abstract View.OnClickListener getOnClickListener(ViewHolder holder, PasswordItem pass); - - // Create new views (invoked by the layout manager) - @Override - @NonNull - public PasswordRecyclerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, - int viewType) { - // create a new view - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.password_row_layout, parent, false); - return new 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 - static class ViewHolder extends RecyclerView.ViewHolder { - // each data item is just a string in this case - public final View view; - public final TextView name; - final TextView type; - final ImageView typeImage; - - ViewHolder(View v) { - super(v); - view = v; - name = view.findViewById(R.id.label); - type = view.findViewById(R.id.type); - typeImage = view.findViewById(R.id.type_image); - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.kt new file mode 100644 index 00000000..f52756f4 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.kt @@ -0,0 +1,118 @@ +package com.zeapo.pwdstore.utils + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.AppCompatTextView +import androidx.recyclerview.widget.RecyclerView + +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.widget.MultiselectableLinearLayout + +import java.util.ArrayList +import java.util.TreeSet + +abstract class EntryRecyclerAdapter internal constructor(val values: ArrayList) : RecyclerView.Adapter() { + internal val selectedItems: MutableSet = TreeSet() + + // Return the size of your dataset (invoked by the layout manager) + override fun getItemCount(): Int { + return values.size + } + + fun clear() { + this.values.clear() + this.notifyDataSetChanged() + } + + fun addAll(list: ArrayList) { + 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) { + val temp = TreeSet() + 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) + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val pass = values[position] + holder.name.text = pass.toString() + if (pass.type == PasswordItem.TYPE_CATEGORY) { + holder.typeImage.setImageResource(R.drawable.ic_folder_tinted_24dp) + } else { + holder.typeImage.setImageResource(R.drawable.ic_action_secure) + holder.name.text = pass.toString() + } + + holder.type.text = pass.fullPathToParent.replace("(^/)|(/$)".toRegex(), "") + + 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 MultiselectableLinearLayout).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 + class ViewHolder(// each data item is just a string in this case + val view: View) : RecyclerView.ViewHolder(view) { + val name: AppCompatTextView = view.findViewById(R.id.label) + val type: AppCompatTextView = view.findViewById(R.id.type) + val typeImage: AppCompatImageView = view.findViewById(R.id.type_image) + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index a2be3ddc..7c2a28c3 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -1,5 +1,14 @@ package com.zeapo.pwdstore.utils +import android.content.Context +import android.util.TypedValue + fun String.splitLines(): Array { return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() } + +fun Context.resolveAttribute(attr: Int): Int { + val typedValue = TypedValue() + this.theme.resolveAttribute(attr, typedValue, true) + return typedValue.data +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java deleted file mode 100644 index 1754e0f9..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.zeapo.pwdstore.utils; - -import android.view.View; - -import androidx.annotation.NonNull; - -import com.zeapo.pwdstore.SelectFolderActivity; -import com.zeapo.pwdstore.SelectFolderFragment; - -import java.util.ArrayList; - -public class FolderRecyclerAdapter extends EntryRecyclerAdapter { - private final SelectFolderFragment.OnFragmentInteractionListener listener; - - // Provide a suitable constructor (depends on the kind of dataset) - public FolderRecyclerAdapter(SelectFolderActivity activity, SelectFolderFragment.OnFragmentInteractionListener listener, ArrayList values) { - super(activity, values); - this.listener = listener; - } - - @NonNull - protected View.OnClickListener getOnClickListener(final ViewHolder holder, final PasswordItem pass) { - return v -> { - listener.onFragmentInteraction(pass); - notifyItemChanged(holder.getAdapterPosition()); - }; - } - -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.kt new file mode 100644 index 00000000..4f0ca4c8 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.kt @@ -0,0 +1,20 @@ +package com.zeapo.pwdstore.utils + +import android.view.View + +import com.zeapo.pwdstore.SelectFolderFragment + +import java.util.ArrayList + +class FolderRecyclerAdapter(private val listener: SelectFolderFragment.OnFragmentInteractionListener, + values: ArrayList +) : 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/utils/PasswordItem.java b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.java deleted file mode 100644 index 1a498721..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.zeapo.pwdstore.utils; - -import androidx.annotation.NonNull; - -import com.zeapo.pwdstore.crypto.PgpActivity; - -import java.io.File; - -public class PasswordItem implements Comparable { - - public final static char TYPE_CATEGORY = 'c'; - public final static char TYPE_PASSWORD = 'p'; - - private final char type; - private final String name; - private final PasswordItem parent; - private final File file; - private final String fullPathToParent; - private final String longName; - - /** - * Create a password item - *

- * Make it protected so that we use a builder - */ - private PasswordItem(String name, PasswordItem parent, char type, File file, File rootDir) { - this.name = name; - this.parent = parent; - this.type = type; - this.file = file; - fullPathToParent = file.getAbsolutePath() - .replace(rootDir.getAbsolutePath(), "") - .replace(file.getName(), ""); - longName = PgpActivity.getLongName(fullPathToParent, rootDir.getAbsolutePath(), toString()); - } - - /** - * Create a new Category item - */ - public static PasswordItem newCategory(String name, File file, PasswordItem parent, File rootDir) { - return new PasswordItem(name, parent, TYPE_CATEGORY, file, rootDir); - } - - /** - * Create a new parentless category item - */ - public static PasswordItem newCategory(String name, File file, File rootDir) { - return new PasswordItem(name, null, TYPE_CATEGORY, file, rootDir); - } - - /** - * Create a new password item - */ - public static PasswordItem newPassword(String name, File file, PasswordItem parent, File rootDir) { - return new PasswordItem(name, parent, TYPE_PASSWORD, file, rootDir); - } - - /** - * Create a new parentless password item - */ - public static PasswordItem newPassword(String name, File file, File rootDir) { - return new PasswordItem(name, null, TYPE_PASSWORD, file, rootDir); - } - - public char getType() { - return this.type; - } - - String getName() { - return this.name; - } - - public PasswordItem getParent() { - return this.parent; - } - - public File getFile() { - return this.file; - } - - public String getFullPathToParent() { - return this.fullPathToParent; - } - - public String getLongName() { - return longName; - } - - @NonNull - @Override - public String toString() { - return this.getName().replace(".gpg", ""); - } - - @Override - public boolean equals(Object o) { - // Makes it possible to have a category and a password with the same name - return o != null - && o.getClass() == PasswordItem.class - && ((PasswordItem) o).getFile().equals(this.getFile()); - } - - @Override - public int compareTo(@NonNull Object o) { - PasswordItem other = (PasswordItem) o; - // Appending the type will make the sort type dependent - return (this.getType() + this.getName()) - .compareToIgnoreCase(other.getType() + other.getName()); - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt new file mode 100644 index 00000000..ddecd2da --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt @@ -0,0 +1,81 @@ +package com.zeapo.pwdstore.utils + +import com.zeapo.pwdstore.crypto.PgpActivity +import java.io.File + +data class PasswordItem( + val name: String, + val parent: PasswordItem? = null, + val type: Char, + val file: File, + val rootDir: File +) : Comparable { + val fullPathToParent = file.absolutePath + .replace(rootDir.absolutePath, "") + .replace(file.name, "") + + val longName = PgpActivity.getLongName( + fullPathToParent, + rootDir.absolutePath, + toString()) + + override fun equals(other: Any?): Boolean { + return (other is PasswordItem) && (other.file == file) + } + + override fun compareTo(other: PasswordItem): Int { + return (type + name).compareTo(other.type + other.name, ignoreCase = true) + } + + override fun toString(): String { + return name.replace(".gpg", "") + } + + override fun hashCode(): Int { + return 0 + } + + companion object { + const val TYPE_CATEGORY = 'c' + const val TYPE_PASSWORD = 'p' + + @JvmStatic + fun newCategory( + name: String, + file: File, + parent: PasswordItem, + rootDir: File + ): PasswordItem { + return PasswordItem(name, parent, TYPE_CATEGORY, file, rootDir) + } + + @JvmStatic + fun newCategory( + name: String, + file: File, + rootDir: File + ): PasswordItem { + return PasswordItem(name, null, TYPE_CATEGORY, file, rootDir) + } + + @JvmStatic + fun newPassword( + name: String, + file: File, + parent: PasswordItem, + rootDir: File + ): PasswordItem { + return PasswordItem(name, parent, TYPE_PASSWORD, file, rootDir) + } + + @JvmStatic + fun newPassword( + name: String, + file: File, + rootDir: File + ): PasswordItem { + return PasswordItem(name, null, TYPE_PASSWORD, file, rootDir) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java deleted file mode 100644 index 9170e408..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.zeapo.pwdstore.utils; - -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.appcompat.view.ActionMode; - -import com.zeapo.pwdstore.PasswordFragment; -import com.zeapo.pwdstore.PasswordStore; -import com.zeapo.pwdstore.R; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.TreeSet; - -public class PasswordRecyclerAdapter extends EntryRecyclerAdapter { - private final PasswordStore activity; - private final PasswordFragment.OnFragmentInteractionListener listener; - public ActionMode mActionMode; - private Boolean canEdit; - private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { - - // Called when the action mode is created; startActionMode() was called - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - // Inflate a menu resource providing context menu items - mode.getMenuInflater().inflate(R.menu.context_pass, menu); - // hide the fab - activity.findViewById(R.id.fab).setVisibility(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 - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - if (canEdit) { - menu.findItem(R.id.menu_edit_password).setVisible(true); - } else { - menu.findItem(R.id.menu_edit_password).setVisible(false); - } - return true; // Return false if nothing is done - } - - // Called when the user selects a contextual menu item - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_delete_password: - activity.deletePasswords(PasswordRecyclerAdapter.this, new TreeSet<>(selectedItems)); - mode.finish(); // Action picked, so close the CAB - return true; - case R.id.menu_edit_password: - activity.editPassword(getValues().get(selectedItems.iterator().next())); - mode.finish(); - return true; - case R.id.menu_move_password: - ArrayList selectedPasswords = new ArrayList<>(); - for (Integer id : selectedItems) { - selectedPasswords.add(getValues().get(id)); - } - activity.movePasswords(selectedPasswords); - default: - return false; - } - } - - // Called when the user exits the action mode - @Override - public void onDestroyActionMode(ActionMode mode) { - for (Iterator it = selectedItems.iterator(); it.hasNext(); ) { - // need the setSelected line in onBind - notifyItemChanged(it.next()); - it.remove(); - } - mActionMode = null; - // show the fab - activity.findViewById(R.id.fab).setVisibility(View.VISIBLE); - } - }; - - // Provide a suitable constructor (depends on the kind of dataset) - public PasswordRecyclerAdapter(PasswordStore activity, PasswordFragment.OnFragmentInteractionListener listener, ArrayList values) { - super(activity, values); - this.activity = activity; - this.listener = listener; - } - - @Override - @NonNull - protected View.OnLongClickListener getOnLongClickListener(final ViewHolder holder, final PasswordItem pass) { - return v -> { - if (mActionMode != null) { - return false; - } - toggleSelection(holder.getAdapterPosition()); - canEdit = pass.getType() == PasswordItem.TYPE_PASSWORD; - // Start the CAB using the ActionMode.Callback - mActionMode = activity.startSupportActionMode(mActionModeCallback); - mActionMode.setTitle("" + selectedItems.size()); - mActionMode.invalidate(); - notifyItemChanged(holder.getAdapterPosition()); - return true; - }; - } - - @Override - @NonNull - protected View.OnClickListener getOnClickListener(final ViewHolder holder, final PasswordItem pass) { - return v -> { - if (mActionMode != null) { - toggleSelection(holder.getAdapterPosition()); - mActionMode.setTitle("" + selectedItems.size()); - if (selectedItems.isEmpty()) { - mActionMode.finish(); - } else if (selectedItems.size() == 1 && !canEdit) { - if (getValues().get(selectedItems.iterator().next()).getType() == PasswordItem.TYPE_PASSWORD) { - canEdit = true; - mActionMode.invalidate(); - } - } else if (selectedItems.size() >= 1 && canEdit) { - canEdit = false; - mActionMode.invalidate(); - } - } else { - listener.onFragmentInteraction(pass); - } - notifyItemChanged(holder.getAdapterPosition()); - }; - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.kt new file mode 100644 index 00000000..d7493a48 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.kt @@ -0,0 +1,116 @@ +package com.zeapo.pwdstore.utils + +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 java.util.ArrayList +import java.util.TreeSet + +class PasswordRecyclerAdapter(private val activity: PasswordStore, + private val listener: PasswordFragment.OnFragmentInteractionListener, + values: ArrayList +) : 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(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() + 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(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/utils/PasswordRepository.java b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.java deleted file mode 100644 index 9c27a448..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.java +++ /dev/null @@ -1,284 +0,0 @@ -package com.zeapo.pwdstore.utils; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.Log; - -import org.apache.commons.io.filefilter.FileFilterUtils; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.lib.StoredConfig; -import org.eclipse.jgit.storage.file.FileRepositoryBuilder; -import org.eclipse.jgit.transport.RefSpec; -import org.eclipse.jgit.transport.RemoteConfig; -import org.eclipse.jgit.transport.URIish; - -import java.io.File; -import java.io.FileFilter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.Set; - -import static java.util.Collections.sort; - -public class PasswordRepository { - - private static Repository repository; - - protected PasswordRepository() { - } - - /** - * Returns the git repository - * - * @param localDir needed only on the creation - * @return the git repository - */ - public static Repository getRepository(File localDir) { - if (repository == null && localDir != null) { - FileRepositoryBuilder builder = new FileRepositoryBuilder(); - try { - repository = builder.setGitDir(localDir) - .readEnvironment() - .build(); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - return repository; - } - - public static boolean isInitialized() { - return repository != null; - } - - public static void createRepository(File localDir) throws Exception { - localDir.delete(); - - Git.init().setDirectory(localDir).call(); - getRepository(localDir); - } - - // TODO add multiple remotes support for pull/push - public static void addRemote(String name, String url, Boolean replace) { - StoredConfig storedConfig = repository.getConfig(); - Set remotes = storedConfig.getSubsections("remote"); - - if (!remotes.contains(name)) { - try { - URIish uri = new URIish(url); - RefSpec refSpec = new RefSpec("+refs/head/*:refs/remotes/" + name + "/*"); - - RemoteConfig remoteConfig = new RemoteConfig(storedConfig, name); - remoteConfig.addFetchRefSpec(refSpec); - remoteConfig.addPushRefSpec(refSpec); - remoteConfig.addURI(uri); - remoteConfig.addPushURI(uri); - - remoteConfig.update(storedConfig); - - storedConfig.save(); - } catch (Exception e) { - e.printStackTrace(); - } - } else if (replace) { - try { - URIish uri = new URIish(url); - - RemoteConfig remoteConfig = new RemoteConfig(storedConfig, name); - // remove the first and eventually the only uri - if (remoteConfig.getURIs().size() > 0) { - remoteConfig.removeURI(remoteConfig.getURIs().get(0)); - } - if (remoteConfig.getPushURIs().size() > 0) { - remoteConfig.removePushURI(remoteConfig.getPushURIs().get(0)); - } - - remoteConfig.addURI(uri); - remoteConfig.addPushURI(uri); - - remoteConfig.update(storedConfig); - - storedConfig.save(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - public static void closeRepository() { - if (repository != null) repository.close(); - repository = null; - } - - public static File getRepositoryDirectory(Context context) { - File dir = null; - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - - if (settings.getBoolean("git_external", false)) { - String external_repo = settings.getString("git_external_repo", null); - if (external_repo != null) { - dir = new File(external_repo); - } - } else { - dir = new File(context.getFilesDir() + "/store"); - } - - return dir; - } - - public static Repository initialize(Context context) { - File dir = getRepositoryDirectory(context); - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - - if (dir == null) { - return null; - } - - // uninitialize the repo if the dir does not exist or is absolutely empty - if (!dir.exists() || !dir.isDirectory() || dir.listFiles().length == 0) { - settings.edit().putBoolean("repository_initialized", false).apply(); - } else { - settings.edit().putBoolean("repository_initialized", true).apply(); - } - - // create the repository static variable in PasswordRepository - return PasswordRepository.getRepository(new File(dir.getAbsolutePath() + "/.git")); - } - - /** - * Gets the password items in the root directory - * - * @return a list of passwords in the root direcotyr - */ - public static ArrayList getPasswords(File rootDir, PasswordSortOrder sortOrder) { - return getPasswords(rootDir, rootDir, sortOrder); - } - - /** - * Gets the .gpg files in a directory - * - * @param path the directory path - * @return the list of gpg files in that directory - */ - public static ArrayList getFilesList(File path) { - if (path == null || !path.exists()) return new ArrayList<>(); - - Log.d("REPO", "current path: " + path.getPath()); - List directories = Arrays.asList(path.listFiles((FileFilter) FileFilterUtils.directoryFileFilter())); - List files = Arrays.asList(path.listFiles((FileFilter) FileFilterUtils.suffixFileFilter(".gpg"))); - - ArrayList items = new ArrayList<>(); - items.addAll(directories); - items.addAll(files); - - return items; - } - - /** - * Gets the passwords (PasswordItem) in a directory - * - * @param path the directory path - * @return a list of password items - */ - public static ArrayList getPasswords(File path, File rootDir, PasswordSortOrder sortOrder) { - //We need to recover the passwords then parse the files - ArrayList passList = getFilesList(path); - - if (passList.size() == 0) return new ArrayList<>(); - - ArrayList passwordList = new ArrayList<>(); - - for (File file : passList) { - if (file.isFile()) { - if (!file.isHidden()) { - passwordList.add(PasswordItem.newPassword(file.getName(), file, rootDir)); - } - } else { - if (!file.isHidden()) { - passwordList.add(PasswordItem.newCategory(file.getName(), file, rootDir)); - } - } - } - sort(passwordList, sortOrder.comparator); - return passwordList; - } - - /** - * Sets the git user name - * - * @param username username - */ - public static void setUserName(String username) { - setStringConfig("user", null, "name", username); - } - - /** - * Sets the git user email - * - * @param email email - */ - public static void setUserEmail(String email) { - setStringConfig("user", null, "email", email); - } - - /** - * Sets a git config value - * - * @param section config section name - * @param subsection config subsection name - * @param name config name - * @param value the value to be set - */ - private static void setStringConfig(String section, String subsection, String name, String value) { - if (isInitialized()) { - StoredConfig config = repository.getConfig(); - config.setString(section, subsection, name, value); - try { - config.save(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - public enum PasswordSortOrder { - - FOLDER_FIRST(new Comparator() { - @Override - public int compare(PasswordItem p1, PasswordItem p2) { - return (p1.getType() + p1.getName()) - .compareToIgnoreCase(p2.getType() + p2.getName()); - } - }), - - INDEPENDENT(new Comparator() { - @Override - public int compare(PasswordItem p1, PasswordItem p2) { - return p1.getName().compareToIgnoreCase(p2.getName()); - } - }), - - FILE_FIRST(new Comparator() { - @Override - public int compare(PasswordItem p1, PasswordItem p2) { - return (p2.getType() + p1.getName()) - .compareToIgnoreCase(p1.getType() + p2.getName()); - } - }); - - private Comparator comparator; - - PasswordSortOrder(Comparator comparator) { - this.comparator = comparator; - } - - public static PasswordSortOrder getSortOrder(SharedPreferences settings) { - return valueOf(settings.getString("sort_order", FOLDER_FIRST.name())); - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt new file mode 100644 index 00000000..9e94e400 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt @@ -0,0 +1,282 @@ +package com.zeapo.pwdstore.utils + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import org.apache.commons.io.filefilter.FileFilterUtils +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Repository +import org.eclipse.jgit.storage.file.FileRepositoryBuilder +import org.eclipse.jgit.transport.RefSpec +import org.eclipse.jgit.transport.RemoteConfig +import org.eclipse.jgit.transport.URIish +import java.io.File +import java.io.FileFilter +import java.util.Comparator + +open class PasswordRepository protected constructor() { + + @Suppress("Unused") + enum class PasswordSortOrder(val comparator: Comparator) { + + FOLDER_FIRST(Comparator { p1: PasswordItem, p2: PasswordItem -> + (p1.type + p1.name) + .compareTo(p2.type + p2.name, ignoreCase = true) + }), + + INDEPENDENT(Comparator { p1: PasswordItem, p2: PasswordItem -> + p1.name.compareTo(p2.name, ignoreCase = true) + }), + + FILE_FIRST(Comparator { p1: PasswordItem, p2: PasswordItem -> + (p2.type + p1.name).compareTo(p1.type + p2.name, ignoreCase = true) + }); + + + companion object { + @JvmStatic + fun getSortOrder(settings: SharedPreferences): PasswordSortOrder { + return valueOf(settings.getString("sort_order", null) ?: FOLDER_FIRST.name) + } + } + } + + companion object { + + private var repository: Repository? = null + + /** + * Returns the git repository + * + * @param localDir needed only on the creation + * @return the git repository + */ + @JvmStatic + fun getRepository(localDir: File?): Repository? { + if (repository == null && localDir != null) { + val builder = FileRepositoryBuilder() + try { + repository = builder.setGitDir(localDir) + .readEnvironment() + .build() + } catch (e: Exception) { + e.printStackTrace() + return null + } + + } + return repository + } + + @JvmStatic + val isInitialized: Boolean + get() = repository != null + + @JvmStatic + @Throws(Exception::class) + fun createRepository(localDir: File) { + localDir.delete() + + Git.init().setDirectory(localDir).call() + getRepository(localDir) + } + + // TODO add multiple remotes support for pull/push + @JvmStatic + fun addRemote(name: String, url: String, replace: Boolean?) { + val storedConfig = repository!!.config + val remotes = storedConfig.getSubsections("remote") + + if (!remotes.contains(name)) { + try { + val uri = URIish(url) + val refSpec = RefSpec("+refs/head/*:refs/remotes/$name/*") + + val remoteConfig = RemoteConfig(storedConfig, name) + remoteConfig.addFetchRefSpec(refSpec) + remoteConfig.addPushRefSpec(refSpec) + remoteConfig.addURI(uri) + remoteConfig.addPushURI(uri) + + remoteConfig.update(storedConfig) + + storedConfig.save() + } catch (e: Exception) { + e.printStackTrace() + } + + } else if (replace!!) { + try { + val uri = URIish(url) + + val remoteConfig = RemoteConfig(storedConfig, name) + // remove the first and eventually the only uri + if (remoteConfig.urIs.size > 0) { + remoteConfig.removeURI(remoteConfig.urIs[0]) + } + if (remoteConfig.pushURIs.size > 0) { + remoteConfig.removePushURI(remoteConfig.pushURIs[0]) + } + + remoteConfig.addURI(uri) + remoteConfig.addPushURI(uri) + + remoteConfig.update(storedConfig) + + storedConfig.save() + } catch (e: Exception) { + e.printStackTrace() + } + + } + } + + @JvmStatic + fun closeRepository() { + if (repository != null) repository!!.close() + repository = null + } + + @JvmStatic + fun getRepositoryDirectory(context: Context): File? { + var dir: File? = null + val settings = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) + + if (settings.getBoolean("git_external", false)) { + val externalRepo = settings.getString("git_external_repo", null) + if (externalRepo != null) { + dir = File(externalRepo) + } + } else { + dir = File(context.filesDir.toString() + "/store") + } + + return dir + } + + @JvmStatic + fun initialize(context: Context): Repository? { + val dir = getRepositoryDirectory(context) + val settings = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) + + if (dir == null) { + return null + } + + // uninitialize the repo if the dir does not exist or is absolutely empty + if (!dir.exists() || !dir.isDirectory || dir.listFiles()!!.isEmpty()) { + settings.edit().putBoolean("repository_initialized", false).apply() + } else { + settings.edit().putBoolean("repository_initialized", true).apply() + } + + // create the repository static variable in PasswordRepository + return getRepository(File(dir.absolutePath + "/.git")) + } + + /** + * Gets the password items in the root directory + * + * @return a list of passwords in the root direcotyr + */ + @JvmStatic + fun getPasswords(rootDir: File, sortOrder: PasswordSortOrder): ArrayList { + return getPasswords(rootDir, rootDir, sortOrder) + } + + /** + * Gets the .gpg files in a directory + * + * @param path the directory path + * @return the list of gpg files in that directory + */ + @JvmStatic + fun getFilesList(path: File?): ArrayList { + if (path == null || !path.exists()) return ArrayList() + + val directories = (path.listFiles(FileFilterUtils.directoryFileFilter() as FileFilter) + ?: emptyArray()).toList() + val files = (path.listFiles(FileFilterUtils.suffixFileFilter(".gpg") as FileFilter) + ?: emptyArray()).toList() + + val items = ArrayList() + items.addAll(directories) + items.addAll(files) + + return items + } + + /** + * Gets the passwords (PasswordItem) in a directory + * + * @param path the directory path + * @return a list of password items + */ + @JvmStatic + fun getPasswords(path: File, rootDir: File, sortOrder: PasswordSortOrder): ArrayList { + //We need to recover the passwords then parse the files + val passList = getFilesList(path) + + if (passList.size == 0) return ArrayList() + + val passwordList = ArrayList() + + for (file in passList) { + if (file.isFile) { + if (!file.isHidden) { + passwordList.add(PasswordItem.newPassword(file.name, file, rootDir)) + } + } else { + if (!file.isHidden) { + passwordList.add(PasswordItem.newCategory(file.name, file, rootDir)) + } + } + } + passwordList.sortWith(sortOrder.comparator) + return passwordList + } + + /** + * Sets the git user name + * + * @param username username + */ + @JvmStatic + fun setUserName(username: String) { + setStringConfig("user", null, "name", username) + } + + /** + * Sets the git user email + * + * @param email email + */ + @JvmStatic + fun setUserEmail(email: String) { + setStringConfig("user", null, "email", email) + } + + /** + * Sets a git config value + * + * @param section config section name + * @param subsection config subsection name + * @param name config name + * @param value the value to be set + */ + @JvmStatic + @Suppress("SameParameterValue") + private fun setStringConfig(section: String, subsection: String?, name: String, value: String) { + if (isInitialized) { + val config = repository!!.config + config.setString(section, subsection, name, value) + try { + config.save() + } catch (e: Exception) { + e.printStackTrace() + } + + } + } + } +} -- cgit v1.2.3