************************************************************************
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) { }
}
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************
************************************************************************