Added the ability to move passwords around. (#210)

* Added the ability to move passwords around.
* Generified the PasswordRecyclerAdapter and the FolderRecyclerAdapter into EntryRecyclerAdapter
This commit is contained in:
Daniël van den Berg 2016-09-09 11:47:49 +02:00 committed by Mohamed Zenadi
parent 86083f03f5
commit a6da17417e
14 changed files with 612 additions and 170 deletions

View file

@ -2,8 +2,6 @@ package com.zeapo.pwdstore;
import android.Manifest; import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -13,11 +11,14 @@ import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat; 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.content.ContextCompat;
import android.support.v4.view.MenuItemCompat; import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.SearchView; import android.support.v7.widget.SearchView;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -38,6 +39,7 @@ import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.Set; import java.util.Set;
@ -463,6 +465,17 @@ public class PasswordStore extends AppCompatActivity {
.show(); .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 * 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); intent.putExtra("Operation", GitActivity.REQUEST_CLONE);
startActivityForResult(intent, GitActivity.REQUEST_CLONE); startActivityForResult(intent, GitActivity.REQUEST_CLONE);
break; 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;
} }
} }
} }

View file

@ -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();
}
}

View file

@ -13,6 +13,8 @@ import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock; import android.os.SystemClock;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@ -27,7 +29,9 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.google.common.primitives.Longs; import com.google.common.primitives.Longs;
import com.zeapo.pwdstore.BuildConfig;
import com.zeapo.pwdstore.R; import com.zeapo.pwdstore.R;
import com.zeapo.pwdstore.SelectFolderFragment;
import com.zeapo.pwdstore.UserPreference; import com.zeapo.pwdstore.UserPreference;
import com.zeapo.pwdstore.pwgenDialogFragment; import com.zeapo.pwdstore.pwgenDialogFragment;
import com.zeapo.pwdstore.utils.PasswordRepository; import com.zeapo.pwdstore.utils.PasswordRepository;
@ -57,6 +61,9 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne
private Activity activity; private Activity activity;
ClipboardManager clipboard; ClipboardManager clipboard;
SelectFolderFragment passwordList;
private Intent selectFolderData;
private boolean registered; private boolean registered;
public static final int REQUEST_CODE_SIGN = 9910; 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 = 9914;
public static final int REQUEST_CODE_GET_KEY_IDS = 9915; public static final int REQUEST_CODE_GET_KEY_IDS = 9915;
public static final int REQUEST_CODE_EDIT = 9916; public static final int REQUEST_CODE_EDIT = 9916;
public static final int REQUEST_CODE_SELECT_FOLDER = 9917;
public final class Constants { public final class Constants {
public static final String TAG = "Keychain"; public static final String TAG = "Keychain";
@ -125,10 +133,15 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present. // Inflate the menu; this adds items to the action bar if it is present.
if (getIntent().getStringExtra("Operation").equals("ENCRYPT")) { switch (getIntent().getStringExtra("Operation")){
getMenuInflater().inflate(R.menu.pgp_handler_new_password, menu); case "ENCRYPT":
} else { getMenuInflater().inflate(R.menu.pgp_handler_new_password, menu);
getMenuInflater().inflate(R.menu.pgp_handler, menu); break;
case "SELECTFOLDER":
getMenuInflater().inflate(R.menu.pgp_handler_select_folder, menu);
break;
default:
getMenuInflater().inflate(R.menu.pgp_handler, menu);
} }
return true; return true;
} }
@ -160,10 +173,22 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne
setResult(RESULT_CANCELED); setResult(RESULT_CANCELED);
finish(); finish();
return true; return true;
case R.id.crypto_select:
selectFolder();
break;
} }
return super.onOptionsItemSelected(item); 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() { public void editPassword() {
// if in encrypt or in decrypt and password is invisible // if in encrypt or in decrypt and password is invisible
// (because !showPassword, so this will instantly close), do nothing // (because !showPassword, so this will instantly close), do nothing
@ -259,6 +284,7 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne
DialogFragment df = new pwgenDialogFragment(); DialogFragment df = new pwgenDialogFragment();
df.show(getFragmentManager(), "generator"); df.show(getFragmentManager(), "generator");
default: default:
Log.wtf(Constants.TAG,"This should not happen.... PgpHandler.java#handleClick(View) default reached.");
// should not happen // 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 { public class PgpCallback implements OpenPgpApi.IOpenPgpCallback {
boolean returnToCiphertextField; boolean returnToCiphertextField;
ByteArrayOutputStream os; ByteArrayOutputStream os;
@ -664,7 +723,11 @@ public class PgpHandler extends AppCompatActivity implements OpenPgpServiceConne
Log.i("PGP", "ISBOUND!!"); Log.i("PGP", "ISBOUND!!");
Bundle extra = getIntent().getExtras(); 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); setContentView(R.layout.decrypt_layout);
((TextView) findViewById(R.id.crypto_password_file)).setText(extra.getString("NAME")); ((TextView) findViewById(R.id.crypto_password_file)).setText(extra.getString("NAME"));
String cat = new File(extra.getString("FILE_PATH").replace(PasswordRepository.getWorkTree().getAbsolutePath(), "")) 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 + "/"); ((TextView) findViewById(R.id.crypto_password_category)).setText(cat + "/");
decryptAndVerify(new Intent()); decryptAndVerify(new Intent());
} else if (extra.getString("Operation").equals("ENCRYPT")) { } else if (operation.equals("ENCRYPT")) {
setContentView(R.layout.encrypt_layout); setContentView(R.layout.encrypt_layout);
Typeface monoTypeface = Typeface.createFromAsset(getAssets(), "fonts/sourcecodepro.ttf"); Typeface monoTypeface = Typeface.createFromAsset(getAssets(), "fonts/sourcecodepro.ttf");
((EditText) findViewById(R.id.crypto_password_edit)).setTypeface(monoTypeface); ((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.replace(PasswordRepository.getWorkTree().getAbsolutePath(), "");
cat = cat + "/"; cat = cat + "/";
((TextView) findViewById(R.id.crypto_password_category)).setText(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()); getKeyIds(new Intent());
// setContentView(R.layout.key_id); // 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]; // String keys = keyIDs.split(",").length > 1 ? keyIDs : keyIDs.split(",")[0];
// ((TextView) findViewById(R.id.crypto_key_ids)).setText(keys); // ((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); setContentView(R.layout.decrypt_layout);
((TextView) findViewById(R.id.crypto_password_file)).setText(extra.getString("NAME")); ((TextView) findViewById(R.id.crypto_password_file)).setText(extra.getString("NAME"));
String cat = new File(extra.getString("FILE_PATH").replace(PasswordRepository.getWorkTree().getAbsolutePath(), "")) 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 + "/"); ((TextView) findViewById(R.id.crypto_password_category)).setText(cat + "/");
edit(new Intent()); edit(new Intent());
} else if (operation.equals("SELECTFOLDER")){
setContentView(R.layout.select_folder_layout);
selectFolder(getIntent());
} }
} }

