diff options
author | Daniƫl van den Berg <danielvandenberg95@gmail.com> | 2016-09-09 11:47:49 +0200 |
---|---|---|
committer | Mohamed Zenadi <zeapo@users.noreply.github.com> | 2016-09-09 11:47:49 +0200 |
commit | a6da17417eed1412dfbd9c06d03d85f100d35608 (patch) | |
tree | dfa093c2c2b0df651be55420bf6b0a903b605ece | |
parent | 86083f03f5a60074cddb7496bff157b31ce8c2a3 (diff) |
Added the ability to move passwords around. (#210)
* Added the ability to move passwords around.
* Generified the PasswordRecyclerAdapter and the FolderRecyclerAdapter into EntryRecyclerAdapter
14 files changed, 604 insertions, 162 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java index e33addc3..996942d7 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java @@ -2,8 +2,6 @@ package com.zeapo.pwdstore; import android.Manifest; import android.app.Activity; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; @@ -13,11 +11,14 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; import android.support.v4.content.ContextCompat; import android.support.v4.view.MenuItemCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.SearchView; +import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -38,6 +39,7 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.Repository; import java.io.File; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.Set; @@ -463,6 +465,17 @@ public class PasswordStore extends AppCompatActivity { .show(); } + public void movePasswords(ArrayList<PasswordItem> values) { + Intent intent = new Intent(this, PgpHandler.class); + ArrayList<String> fileLocations = new ArrayList<>(); + for (PasswordItem passwordItem : values){ + fileLocations.add(passwordItem.getFile().getAbsolutePath()); + } + intent.putExtra("Files",fileLocations); + intent.putExtra("Operation", "SELECTFOLDER"); + startActivityForResult(intent, PgpHandler.REQUEST_CODE_SELECT_FOLDER); + } + /** * clears adapter's content and updates it with a fresh list of passwords from the root */ @@ -558,6 +571,36 @@ public class PasswordStore extends AppCompatActivity { intent.putExtra("Operation", GitActivity.REQUEST_CLONE); startActivityForResult(intent, GitActivity.REQUEST_CLONE); break; + case PgpHandler.REQUEST_CODE_SELECT_FOLDER: + Log.d("Moving","Moving passwords to "+data.getStringExtra("SELECTED_FOLDER_PATH")); + Log.d("Moving", TextUtils.join(", ", data.getStringArrayListExtra("Files"))); + File target = new File(data.getStringExtra("SELECTED_FOLDER_PATH")); + if (!target.isDirectory()){ + Log.e("Moving","Tried moving passwords to a non-existing folder."); + break; + } + + Repository repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory(activity)); + Git git = new Git(repo); + GitAsyncTask tasks = new GitAsyncTask(activity, false, true, CommitCommand.class); + + for (String string : data.getStringArrayListExtra("Files")){ + File source = new File(string); + if (!source.exists()){ + Log.e("Moving","Tried moving something that appears non-existent."); + continue; + } + if (!source.renameTo(new File(target.getAbsolutePath()+"/"+source.getName()))){ + Log.e("Moving","Something went wrong while moving."); + }else{ + tasks.execute( + git.add().addFilepattern(source.getAbsolutePath().replace(PasswordRepository.getWorkTree() + "/", "")), + git.commit().setMessage("[ANDROID PwdStore] Moved "+string.replace(PasswordRepository.getWorkTree() + "/", "")+" to "+target.getAbsolutePath().replace(PasswordRepository.getWorkTree() + "/","")+target.getAbsolutePath()+"/"+source.getName()+".") + ); + } + } + updateListAdapter(); + break; } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/SelectFolderFragment.java b/app/src/main/java/com/zeapo/pwdstore/SelectFolderFragment.java new file mode 100644 index 00000000..c9bc3596 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/SelectFolderFragment.java @@ -0,0 +1,227 @@ +package com.zeapo.pwdstore; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.design.widget.FloatingActionButton; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.zeapo.pwdstore.crypto.PgpHandler; +import com.zeapo.pwdstore.utils.FolderRecyclerAdapter; +import com.zeapo.pwdstore.utils.PasswordItem; +import com.zeapo.pwdstore.utils.PasswordRepository; + +import java.io.File; +import java.util.ArrayList; +import java.util.Stack; + +/** + * A fragment representing a list of Items. + * <p /> + * Large screen devices (such as tablets) are supported by replacing the ListView + * with a GridView. + * <p /> + */ +public class SelectFolderFragment extends Fragment{ + + public interface OnFragmentInteractionListener { + public void onFragmentInteraction(PasswordItem item); + } + + // store the pass files list in a stack + private Stack<ArrayList<PasswordItem>> passListStack; + private Stack<File> pathStack; + private Stack<Integer> scrollPosition; + private FolderRecyclerAdapter recyclerAdapter; + private RecyclerView recyclerView; + private RecyclerView.LayoutManager mLayoutManager; + private OnFragmentInteractionListener mListener; + private SharedPreferences settings; + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public SelectFolderFragment() { } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String path = getArguments().getString("Path"); + + settings = PreferenceManager.getDefaultSharedPreferences(getActivity()); + passListStack = new Stack<ArrayList<PasswordItem>>(); + scrollPosition = new Stack<Integer>(); + pathStack = new Stack<File>(); + recyclerAdapter = new FolderRecyclerAdapter((PgpHandler) getActivity(), mListener, + PasswordRepository.getPasswords(new File(path), PasswordRepository.getRepositoryDirectory(getActivity()))); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.password_recycler_view, container, false); + + // use a linear layout manager + mLayoutManager = new LinearLayoutManager(getActivity()); + + recyclerView = (RecyclerView) view.findViewById(R.id.pass_recycler); + recyclerView.setLayoutManager(mLayoutManager); + + // use divider + recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), R.drawable.divider)); + + // Set the adapter + recyclerView.setAdapter(recyclerAdapter); + + final FloatingActionButton fab = (FloatingActionButton) view.findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ((PasswordStore) getActivity()).createPassword(); + } + }); + + registerForContextMenu(recyclerView); + return view; + } + + @Override + public void onAttach(final Context context) { + super.onAttach(context); + try { + mListener = new OnFragmentInteractionListener() { + public void onFragmentInteraction(PasswordItem item) { + if (item.getType() == PasswordItem.TYPE_CATEGORY) { + // push the current password list (non filtered plz!) + passListStack.push(pathStack.isEmpty() ? + PasswordRepository.getPasswords(PasswordRepository.getRepositoryDirectory(context)) : + PasswordRepository.getPasswords(pathStack.peek(), PasswordRepository.getRepositoryDirectory(context))); + //push the category were we're going + pathStack.push(item.getFile()); + scrollPosition.push(recyclerView.getVerticalScrollbarPosition()); + + recyclerView.scrollToPosition(0); + recyclerAdapter.clear(); + recyclerAdapter.addAll(PasswordRepository.getPasswords(item.getFile(), PasswordRepository.getRepositoryDirectory(context))); + + ((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + } + + public void savePosition(Integer position) { + + } + }; + } catch (ClassCastException e) { + throw new ClassCastException(context.toString() + + " must implement OnFragmentInteractionListener"); + } + } + + @Override + public void onPause() { + super.onPause(); +// mListener.savePosition(mListView.getFirstVisiblePosition()); +// mListView.closeOpenedItems(); + } + + /** + * clears the adapter content and sets it back to the root view + */ + public void updateAdapter() { + passListStack.clear(); + pathStack.clear(); + scrollPosition.clear(); + recyclerAdapter.clear(); + recyclerAdapter.addAll(PasswordRepository.getPasswords(PasswordRepository.getRepositoryDirectory(getActivity()))); + + ((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); + } + + /** + * refreshes the adapter with the latest opened category + */ + public void refreshAdapter() { + recyclerAdapter.clear(); + recyclerAdapter.addAll(pathStack.isEmpty() ? + PasswordRepository.getPasswords(PasswordRepository.getRepositoryDirectory(getActivity())) : + PasswordRepository.getPasswords(pathStack.peek(), PasswordRepository.getRepositoryDirectory(getActivity()))); + } + + /** + * filters the list adapter + * @param filter the filter to apply + */ + public void filterAdapter(String filter) { + Log.d("FRAG", "filter: " + filter); + + if (filter.isEmpty()) { + refreshAdapter(); + } else { + recursiveFilter(filter, pathStack.isEmpty() ? null : pathStack.peek()); + } + } + + /** + * recursively filters a directory and extract all the matching items + * @param filter the filter to apply + * @param dir the directory to filter + */ + private void recursiveFilter(String filter, File dir) { + // on the root the pathStack is empty + ArrayList<PasswordItem> passwordItems = dir == null ? + PasswordRepository.getPasswords(PasswordRepository.getRepositoryDirectory(getActivity())) : + PasswordRepository.getPasswords(dir, PasswordRepository.getRepositoryDirectory(getActivity())); + + boolean rec = settings.getBoolean("filter_recursively", true); + for (PasswordItem item : passwordItems) { + if (item.getType() == PasswordItem.TYPE_CATEGORY && rec) { + recursiveFilter(filter, item.getFile()); + } + boolean matches = item.toString().toLowerCase().contains(filter.toLowerCase()); + boolean inAdapter = recyclerAdapter.getValues().contains(item); + if (matches && !inAdapter) { + recyclerAdapter.add(item); + } else if (!matches && inAdapter) { + recyclerAdapter.remove(recyclerAdapter.getValues().indexOf(item)); + } + } + } + + /** + * Goes back one level back in the path + */ + public void popBack() { + if (passListStack.isEmpty()) + return; + + recyclerView.scrollToPosition(scrollPosition.pop()); + recyclerAdapter.clear(); + recyclerAdapter.addAll(passListStack.pop()); + pathStack.pop(); + } + + /** + * gets the current directory + * @return the current directory + */ + public File getCurrentDir() { + if (pathStack.isEmpty()) + return PasswordRepository.getWorkTree(); + else + return pathStack.peek(); + } + + public boolean isNotEmpty() { + return !passListStack.isEmpty(); + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java index 60599d6d..c28e6f82 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpHandler.java @@ -13,6 +13,8 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.preference.PreferenceManager; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.util.Log; @@ -27,7 +29,9 @@ import android.widget.TextView; import android.widget.Toast; import com.google.common.primitives.Longs; +import com.zeapo.pwdstore.BuildConfig; import com.zeapo.pwdstore.R; +import com.zeapo.pwdstore.SelectFolderFragment; import com.zeapo.pwdstore.UserPreference; import com.zeapo.pwdstore.pwgenDialogFragment; import com.zeapo.pwdstore.utils.PasswordRepository; @@ -57,6 +61,9 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne private Activity activity; ClipboardManager clipboard; + SelectFolderFragment passwordList; + private Intent selectFolderData; + private boolean registered; public static final int REQUEST_CODE_SIGN = 9910; @@ -66,6 +73,7 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne public static final int REQUEST_CODE_GET_KEY = 9914; public static final int REQUEST_CODE_GET_KEY_IDS = 9915; public static final int REQUEST_CODE_EDIT = 9916; + public static final int REQUEST_CODE_SELECT_FOLDER = 9917; public final class Constants { public static final String TAG = "Keychain"; @@ -125,10 +133,15 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. - if (getIntent().getStringExtra("Operation").equals("ENCRYPT")) { - getMenuInflater().inflate(R.menu.pgp_handler_new_password, menu); - } else { - getMenuInflater().inflate(R.menu.pgp_handler, menu); + switch (getIntent().getStringExtra("Operation")){ + case "ENCRYPT": + getMenuInflater().inflate(R.menu.pgp_handler_new_password, menu); + break; + case "SELECTFOLDER": + getMenuInflater().inflate(R.menu.pgp_handler_select_folder, menu); + break; + default: + getMenuInflater().inflate(R.menu.pgp_handler, menu); } return true; } @@ -160,10 +173,22 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne setResult(RESULT_CANCELED); finish(); return true; + case R.id.crypto_select: + selectFolder(); + break; } return super.onOptionsItemSelected(item); } + private void selectFolder() { + if (selectFolderData == null || passwordList == null){ + Log.wtf(Constants.TAG,"Folder selected while the app didn't ask for one to be selected?"); + } + selectFolderData.putExtra("SELECTED_FOLDER_PATH",passwordList.getCurrentDir().getAbsolutePath()); + setResult(RESULT_OK,selectFolderData); + finish(); + } + public void editPassword() { // if in encrypt or in decrypt and password is invisible // (because !showPassword, so this will instantly close), do nothing @@ -259,6 +284,7 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne DialogFragment df = new pwgenDialogFragment(); df.show(getFragmentManager(), "generator"); default: + Log.wtf(Constants.TAG,"This should not happen.... PgpHandler.java#handleClick(View) default reached."); // should not happen } @@ -407,6 +433,39 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne } } + private void selectFolder(Intent data) { + + if (data.getStringExtra("Operation") == null || !data.getStringExtra("Operation").equals("SELECTFOLDER")){ + Log.e(Constants.TAG,"PgpHandler#selectFolder(Intent) triggered with incorrect intent."); + if (BuildConfig.DEBUG){ + throw new UnsupportedOperationException("Triggered with incorrect intent."); + } + return; + } + + Log.d(Constants.TAG,"PgpHandler#selectFolder(Intent)."); + + + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + + + passwordList = new SelectFolderFragment(); + Bundle args = new Bundle(); + args.putString("Path", PasswordRepository.getWorkTree().getAbsolutePath()); + + passwordList.setArguments(args); + + getSupportActionBar().show(); + + fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); + + fragmentTransaction.replace(R.id.pgp_handler_linearlayout, passwordList, "PasswordsList"); + fragmentTransaction.commit(); + + this.selectFolderData = data; + } + public class PgpCallback implements OpenPgpApi.IOpenPgpCallback { boolean returnToCiphertextField; ByteArrayOutputStream os; @@ -664,7 +723,11 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne Log.i("PGP", "ISBOUND!!"); Bundle extra = getIntent().getExtras(); - if (extra.getString("Operation").equals("DECRYPT")) { + final String operation = extra.getString("Operation"); + if (operation == null){ + return; + } + if (operation.equals("DECRYPT")) { setContentView(R.layout.decrypt_layout); ((TextView) findViewById(R.id.crypto_password_file)).setText(extra.getString("NAME")); String cat = new File(extra.getString("FILE_PATH").replace(PasswordRepository.getWorkTree().getAbsolutePath(), "")) @@ -672,7 +735,7 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne ((TextView) findViewById(R.id.crypto_password_category)).setText(cat + "/"); decryptAndVerify(new Intent()); - } else if (extra.getString("Operation").equals("ENCRYPT")) { + } else if (operation.equals("ENCRYPT")) { setContentView(R.layout.encrypt_layout); Typeface monoTypeface = Typeface.createFromAsset(getAssets(), "fonts/sourcecodepro.ttf"); ((EditText) findViewById(R.id.crypto_password_edit)).setTypeface(monoTypeface); @@ -681,7 +744,7 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne cat = cat.replace(PasswordRepository.getWorkTree().getAbsolutePath(), ""); cat = cat + "/"; ((TextView) findViewById(R.id.crypto_password_category)).setText(cat); - } else if (extra.getString("Operation").equals("GET_KEY_ID")) { + } else if (operation.equals("GET_KEY_ID")) { getKeyIds(new Intent()); // setContentView(R.layout.key_id); @@ -689,7 +752,7 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne // String keys = keyIDs.split(",").length > 1 ? keyIDs : keyIDs.split(",")[0]; // ((TextView) findViewById(R.id.crypto_key_ids)).setText(keys); // } - } else if (extra.getString("Operation").equals("EDIT")) { + } else if (operation.equals("EDIT")) { setContentView(R.layout.decrypt_layout); ((TextView) findViewById(R.id.crypto_password_file)).setText(extra.getString("NAME")); String cat = new File(extra.getString("FILE_PATH").replace(PasswordRepository.getWorkTree().getAbsolutePath(), "")) @@ -697,6 +760,9 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne ((TextView) findViewById(R.id.crypto_password_category)).setText(cat + "/"); edit(new Intent()); + } else if (operation.equals("SELECTFOLDER")){ + setContentView(R.layout.select_folder_layout); + selectFolder(getIntent()); } } diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java new file mode 100644 index 00000000..78b81928 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java @@ -0,0 +1,162 @@ +package com.zeapo.pwdstore.utils; + +import android.app.Activity; +import android.graphics.Color; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.zeapo.pwdstore.R; + +import java.util.ArrayList; +import java.util.Set; +import java.util.TreeSet; + +public abstract class EntryRecyclerAdapter extends RecyclerView.Adapter<EntryRecyclerAdapter.ViewHolder> { + private final Activity activity; + protected final ArrayList<PasswordItem> values; + protected final Set<Integer> selectedItems = new TreeSet<>(); + + public EntryRecyclerAdapter(Activity activity, ArrayList<PasswordItem> 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<PasswordItem> getValues() { + return this.values; + } + + public void clear() { + this.values.clear(); + this.notifyDataSetChanged(); + } + + public void addAll(ArrayList<PasswordItem> list) { + this.values.addAll(list); + this.notifyDataSetChanged(); + } + + public void add(PasswordItem item) { + this.values.add(item); + this.notifyItemInserted(getItemCount()); + } + + public 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<Integer> selectedItems) { + Set<Integer> 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 + protected View.OnLongClickListener getOnLongClickListener(ViewHolder holder, PasswordItem pass) { + return new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + return false; + } + }; + } + + // Replace the contents of a view (invoked by the layout manager) + @Override + public void onBindViewHolder(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); + holder.name.setText(pass.toString() + "/"); + } else { + holder.typeImage.setImageResource(R.drawable.ic_action_secure); + holder.name.setText(pass.toString()); + } + + holder.type.setText(pass.getFullPathName()); + if (pass.getType() == PasswordItem.TYPE_CATEGORY) { +// holder.card.setCardBackgroundColor(activity.getResources().getColor(R.color.blue_grey_200)); + } else { +// holder.card.setCardBackgroundColor(activity.getResources().getColor(R.color.blue_grey_50)); + } + + 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.setBackgroundResource(Color.alpha(1)); + holder.type.setTextColor(ContextCompat.getColor(activity, R.color.grey_500)); + } + } + + @NonNull + protected abstract View.OnClickListener getOnClickListener(ViewHolder holder, PasswordItem pass); + + // 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 + public static class ViewHolder extends RecyclerView.ViewHolder { + // each data item is just a string in this case + public View view; + public TextView name; + public TextView type; + public ImageView typeImage; + + public ViewHolder(View v) { + super(v); + view = v; + name = (TextView) view.findViewById(R.id.label); + type = (TextView) view.findViewById(R.id.type); + typeImage = (ImageView) view.findViewById(R.id.type_image); + } + } + + // Create new views (invoked by the layout manager) + @Override + public PasswordRecyclerAdapter.ViewHolder onCreateViewHolder(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); + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java new file mode 100644 index 00000000..04b243e1 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java @@ -0,0 +1,31 @@ +package com.zeapo.pwdstore.utils; + +import android.support.annotation.NonNull; +import android.view.View; + +import com.zeapo.pwdstore.SelectFolderFragment; +import com.zeapo.pwdstore.crypto.PgpHandler; + +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(PgpHandler activity, SelectFolderFragment.OnFragmentInteractionListener listener, ArrayList<PasswordItem> values) { + super(activity, values); + this.listener = listener; + } + + @NonNull + protected View.OnClickListener getOnClickListener(final ViewHolder holder, final PasswordItem pass) { + return new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onFragmentInteraction(pass); + notifyItemChanged(holder.getAdapterPosition()); + } + }; + } + +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java index 0835425a..a4446198 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java @@ -1,17 +1,10 @@ package com.zeapo.pwdstore.utils; -import android.graphics.Color; -import android.os.Build; -import android.support.v4.content.ContextCompat; +import android.support.annotation.NonNull; import android.support.v7.view.ActionMode; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; import com.zeapo.pwdstore.PasswordFragment; import com.zeapo.pwdstore.PasswordStore; @@ -19,76 +12,46 @@ import com.zeapo.pwdstore.R; import java.util.ArrayList; import java.util.Iterator; -import java.util.Set; import java.util.TreeSet; -public class PasswordRecyclerAdapter extends RecyclerView.Adapter<PasswordRecyclerAdapter.ViewHolder> { +public class PasswordRecyclerAdapter extends EntryRecyclerAdapter { private final PasswordStore activity; - private final ArrayList<PasswordItem> values; private final PasswordFragment.OnFragmentInteractionListener listener; - private final Set<Integer> selectedItems; private ActionMode mActionMode; private Boolean canEdit; - // 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 - public static class ViewHolder extends RecyclerView.ViewHolder { - // each data item is just a string in this case - public View view; - public TextView name; - public TextView type; - public ImageView typeImage; - - public ViewHolder(View v) { - super(v); - view = v; - name = (TextView) view.findViewById(R.id.label); - type = (TextView) view.findViewById(R.id.type); - typeImage = (ImageView) view.findViewById(R.id.type_image); - } - } - // Provide a suitable constructor (depends on the kind of dataset) public PasswordRecyclerAdapter(PasswordStore activity, PasswordFragment.OnFragmentInteractionListener listener, ArrayList<PasswordItem> values) { - this.values = values; + super(activity, values); this.activity = activity; this.listener = listener; - selectedItems = new TreeSet<>(); } - // Create new views (invoked by the layout manager) @Override - public PasswordRecyclerAdapter.ViewHolder onCreateViewHolder(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); + @NonNull + protected View.OnLongClickListener getOnLongClickListener(final ViewHolder holder, final PasswordItem pass) { + return new View.OnLongClickListener() { + @Override + public boolean onLongClick(View 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; + } + }; } - // Replace the contents of a view (invoked by the layout manager) @Override - public void onBindViewHolder(final ViewHolder holder, int position) { - final PasswordItem pass = values.get(position); - holder.name.setText(pass.toString()); - if (pass.getType() == PasswordItem.TYPE_CATEGORY) { - holder.typeImage.setImageResource(R.drawable.ic_folder_grey600_24dp); - holder.name.setText(pass.toString() + "/"); - } else { - holder.typeImage.setImageResource(R.drawable.ic_action_secure); - holder.name.setText(pass.toString()); - } - int sdk = Build.VERSION.SDK_INT; - - holder.type.setText(pass.getFullPathName()); - if (pass.getType() == PasswordItem.TYPE_CATEGORY) { -// holder.card.setCardBackgroundColor(activity.getResources().getColor(R.color.blue_grey_200)); - } else { -// holder.card.setCardBackgroundColor(activity.getResources().getColor(R.color.blue_grey_50)); - } - - holder.view.setOnClickListener(new View.OnClickListener() { + @NonNull + protected View.OnClickListener getOnClickListener(final ViewHolder holder, final PasswordItem pass) { + return new View.OnClickListener() { @Override public void onClick(View v) { if (mActionMode != null) { @@ -97,7 +60,7 @@ public class PasswordRecyclerAdapter extends RecyclerView.Adapter<PasswordRecycl if (selectedItems.isEmpty()) { mActionMode.finish(); } else if (selectedItems.size() == 1 && !canEdit) { - if (values.get(selectedItems.iterator().next()).getType() == PasswordItem.TYPE_PASSWORD) { + if (getValues().get(selectedItems.iterator().next()).getType() == PasswordItem.TYPE_PASSWORD) { canEdit = true; mActionMode.invalidate(); } @@ -110,35 +73,7 @@ public class PasswordRecyclerAdapter extends RecyclerView.Adapter<PasswordRecycl } notifyItemChanged(holder.getAdapterPosition()); } - }); - - holder.view.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View 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; - } - }); - - // 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.setBackgroundResource(Color.alpha(1)); - holder.type.setTextColor(ContextCompat.getColor(activity, R.color.grey_500)); - } + }; } private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { @@ -172,9 +107,15 @@ public class PasswordRecyclerAdapter extends RecyclerView.Adapter<PasswordRecycl mode.finish(); // Action picked, so close the CAB return true; case R.id.menu_edit_password: - activity.editPassword(values.get(selectedItems.iterator().next())); + activity.editPassword(getValues().get(selectedItems.iterator().next())); mode.finish(); return true; + case R.id.menu_move_password: + ArrayList<PasswordItem> selectedPasswords = new ArrayList<>(); + for (Integer id : selectedItems) { + selectedPasswords.add(getValues().get(id)); + } + activity.movePasswords(selectedPasswords); default: return false; } @@ -183,67 +124,12 @@ public class PasswordRecyclerAdapter extends RecyclerView.Adapter<PasswordRecycl // Called when the user exits the action mode @Override public void onDestroyActionMode(ActionMode mode) { - for (Iterator it = selectedItems.iterator(); it.hasNext();) { + for (Iterator<Integer> it = selectedItems.iterator(); it.hasNext(); ) { // need the setSelected line in onBind - notifyItemChanged((Integer) it.next()); + notifyItemChanged(it.next()); it.remove(); } mActionMode = null; } }; - - // Return the size of your dataset (invoked by the layout manager) - @Override - public int getItemCount() { - return values.size(); - } - - public ArrayList<PasswordItem> getValues() { - return this.values; - } - - public void clear() { - this.values.clear(); - this.notifyDataSetChanged(); - } - - public void addAll(ArrayList<PasswordItem> list) { - this.values.addAll(list); - this.notifyDataSetChanged(); - } - - public void add(PasswordItem item) { - this.values.add(item); - this.notifyItemInserted(values.size()); - } - - 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); - } - - public 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<Integer> selectedItems) { - Set<Integer> temp = new TreeSet<>(); - for (int selected : selectedItems) { - if (selected > position) { - temp.add(selected - 1); - } else { - temp.add(selected); - } - } - selectedItems.clear(); - selectedItems.addAll(temp); - } } diff --git a/app/src/main/res/drawable-hdpi/ic_folder_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_folder_white_24dp.png Binary files differnew file mode 100644 index 00000000..02ea533a --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_folder_white_24dp.png diff --git a/app/src/main/res/drawable-mdpi/ic_folder_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_folder_white_24dp.png Binary files differnew file mode 100644 index 00000000..831d723b --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_folder_white_24dp.png diff --git a/app/src/main/res/drawable-xhdpi/ic_folder_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_folder_white_24dp.png Binary files differnew file mode 100644 index 00000000..71a5a137 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_folder_white_24dp.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_folder_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_folder_white_24dp.png Binary files differnew file mode 100644 index 00000000..b93d5a1e --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_folder_white_24dp.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_folder_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_folder_white_24dp.png Binary files differnew file mode 100644 index 00000000..a1afbe9d --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_folder_white_24dp.png diff --git a/app/src/main/res/layout/select_folder_layout.xml b/app/src/main/res/layout/select_folder_layout.xml new file mode 100644 index 00000000..142093ec --- /dev/null +++ b/app/src/main/res/layout/select_folder_layout.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:id="@+id/pgp_handler_linearlayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"/> +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/menu/context_pass.xml b/app/src/main/res/menu/context_pass.xml index ac0d88ab..bcdb383c 100644 --- a/app/src/main/res/menu/context_pass.xml +++ b/app/src/main/res/menu/context_pass.xml @@ -3,10 +3,15 @@ xmlns:app="http://schemas.android.com/apk/res-auto" tools:context=".pwdstore"> + <item android:id="@+id/menu_move_password" + android:icon="@drawable/ic_folder_white_24dp" + app:showAsAction="ifRoom" + android:title="Move"/> + <item android:id="@+id/menu_edit_password" - android:icon="@drawable/ic_edit_white_24dp" - app:showAsAction="ifRoom" - android:title="Edit"/> + android:icon="@drawable/ic_edit_white_24dp" + app:showAsAction="ifRoom" + android:title="Edit"/> <item android:id="@+id/menu_delete_password" android:icon="@drawable/ic_delete_white_24dp" diff --git a/app/src/main/res/menu/pgp_handler_select_folder.xml b/app/src/main/res/menu/pgp_handler_select_folder.xml new file mode 100644 index 00000000..9bfba01b --- /dev/null +++ b/app/src/main/res/menu/pgp_handler_select_folder.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:pwstore="http://schemas.android.com/apk/res-auto" + tools:context="com.zeapo.pwdstore.crypto.PgpHandler" > + <item android:title="Select" + android:icon="@drawable/ic_done_white_24dp" + pwstore:showAsAction="ifRoom" + android:id="@+id/crypto_select" + /> +</menu>
\ No newline at end of file |