Java tutorial
/** * Copyright 2014 Cody Munger * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.munger.passwordkeeper.view; import java.util.ArrayList; import java.util.List; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; import android.text.Editable; import android.text.TextWatcher; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import com.munger.passwordkeeper.MainActivity; import com.munger.passwordkeeper.R; import com.munger.passwordkeeper.alert.ConfirmFragment; import com.munger.passwordkeeper.struct.PasswordDetails; import com.munger.passwordkeeper.struct.PasswordDocumentFile; import com.munger.passwordkeeper.view.widget.DetailItemWidget; import com.munger.passwordkeeper.view.widget.TextInputWidget; public class ViewDetailFragment extends Fragment { private MainActivity parent; private View root = null; /** label to display the detail name */ private TextInputWidget nameLabel; /** label to display the detail URL/location */ private TextInputWidget locationLabel; /** list to display pair widgets */ private ListView itemList; /** button to add a new pair in edit mode */ private Button addButton; /** the current details we're working on */ private PasswordDetails details = null; /** the original details in case we want to revert changes */ private PasswordDetails originalDetails = null; public static String getName() { return "Detail"; } /** listener opens the copy submenu on a long click */ private View.OnLongClickListener copyMenuListener; @Override public void onSaveInstanceState(Bundle outState) { if (details != null && parent.document instanceof PasswordDocumentFile) { outState.putString("file", parent.document.name); } //else if (details != null &&) outState.putString("password", parent.password); outState.putString("index", details.index); }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); parent = (MainActivity) getActivity(); if (savedInstanceState != null) { String file = savedInstanceState.getString("file"); String password = savedInstanceState.getString("password"); String index = savedInstanceState.getString("index"); parent.setFile(file, password); parent.setDetails(index); parent.fragmentExists(this); setDetails(parent.getDetails()); } }; /** * Gather references to commonly used components and setup event handlers. */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { root = inflater.inflate(R.layout.fragment_viewdetail, container, false); setHasOptionsMenu(true); //get common components for later reference nameLabel = (TextInputWidget) root.findViewById(R.id.viewdetail_namelbl); locationLabel = (TextInputWidget) root.findViewById(R.id.viewdetail_locationlbl); itemList = (ListView) root.findViewById(R.id.viewdetail_itemlist); addButton = (Button) root.findViewById(R.id.viewdetail_addbtn); nameLabel.getLabel().setOnLongClickListener(copyMenuListener); locationLabel.getLabel().setOnLongClickListener(copyMenuListener); if (details != null) setupFields(); //copy any field that gets a long click copyMenuListener = new View.OnLongClickListener() { public boolean onLongClick(View v) { if (actionMode != null) return false; if (!(v instanceof TextView)) return false; actionSelected = (TextView) v; selectText(actionSelected, true); actionMode = parent.startActionMode(actionCallback); return true; } }; //update the changeable copy of the details whenever a change to any text input is done nameLabel.setInputChangeListener(new TextInputWidget.InputChangedListener() { public void changed() { if (details != null) details.name = nameLabel.getText(); } }); locationLabel.setInputChangeListener(new TextInputWidget.InputChangedListener() { public void changed() { if (details != null) details.location = locationLabel.getText(); } }); //add a pair to the current details if the add button is clicked addButton.setOnClickListener(new View.OnClickListener() { public void onClick(View arg0) { if (details == null) return; addPair(); } }); setupEditable(); return root; } /** currently displaying the filtered list of pairs */ private boolean useFiltered = false; /** list generator for the unfiltered pairs list */ private DetailArrayAdapter pairListAdapter = null; /** list generator for the filtered pairs list */ private DetailArrayAdapter filterAdapter = null; /** filtered list filled in on a search */ private ArrayList<PasswordDetails.Pair> filtered = new ArrayList<PasswordDetails.Pair>(); /** * Filter the pairs list when a search is run. * Pairs will be filtered based on weather the key or value contains the provided string * @param orig The complete list of pairs belongs to this password detail * @param search The string to search on. * @return the filtered list of pairs */ public ArrayList<PasswordDetails.Pair> searchDetails(ArrayList<PasswordDetails.Pair> orig, String search) { ArrayList<PasswordDetails.Pair> ret = new ArrayList<PasswordDetails.Pair>(); search = search.toLowerCase(); for (PasswordDetails.Pair pair : orig) { if (pair.key.toLowerCase().contains(search) || pair.value.toLowerCase().contains(search)) { ret.add(pair); } } return ret; } /** * Setup handlers for the edit mode and searching */ @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.main, menu); MenuItem searchItem = menu.findItem(R.id.action_search); SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { public boolean onQueryTextSubmit(String arg0) { return false; } //switch to the filtered view when the serach bar has text in it public boolean onQueryTextChange(String arg0) { if (arg0.isEmpty()) { if (useFiltered == true) { useFiltered = false; itemList.setAdapter(pairListAdapter); } } else { if (details != null) { filtered = searchDetails(details.details, arg0); filterAdapter.clear(); filterAdapter.addAll(filtered); if (useFiltered == false) { useFiltered = true; itemList.setAdapter(filterAdapter); } filterAdapter.notifyDataSetChanged(); } } return false; } }); }; @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_edit) { setEditable(!editable); return true; } return super.onOptionsItemSelected(item); }; /** * Select the textView by changing the background * An interesting feature of android is changing the background on a textview will cause the padding to get reset. * @param v the view to be selected * @param selected are we selecting or deselecting the view? */ private void selectText(TextView v, boolean selected) { int left = v.getPaddingLeft(); int right = v.getPaddingRight(); int top = v.getPaddingTop(); int bott = v.getPaddingBottom(); int resid = 0; if (selected) resid = R.drawable.abc_list_selector_background_transition_holo_dark; v.setBackgroundResource(resid); v.setPadding(left, top, right, bott); } /** * Delete the specified pair from the current pairs. * @param p the pair to be deleted */ private void deletePair(PasswordDetails.Pair p) { details.details.remove(p); pairListAdapter.notifyDataSetChanged(); if (useFiltered) { filtered.remove(p); filterAdapter.remove(p); filterAdapter.notifyDataSetChanged(); } } /** * Add a new blank pair to the current pairs list. */ private void addPair() { PasswordDetails.Pair pair = new PasswordDetails.Pair(); details.details.add(pair); pairListAdapter.notifyDataSetChanged(); } /** * Set the details this fragments renders and update the view * @param dets the details to set on this fragment. */ public void setDetails(PasswordDetails dets) { originalDetails = dets; details = dets.copy(); if (root != null) { setupFields(); } } /** * populate all text labels with the current details information */ private void setupFields() { pairListAdapter = new DetailArrayAdapter(this, parent, details.details); filterAdapter = new DetailArrayAdapter(this, parent, filtered); itemList.setAdapter(pairListAdapter); nameLabel.setText(details.name); locationLabel.setText(details.location); } /** is this fragment currently in editing mode? */ private boolean editable = false; /** * change the editing mode of this fragment. * switches all the contained views to editing/view mode. * @param editable */ public void setEditable(boolean editable) { this.editable = editable; setupEditable(); if (isVisible()) { if (!editable && originalDetails != null && originalDetails.diff(details)) { saveDetails(); } } } /** * handle exiting from this view. * @return true if we want the back action to proceed. */ public boolean backPressed() { if (editable && originalDetails != null && originalDetails.diff(details)) { //if there are changes that need to be saved, a confirmation popup is brought up and the back action is cancelled. goingBack = true; saveDetails(); return false; } return true; } private boolean goingBack = false; /** * bring up a popup that asks the user if they want to save changes to the details. */ private void saveDetails() { ConfirmFragment frag = new ConfirmFragment("Save changes?", new ConfirmFragment.Listener() { public void okay() { //save the details and initiate a new back action, //this will succeed because the two copies of the details will be identical. parent.saveDetail(details); originalDetails = details; if (!goingBack) onResume(); else parent.onBackPressed(); } public void cancel() { setDetails(originalDetails); } }); frag.show(parent.getSupportFragmentManager(), "confirm_fragment"); } /** * A DetailItemWidget with data and event listeners added. */ private static class PairDetailItemWidget extends DetailItemWidget { private PasswordDetails.Pair pair; private ViewDetailFragment parent; public PairDetailItemWidget(PasswordDetails.Pair p, ViewDetailFragment par, Context context) { super(context); parent = par; //set the data for this widget setPair(p); //bring up the copy sub menu on long click keyLabel.setOnLongClickListener(parent.copyMenuListener); valueLabel.setOnLongClickListener(parent.copyMenuListener); //set up change listeners to keep the backing data up to date keyInput.addTextChangedListener(new TextWatcher() { public void onTextChanged(CharSequence s, int start, int before, int count) { } public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void afterTextChanged(Editable s) { pair.key = keyInput.getText().toString(); } }); valueInput.addTextChangedListener(new TextWatcher() { public void onTextChanged(CharSequence s, int start, int before, int count) { } public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void afterTextChanged(Editable s) { pair.value = valueInput.getText().toString(); } }); //bring up the copy/edit sub menu on a long click valueInput.setOnLongClickListener(parent.copyMenuListener); //delete this pair on delete click deleteBtn.setOnClickListener(new OnClickListener() { public void onClick(View arg0) { parent.deletePair(pair); } }); } /** * update the UI when the underlying pair is updated * @param pair */ public void setPair(PasswordDetails.Pair pair) { this.pair = pair; if (pair != null) { setKey(pair.key); setValue(pair.value); } else { setKey(""); setValue(""); } } @SuppressWarnings("unused") public PasswordDetails.Pair getPair() { return pair; } } /** * Generator for widgets when a list view is assigned a list of password details pairs. * Should generate a PairDetailItemWidget for each PasswordDetails.Pair in the list. */ private static class DetailArrayAdapter extends ArrayAdapter<PasswordDetails.Pair> { private ViewDetailFragment host; public DetailArrayAdapter(ViewDetailFragment host, Context context, List<PasswordDetails.Pair> objects) { super(context, 0, objects); this.host = host; } /** * create a new widget and update inputs and events */ @Override public View getView(int position, View convertView, final ViewGroup par) { final PasswordDetails.Pair pair = getItem(position); PairDetailItemWidget ret; //just update the views if it's a recycled item if (convertView == null) { ret = new PairDetailItemWidget(pair, host, getContext()); } else { ret = (PairDetailItemWidget) convertView; ret.setPair(pair); } ret.setEditable(host.editable); return ret; } } /** Context menu created on all inputs */ private ActionMode actionMode = null; /** The current input selected for a context menu */ private TextView actionSelected = null; /** * setup the context menu that will be assigned to all editiable input fields. * This menu will allow copy, paste, and random password generation */ private ActionMode.Callback actionCallback = new ActionMode.Callback() { public boolean onPrepareActionMode(ActionMode arg0, Menu arg1) { return false; } /** * Deselect inputs when they are no longer selected */ public void onDestroyActionMode(ActionMode arg0) { actionMode = null; selectText(actionSelected, false); actionSelected = null; } /** * Create the context menu based on the current state of the input. * uneditable are just copyable and editable are copy/paste/random */ public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inf = mode.getMenuInflater(); if (!editable) inf.inflate(R.menu.detail_action, menu); else inf.inflate(R.menu.detailedit_action, menu); return true; } /** * perform the selected action on the selected input */ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { int id = item.getItemId(); ClipboardManager clipboard = (ClipboardManager) getActivity() .getSystemService(Context.CLIPBOARD_SERVICE); if (id == R.id.action_detail_copy) { //copy the text to the clipboard ClipData clip = ClipData.newPlainText("password-keeper", actionSelected.getText().toString()); clipboard.setPrimaryClip(clip); } else if (id == R.id.action_detail_paste) { //paste the clipboard text to the input ClipData clip = clipboard.getPrimaryClip(); int sz = clip.getItemCount(); if (sz > 0) { ClipData.Item it = clip.getItemAt(0); String data = it.coerceToText(parent).toString(); actionSelected.setText(data); } } else if (id == R.id.action_detail_random) { //generate a password and copy the resulting password to the clipboard String pw = generateRandomPassword(8); actionSelected.setText(pw); ClipData clip = ClipData.newPlainText("password-keeper", pw); clipboard.setPrimaryClip(clip); } return false; } }; /** * Generate a password of length size. * The password is guaranteed to have at least one lower case, upper case, and numeric letter in it. * @param length the length in characters of the generated password * @return the generated password */ public String generateRandomPassword(int length) { StringBuilder ret; int capCount; int lowerCount; int numCount; //loop until each type of character has been generated at least once do { capCount = 0; lowerCount = 0; numCount = 0; ret = new StringBuilder(); for (int i = 0; i < length; i++) { int type = (int) (Math.random() * (double) 3); //add a lower case letter if (type == 0) { int n = (int) (Math.random() * (double) 26); ret.append((char) ('a' + n)); lowerCount++; } //add an upper case letter else if (type == 1) { int n = (int) (Math.random() * (double) 26); ret.append((char) ('A' + n)); capCount++; } //add a number else { int n = (int) (Math.random() * (double) 10); ret.append((char) ('0' + n)); numCount++; } } } while (capCount == 0 || lowerCount == 0 || numCount == 0); return ret.toString(); } /** * swap all fragment components to the proper editing mode */ private void setupEditable() { if (root == null) return; nameLabel.setEditable(editable); //remove/replace default data if it exists if (editable && nameLabel.getText().equals("new entry")) { nameLabel.setText(""); nameLabel.requestFocus(); } else if (!editable && nameLabel.getText().isEmpty()) nameLabel.setText("new entry"); //set all components in the fragment to editable mode locationLabel.setEditable(editable); addButton.setVisibility(editable ? View.VISIBLE : View.GONE); int sz = details.details.size(); if (sz == 0) { pairListAdapter.add(new PasswordDetails.Pair()); pairListAdapter.notifyDataSetChanged(); } sz = itemList.getCount(); for (int i = 0; i < sz; i++) { DetailItemWidget item = (DetailItemWidget) itemList.getChildAt(i); if (item != null) item.setEditable(editable); } root.invalidate(); } }