************************************************************************ ItemDivider.java ************************************************************************ // ItemDivider.java // Class that defines dividers displayed between the RecyclerView items; // based on Google's sample implementation at bit.ly/DividerItemDecoration package at.htl.addressbook; import android.content.Context; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.support.v7.widget.RecyclerView; import android.view.View; class ItemDivider extends RecyclerView.ItemDecoration { private final Drawable divider; // constructor loads built-in Android list item divider public ItemDivider(Context context) { int[] attrs = {android.R.attr.listDivider}; divider = context.obtainStyledAttributes(attrs).getDrawable(0); } // draws the list item dividers onto the RecyclerView @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); // calculate left/right x-coordinates for all dividers int left = parent.getPaddingLeft(); int right = parent.getWidth() - parent.getPaddingRight(); // for every item but the last, draw a line below it for (int i = 0; i < parent.getChildCount() - 1; ++i) { View item = parent.getChildAt(i); // get ith list item // calculate top/bottom y-coordinates for current divider int top = item.getBottom() + ((RecyclerView.LayoutParams) item.getLayoutParams()).bottomMargin; int bottom = top + divider.getIntrinsicHeight(); // draw the divider with the calculated bounds divider.setBounds(left, top, right, bottom); divider.draw(c); } } } ************************************************************************ strings.xml ************************************************************************ AddressBook Edit Delete Name (Required) E-Mail Phone Street City State Zip Name: E-Mail: Phone: Street: City: State: Zip: Are You Sure? This will permanently delete the contact Cancel Delete Contact added successfully Contact was not added due to an error Contact updated Contact was not updated due to an error Invalid query Uri: Invalid insert Uri: Invalid update Uri: Invalid delete Uri: Insert failed: ************************************************************************ content_main.xml for tablets (!) ************************************************************************ ************************************************************************ fragment_details.xml ************************************************************************ ************************************************************************ fragment_details_menu.xml ************************************************************************ ************************************************************************ DatabaseDescription.java ************************************************************************ // DatabaseDescription.java // Describes the table name and column names for this app's database, // and other information required by the ContentProvider package at.htl.addressbook.data; import android.content.ContentUris; import android.net.Uri; import android.provider.BaseColumns; public class DatabaseDescription { // ContentProvider's name: typically the package name public static final String AUTHORITY = "at.htl.addressbook.data"; // base URI used to interact with the ContentProvider private static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY); // nested class defines contents of the contacts table public static final class Contact implements BaseColumns { public static final String TABLE_NAME = "contacts"; // table's name // Uri for the contacts table public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon().appendPath(TABLE_NAME).build(); // column names for contacts table's columns public static final String COLUMN_NAME = "name"; public static final String COLUMN_PHONE = "phone"; public static final String COLUMN_EMAIL = "email"; public static final String COLUMN_STREET = "street"; public static final String COLUMN_CITY = "city"; public static final String COLUMN_STATE = "state"; public static final String COLUMN_ZIP = "zip"; // creates a Uri for a specific contact public static Uri buildContactUri(long id) { return ContentUris.withAppendedId(CONTENT_URI, id); } } } ************************************************************************ AddEditFragment.java ************************************************************************ // Fragment for adding a new contact or editing an existing one package at.htl.addressbook; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.design.widget.TextInputLayout; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import at.htl.addressbook.data.DatabaseDescription; import static at.htl.addressbook.data.DatabaseDescription.*; public class AddEditFragment extends Fragment implements LoaderManager.LoaderCallbacks { // defines callback method implemented by MainActivity public interface AddEditFragmentListener { // called when contact is saved void onAddEditCompleted(Uri contactUri); } // constant used to identify the Loader private static final int CONTACT_LOADER = 0; private AddEditFragmentListener listener; // MainActivity private Uri contactUri; // Uri of selected contact private boolean addingNewContact = true; // adding (true) or editing // EditTexts for contact information private TextInputLayout nameTextInputLayout; private TextInputLayout phoneTextInputLayout; private TextInputLayout emailTextInputLayout; private TextInputLayout streetTextInputLayout; private TextInputLayout cityTextInputLayout; private TextInputLayout stateTextInputLayout; private TextInputLayout zipTextInputLayout; private FloatingActionButton saveContactFAB; private CoordinatorLayout coordinatorLayout; // used with SnackBars // set AddEditFragmentListener when Fragment attached @Override public void onAttach(Context context) { super.onAttach(context); listener = (AddEditFragmentListener) context; } // remove AddEditFragmentListener when Fragment detached @Override public void onDetach() { super.onDetach(); listener = null; } // called when Fragment's view needs to be created @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); setHasOptionsMenu(true); // fragment has menu items to display // inflate GUI and get references to EditTexts View view = inflater.inflate(R.layout.fragment_add_edit, container, false); nameTextInputLayout = (TextInputLayout) view.findViewById(R.id.nameTextInputLayout); nameTextInputLayout.getEditText().addTextChangedListener( nameChangedListener); phoneTextInputLayout = (TextInputLayout) view.findViewById(R.id.phoneTextInputLayout); emailTextInputLayout = (TextInputLayout) view.findViewById(R.id.emailTextInputLayout); streetTextInputLayout = (TextInputLayout) view.findViewById(R.id.streetTextInputLayout); cityTextInputLayout = (TextInputLayout) view.findViewById(R.id.cityTextInputLayout); stateTextInputLayout = (TextInputLayout) view.findViewById(R.id.stateTextInputLayout); zipTextInputLayout = (TextInputLayout) view.findViewById(R.id.zipTextInputLayout); // set FloatingActionButton's event listener saveContactFAB = (FloatingActionButton) view.findViewById( R.id.saveFloatingActionButton); saveContactFAB.setOnClickListener(saveContactButtonClicked); updateSaveButtonFAB(); // used to display SnackBars with brief messages coordinatorLayout = (CoordinatorLayout) getActivity().findViewById( R.id.coordinatorLayout); Bundle arguments = getArguments(); // null if creating new contact if (arguments != null) { addingNewContact = false; contactUri = arguments.getParcelable(MainActivity.CONTACT_URI); } // if editing an existing contact, create Loader to get the contact if (contactUri != null) { getLoaderManager().initLoader(CONTACT_LOADER, null, this); } return view; } // detects when the text in the nameTextInputLayout's EditText changes // to hide or show saveButtonFAB private final TextWatcher nameChangedListener = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} // called when the text in nameTextInputLayout changes @Override public void onTextChanged(CharSequence s, int start, int before, int count) { updateSaveButtonFAB(); } @Override public void afterTextChanged(Editable s) { } }; // shows saveButtonFAB only if the name is not empty private void updateSaveButtonFAB() { String input = nameTextInputLayout.getEditText().getText().toString(); // if there is a name for the contact, show the FloatingActionButton if (input.trim().length() != 0) { saveContactFAB.show(); } else { saveContactFAB.hide(); } } // responds to event generated when user saves a contact private final View.OnClickListener saveContactButtonClicked = new View.OnClickListener() { @Override public void onClick(View v) { // hide the virtual keyboard ((InputMethodManager) getActivity().getSystemService( Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow( getView().getWindowToken(), 0); saveContact(); // save contact to the database } }; // saves contact information to the database private void saveContact() { // create ContentValues object containing contact's key-value pairs ContentValues contentValues = new ContentValues(); contentValues.put(Contact.COLUMN_NAME, nameTextInputLayout.getEditText().getText().toString()); contentValues.put(Contact.COLUMN_PHONE, phoneTextInputLayout.getEditText().getText().toString()); contentValues.put(Contact.COLUMN_EMAIL, emailTextInputLayout.getEditText().getText().toString()); contentValues.put(Contact.COLUMN_STREET, streetTextInputLayout.getEditText().getText().toString()); contentValues.put(Contact.COLUMN_CITY, cityTextInputLayout.getEditText().getText().toString()); contentValues.put(Contact.COLUMN_STATE, stateTextInputLayout.getEditText().getText().toString()); contentValues.put(Contact.COLUMN_ZIP, zipTextInputLayout.getEditText().getText().toString()); if (addingNewContact) { // use Activity's ContentResolver to invoke // insert on the AddressBookContentProvider Uri newContactUri = getActivity().getContentResolver().insert( Contact.CONTENT_URI, contentValues); if (newContactUri != null) { Snackbar.make(coordinatorLayout, R.string.contact_added, Snackbar.LENGTH_LONG).show(); listener.onAddEditCompleted(newContactUri); } else { Snackbar.make(coordinatorLayout, R.string.contact_not_added, Snackbar.LENGTH_LONG).show(); } } else { // use Activity's ContentResolver to invoke // insert on the AddressBookContentProvider int updatedRows = getActivity().getContentResolver().update( contactUri, contentValues, null, null); if (updatedRows > 0) { listener.onAddEditCompleted(contactUri); Snackbar.make(coordinatorLayout, R.string.contact_updated, Snackbar.LENGTH_LONG).show(); } else { Snackbar.make(coordinatorLayout, R.string.contact_not_updated, Snackbar.LENGTH_LONG).show(); } } } // called by LoaderManager to create a Loader @Override public Loader onCreateLoader(int id, Bundle args) { // create an appropriate CursorLoader based on the id argument; // only one Loader in this fragment, so the switch is unnecessary switch (id) { case CONTACT_LOADER: return new CursorLoader(getActivity(), contactUri, // Uri of contact to display null, // null projection returns all columns null, // null selection returns all rows null, // no selection arguments null); // sort order default: return null; } } // called by LoaderManager when loading completes @Override public void onLoadFinished(Loader loader, Cursor data) { // if the contact exists in the database, display its data if (data != null && data.moveToFirst()) { // get the column index for each data item int nameIndex = data.getColumnIndex(Contact.COLUMN_NAME); int phoneIndex = data.getColumnIndex(Contact.COLUMN_PHONE); int emailIndex = data.getColumnIndex(Contact.COLUMN_EMAIL); int streetIndex = data.getColumnIndex(Contact.COLUMN_STREET); int cityIndex = data.getColumnIndex(Contact.COLUMN_CITY); int stateIndex = data.getColumnIndex(Contact.COLUMN_STATE); int zipIndex = data.getColumnIndex(Contact.COLUMN_ZIP); // fill EditTexts with the retrieved data nameTextInputLayout.getEditText().setText( data.getString(nameIndex)); phoneTextInputLayout.getEditText().setText( data.getString(phoneIndex)); emailTextInputLayout.getEditText().setText( data.getString(emailIndex)); streetTextInputLayout.getEditText().setText( data.getString(streetIndex)); cityTextInputLayout.getEditText().setText( data.getString(cityIndex)); stateTextInputLayout.getEditText().setText( data.getString(stateIndex)); zipTextInputLayout.getEditText().setText( data.getString(zipIndex)); updateSaveButtonFAB(); } } // called by LoaderManager when the Loader is being reset @Override public void onLoaderReset(Loader loader) { } } ************************************************************************ DetailFragment.java ************************************************************************ // Fragment subclass that displays one contact's details package at.htl.addressbook; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v7.app.AlertDialog; 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.TextView; import at.htl.addressbook.data.DatabaseDescription; import static at.htl.addressbook.data.DatabaseDescription.*; public class DetailFragment extends Fragment implements LoaderManager.LoaderCallbacks { // callback methods implemented by MainActivity public interface DetailFragmentListener { void onContactDeleted(); // called when a contact is deleted // pass Uri of contact to edit to the DetailFragmentListener void onEditContact(Uri contactUri); } private static final int CONTACT_LOADER = 0; // identifies the Loader private DetailFragmentListener listener; // MainActivity private Uri contactUri; // Uri of selected contact private TextView nameTextView; // displays contact's name private TextView phoneTextView; // displays contact's phone private TextView emailTextView; // displays contact's email private TextView streetTextView; // displays contact's street private TextView cityTextView; // displays contact's city private TextView stateTextView; // displays contact's state private TextView zipTextView; // displays contact's zip // set DetailFragmentListener when fragment attached @Override public void onAttach(Context context) { super.onAttach(context); listener = (DetailFragmentListener) context; } // remove DetailFragmentListener when fragment detached @Override public void onDetach() { super.onDetach(); listener = null; } // called when DetailFragmentListener's view needs to be created @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); setHasOptionsMenu(true); // this fragment has menu items to display // get Bundle of arguments then extract the contact's Uri Bundle arguments = getArguments(); if (arguments != null) { contactUri = arguments.getParcelable(MainActivity.CONTACT_URI); } // inflate DetailFragment's layout View view = inflater.inflate(R.layout.fragment_detail, container, false); // get the EditTexts nameTextView = (TextView) view.findViewById(R.id.nameTextView); phoneTextView = (TextView) view.findViewById(R.id.phoneTextView); emailTextView = (TextView) view.findViewById(R.id.emailTextView); streetTextView = (TextView) view.findViewById(R.id.streetTextView); cityTextView = (TextView) view.findViewById(R.id.cityTextView); stateTextView = (TextView) view.findViewById(R.id.stateTextView); zipTextView = (TextView) view.findViewById(R.id.zipTextView); // load the contact getLoaderManager().initLoader(CONTACT_LOADER, null, this); return view; } // display this fragment's menu items @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.fragment_details_menu, menu); } // handle menu item selections @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_edit: listener.onEditContact(contactUri); // pass Uri to listener return true; case R.id.action_delete: deleteContact(); return true; } return super.onOptionsItemSelected(item); } // delete a contact private void deleteContact() { // use FragmentManager to display the confirmDelete DialogFragment confirmDelete.show(getFragmentManager(), "confirm delete"); } // DialogFragment to confirm deletion of contact private final DialogFragment confirmDelete = new DialogFragment() { // create an AlertDialog and return it @Override public Dialog onCreateDialog(Bundle bundle) { // create a new AlertDialog Builder AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.confirm_title); builder.setMessage(R.string.confirm_message); // provide an OK button that simply dismisses the dialog builder.setPositiveButton(R.string.button_delete, new DialogInterface.OnClickListener() { @Override public void onClick( DialogInterface dialog, int button) { // use Activity's ContentResolver to invoke // delete on the AddressBookContentProvider getActivity().getContentResolver().delete( contactUri, null, null); listener.onContactDeleted(); // notify listener } } ); builder.setNegativeButton(R.string.button_cancel, null); return builder.create(); // return the AlertDialog } }; // called by LoaderManager to create a Loader @Override public Loader onCreateLoader(int id, Bundle args) { // create an appropriate CursorLoader based on the id argument; // only one Loader in this fragment, so the switch is unnecessary CursorLoader cursorLoader; switch (id) { case CONTACT_LOADER: cursorLoader = new CursorLoader(getActivity(), contactUri, // Uri of contact to display null, // null projection returns all columns null, // null selection returns all rows null, // no selection arguments null); // sort order break; default: cursorLoader = null; break; } return cursorLoader; } // called by LoaderManager when loading completes @Override public void onLoadFinished(Loader loader, Cursor data) { // if the contact exists in the database, display its data if (data != null && data.moveToFirst()) { // get the column index for each data item int nameIndex = data.getColumnIndex(Contact.COLUMN_NAME); int phoneIndex = data.getColumnIndex(Contact.COLUMN_PHONE); int emailIndex = data.getColumnIndex(Contact.COLUMN_EMAIL); int streetIndex = data.getColumnIndex(Contact.COLUMN_STREET); int cityIndex = data.getColumnIndex(Contact.COLUMN_CITY); int stateIndex = data.getColumnIndex(Contact.COLUMN_STATE); int zipIndex = data.getColumnIndex(Contact.COLUMN_ZIP); // fill TextViews with the retrieved data nameTextView.setText(data.getString(nameIndex)); phoneTextView.setText(data.getString(phoneIndex)); emailTextView.setText(data.getString(emailIndex)); streetTextView.setText(data.getString(streetIndex)); cityTextView.setText(data.getString(cityIndex)); stateTextView.setText(data.getString(stateIndex)); zipTextView.setText(data.getString(zipIndex)); } } // called by LoaderManager when the Loader is being reset @Override public void onLoaderReset(Loader loader) { } } ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************ ************************************************************************