View file

@ -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);
}
}

View file

@ -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());
}
};
}
}

View file

@ -1,17 +1,10 @@
package com.zeapo.pwdstore.utils; package com.zeapo.pwdstore.utils;
import android.graphics.Color; import android.support.annotation.NonNull;
import android.os.Build;
import android.support.v4.content.ContextCompat;
import android.support.v7.view.ActionMode; import android.support.v7.view.ActionMode;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; 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.PasswordFragment;
import com.zeapo.pwdstore.PasswordStore; import com.zeapo.pwdstore.PasswordStore;
@ -19,100 +12,25 @@ import com.zeapo.pwdstore.R;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
public class PasswordRecyclerAdapter extends RecyclerView.Adapter<PasswordRecyclerAdapter.ViewHolder> { public class PasswordRecyclerAdapter extends EntryRecyclerAdapter {
private final PasswordStore activity; private final PasswordStore activity;
private final ArrayList<PasswordItem> values;
private final PasswordFragment.OnFragmentInteractionListener listener; private final PasswordFragment.OnFragmentInteractionListener listener;
private final Set<Integer> selectedItems;
private ActionMode mActionMode; private ActionMode mActionMode;
private Boolean canEdit; 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) // Provide a suitable constructor (depends on the kind of dataset)
public PasswordRecyclerAdapter(PasswordStore activity, PasswordFragment.OnFragmentInteractionListener listener, ArrayList<PasswordItem> values) { public PasswordRecyclerAdapter(PasswordStore activity, PasswordFragment.OnFragmentInteractionListener listener, ArrayList<PasswordItem> values) {
this.values = values; super(activity, values);
this.activity = activity; this.activity = activity;
this.listener = listener; this.listener = listener;
selectedItems = new TreeSet<>();
} }
// Create new views (invoked by the layout manager)
@Override @Override
public PasswordRecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, @NonNull
int viewType) { protected View.OnLongClickListener getOnLongClickListener(final ViewHolder holder, final PasswordItem pass) {
// create a new view return new View.OnLongClickListener() {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.password_row_layout, parent, false);
return new ViewHolder(v);
}
// 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() {
@Override
public void onClick(View v) {
if (mActionMode != null) {
toggleSelection(holder.getAdapterPosition());
mActionMode.setTitle("" + selectedItems.size());
if (selectedItems.isEmpty()) {
mActionMode.finish();
} else if (selectedItems.size() == 1 && !canEdit) {
if (values.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());
}
});
holder.view.setOnLongClickListener(new View.OnLongClickListener() {
@Override @Override
public boolean onLongClick(View v) { public boolean onLongClick(View v) {
if (mActionMode != null) { if (mActionMode != null) {
@ -127,18 +45,35 @@ public class PasswordRecyclerAdapter extends RecyclerView.Adapter<PasswordRecycl
notifyItemChanged(holder.getAdapterPosition()); notifyItemChanged(holder.getAdapterPosition());
return true; return true;
} }
}); };
}
// after removal, everything is rebound for some reason; views are shuffled? @Override
boolean selected = selectedItems.contains(position); @NonNull
holder.view.setSelected(selected); protected View.OnClickListener getOnClickListener(final ViewHolder holder, final PasswordItem pass) {
if (selected) { return new View.OnClickListener() {
holder.itemView.setBackgroundResource(R.color.deep_orange_200); @Override
holder.type.setTextColor(Color.BLACK); public void onClick(View v) {
} else { if (mActionMode != null) {
holder.itemView.setBackgroundResource(Color.alpha(1)); toggleSelection(holder.getAdapterPosition());
holder.type.setTextColor(ContextCompat.getColor(activity, R.color.grey_500)); 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());
}
};
} }
private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { 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 mode.finish(); // Action picked, so close the CAB
return true; return true;
case R.id.menu_edit_password: case R.id.menu_edit_password:
activity.editPassword(values.get(selectedItems.iterator().next())); activity.editPassword(getValues().get(selectedItems.iterator().next()));
mode.finish(); mode.finish();
return true; 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: default:
return false; return false;
} }
@ -183,67 +124,12 @@ public class PasswordRecyclerAdapter extends RecyclerView.Adapter<PasswordRecycl
// Called when the user exits the action mode // Called when the user exits the action mode
@Override @Override
public void onDestroyActionMode(ActionMode mode) { 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 // need the setSelected line in onBind
notifyItemChanged((Integer) it.next()); notifyItemChanged(it.next());
it.remove(); it.remove();
} }
mActionMode = null; 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);
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

View file

@ -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>

View file

@ -3,10 +3,15 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".pwdstore"> 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" <item android:id="@+id/menu_edit_password"
android:icon="@drawable/ic_edit_white_24dp" android:icon="@drawable/ic_edit_white_24dp"
app:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:title="Edit"/> android:title="Edit"/>
<item android:id="@+id/menu_delete_password" <item android:id="@+id/menu_delete_password"
android:icon="@drawable/ic_delete_white_24dp" android:icon="@drawable/ic_delete_white_24dp"

View file

@ -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>