diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7b6a6914..311a069c 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -29,7 +29,6 @@
@@ -37,6 +36,10 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".CatalogActivity" />
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/pets/CatalogActivity.java b/app/src/main/java/com/example/android/pets/CatalogActivity.java
index 87d8a38c..78935997 100755
--- a/app/src/main/java/com/example/android/pets/CatalogActivity.java
+++ b/app/src/main/java/com/example/android/pets/CatalogActivity.java
@@ -15,28 +15,37 @@
*/
package com.example.android.pets;
+import android.app.LoaderManager;
+import android.content.ContentUris;
import android.content.ContentValues;
+import android.content.CursorLoader;
import android.content.Intent;
+import android.content.Loader;
import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
-import android.widget.TextView;
+import android.widget.AdapterView;
+import android.widget.ListView;
import com.example.android.pets.data.PetContract.PetEntry;
-import com.example.android.pets.data.PetDbHelper;
/**
* Displays list of pets that were entered and stored in the app.
*/
-public class CatalogActivity extends AppCompatActivity {
+public class CatalogActivity extends AppCompatActivity implements
+ LoaderManager.LoaderCallbacks {
- /** Database helper that will provide us access to the database */
- private PetDbHelper mDbHelper;
+ /** Identifier for the pet data loader */
+ private static final int PET_LOADER = 0;
+
+ /** Adapter for the ListView */
+ PetCursorAdapter mCursorAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -53,98 +62,48 @@ public void onClick(View view) {
}
});
- // To access our database, we instantiate our subclass of SQLiteOpenHelper
- // and pass the context, which is the current activity.
- mDbHelper = new PetDbHelper(this);
- }
+ // Find the ListView which will be populated with the pet data
+ ListView petListView = (ListView) findViewById(R.id.list);
- @Override
- protected void onStart() {
- super.onStart();
- displayDatabaseInfo();
- }
+ // Find and set empty view on the ListView, so that it only shows when the list has 0 items.
+ View emptyView = findViewById(R.id.empty_view);
+ petListView.setEmptyView(emptyView);
- /**
- * Temporary helper method to display information in the onscreen TextView about the state of
- * the pets database.
- */
- private void displayDatabaseInfo() {
- // Create and/or open a database to read from it
- SQLiteDatabase db = mDbHelper.getReadableDatabase();
+ // Setup an Adapter to create a list item for each row of pet data in the Cursor.
+ // There is no pet data yet (until the loader finishes) so pass in null for the Cursor.
+ mCursorAdapter = new PetCursorAdapter(this, null);
+ petListView.setAdapter(mCursorAdapter);
- // Define a projection that specifies which columns from the database
- // you will actually use after this query.
- String[] projection = {
- PetEntry._ID,
- PetEntry.COLUMN_PET_NAME,
- PetEntry.COLUMN_PET_BREED,
- PetEntry.COLUMN_PET_GENDER,
- PetEntry.COLUMN_PET_WEIGHT };
-
- // Perform a query on the pets table
- Cursor cursor = db.query(
- PetEntry.TABLE_NAME, // The table to query
- projection, // The columns to return
- null, // The columns for the WHERE clause
- null, // The values for the WHERE clause
- null, // Don't group the rows
- null, // Don't filter by row groups
- null); // The sort order
-
- TextView displayView = (TextView) findViewById(R.id.text_view_pet);
-
- try {
- // Create a header in the Text View that looks like this:
- //
- // The pets table contains pets.
- // _id - name - breed - gender - weight
- //
- // In the while loop below, iterate through the rows of the cursor and display
- // the information from each column in this order.
- displayView.setText("The pets table contains " + cursor.getCount() + " pets.\n\n");
- displayView.append(PetEntry._ID + " - " +
- PetEntry.COLUMN_PET_NAME + " - " +
- PetEntry.COLUMN_PET_BREED + " - " +
- PetEntry.COLUMN_PET_GENDER + " - " +
- PetEntry.COLUMN_PET_WEIGHT + "\n");
-
- // Figure out the index of each column
- int idColumnIndex = cursor.getColumnIndex(PetEntry._ID);
- int nameColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_NAME);
- int breedColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_BREED);
- int genderColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_GENDER);
- int weightColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_WEIGHT);
-
- // Iterate through all the returned rows in the cursor
- while (cursor.moveToNext()) {
- // Use that index to extract the String or Int value of the word
- // at the current row the cursor is on.
- int currentID = cursor.getInt(idColumnIndex);
- String currentName = cursor.getString(nameColumnIndex);
- String currentBreed = cursor.getString(breedColumnIndex);
- int currentGender = cursor.getInt(genderColumnIndex);
- int currentWeight = cursor.getInt(weightColumnIndex);
- // Display the values from each column of the current row in the cursor in the TextView
- displayView.append(("\n" + currentID + " - " +
- currentName + " - " +
- currentBreed + " - " +
- currentGender + " - " +
- currentWeight));
+ // Setup the item click listener
+ petListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> adapterView, View view, int position, long id) {
+ // Create new intent to go to {@link EditorActivity}
+ Intent intent = new Intent(CatalogActivity.this, EditorActivity.class);
+
+ // Form the content URI that represents the specific pet that was clicked on,
+ // by appending the "id" (passed as input to this method) onto the
+ // {@link PetEntry#CONTENT_URI}.
+ // For example, the URI would be "content://com.example.android.pets/pets/2"
+ // if the pet with ID 2 was clicked on.
+ Uri currentPetUri = ContentUris.withAppendedId(PetEntry.CONTENT_URI, id);
+
+ // Set the URI on the data field of the intent
+ intent.setData(currentPetUri);
+
+ // Launch the {@link EditorActivity} to display the data for the current pet.
+ startActivity(intent);
}
- } finally {
- // Always close the cursor when you're done reading from it. This releases all its
- // resources and makes it invalid.
- cursor.close();
- }
+ });
+
+ // Kick off the loader
+ getLoaderManager().initLoader(PET_LOADER, null, this);
}
/**
* Helper method to insert hardcoded pet data into the database. For debugging purposes only.
*/
private void insertPet() {
- // Gets the database in write mode
- SQLiteDatabase db = mDbHelper.getWritableDatabase();
-
// Create a ContentValues object where column names are the keys,
// and Toto's pet attributes are the values.
ContentValues values = new ContentValues();
@@ -153,14 +112,19 @@ private void insertPet() {
values.put(PetEntry.COLUMN_PET_GENDER, PetEntry.GENDER_MALE);
values.put(PetEntry.COLUMN_PET_WEIGHT, 7);
- // Insert a new row for Toto in the database, returning the ID of that new row.
- // The first argument for db.insert() is the pets table name.
- // The second argument provides the name of a column in which the framework
- // can insert NULL in the event that the ContentValues is empty (if
- // this is set to "null", then the framework will not insert a row when
- // there are no values).
- // The third argument is the ContentValues object containing the info for Toto.
- long newRowId = db.insert(PetEntry.TABLE_NAME, null, values);
+ // Insert a new row for Toto into the provider using the ContentResolver.
+ // Use the {@link PetEntry#CONTENT_URI} to indicate that we want to insert
+ // into the pets database table.
+ // Receive the new content URI that will allow us to access Toto's data in the future.
+ Uri newUri = getContentResolver().insert(PetEntry.CONTENT_URI, values);
+ }
+
+ /**
+ * Helper method to delete all pets in the database.
+ */
+ private void deleteAllPets() {
+ int rowsDeleted = getContentResolver().delete(PetEntry.CONTENT_URI, null, null);
+ Log.v("CatalogActivity", rowsDeleted + " rows deleted from pet database");
}
@Override
@@ -178,13 +142,41 @@ public boolean onOptionsItemSelected(MenuItem item) {
// Respond to a click on the "Insert dummy data" menu option
case R.id.action_insert_dummy_data:
insertPet();
- displayDatabaseInfo();
return true;
// Respond to a click on the "Delete all entries" menu option
case R.id.action_delete_all_entries:
- // Do nothing for now
+ deleteAllPets();
return true;
}
return super.onOptionsItemSelected(item);
}
+
+ @Override
+ public Loader onCreateLoader(int i, Bundle bundle) {
+ // Define a projection that specifies the columns from the table we care about.
+ String[] projection = {
+ PetEntry._ID,
+ PetEntry.COLUMN_PET_NAME,
+ PetEntry.COLUMN_PET_BREED };
+
+ // This loader will execute the ContentProvider's query method on a background thread
+ return new CursorLoader(this, // Parent activity context
+ PetEntry.CONTENT_URI, // Provider content URI to query
+ projection, // Columns to include in the resulting Cursor
+ null, // No selection clause
+ null, // No selection arguments
+ null); // Default sort order
+ }
+
+ @Override
+ public void onLoadFinished(Loader loader, Cursor data) {
+ // Update {@link PetCursorAdapter} with this new cursor containing updated pet data
+ mCursorAdapter.swapCursor(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader loader) {
+ // Callback called when the data needs to be deleted
+ mCursorAdapter.swapCursor(null);
+ }
}
diff --git a/app/src/main/java/com/example/android/pets/EditorActivity.java b/app/src/main/java/com/example/android/pets/EditorActivity.java
index 78fc37b9..03f098f8 100755
--- a/app/src/main/java/com/example/android/pets/EditorActivity.java
+++ b/app/src/main/java/com/example/android/pets/EditorActivity.java
@@ -15,14 +15,22 @@
*/
package com.example.android.pets;
+import android.app.AlertDialog;
+import android.app.LoaderManager;
import android.content.ContentValues;
-import android.database.sqlite.SQLiteDatabase;
+import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
+import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
@@ -31,12 +39,18 @@
import android.widget.Toast;
import com.example.android.pets.data.PetContract.PetEntry;
-import com.example.android.pets.data.PetDbHelper;
/**
* Allows user to create a new pet or edit an existing one.
*/
-public class EditorActivity extends AppCompatActivity {
+public class EditorActivity extends AppCompatActivity implements
+ LoaderManager.LoaderCallbacks {
+
+ /** Identifier for the pet data loader */
+ private static final int EXISTING_PET_LOADER = 0;
+
+ /** Content URI for the existing pet (null if it's a new pet) */
+ private Uri mCurrentPetUri;
/** EditText field to enter the pet's name */
private EditText mNameEditText;
@@ -57,17 +71,63 @@ public class EditorActivity extends AppCompatActivity {
*/
private int mGender = PetEntry.GENDER_UNKNOWN;
+ /** Boolean flag that keeps track of whether the pet has been edited (true) or not (false) */
+ private boolean mPetHasChanged = false;
+
+ /**
+ * OnTouchListener that listens for any user touches on a View, implying that they are modifying
+ * the view, and we change the mPetHasChanged boolean to true.
+ */
+ private View.OnTouchListener mTouchListener = new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ mPetHasChanged = true;
+ return false;
+ }
+ };
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_editor);
+ // Examine the intent that was used to launch this activity,
+ // in order to figure out if we're creating a new pet or editing an existing one.
+ Intent intent = getIntent();
+ mCurrentPetUri = intent.getData();
+
+ // If the intent DOES NOT contain a pet content URI, then we know that we are
+ // creating a new pet.
+ if (mCurrentPetUri == null) {
+ // This is a new pet, so change the app bar to say "Add a Pet"
+ setTitle(getString(R.string.editor_activity_title_new_pet));
+
+ // Invalidate the options menu, so the "Delete" menu option can be hidden.
+ // (It doesn't make sense to delete a pet that hasn't been created yet.)
+ invalidateOptionsMenu();
+ } else {
+ // Otherwise this is an existing pet, so change app bar to say "Edit Pet"
+ setTitle(getString(R.string.editor_activity_title_edit_pet));
+
+ // Initialize a loader to read the pet data from the database
+ // and display the current values in the editor
+ getLoaderManager().initLoader(EXISTING_PET_LOADER, null, this);
+ }
+
// Find all relevant views that we will need to read user input from
mNameEditText = (EditText) findViewById(R.id.edit_pet_name);
mBreedEditText = (EditText) findViewById(R.id.edit_pet_breed);
mWeightEditText = (EditText) findViewById(R.id.edit_pet_weight);
mGenderSpinner = (Spinner) findViewById(R.id.spinner_gender);
+ // Setup OnTouchListeners on all the input fields, so we can determine if the user
+ // has touched or modified them. This will let us know if there are unsaved changes
+ // or not, if the user tries to leave the editor without saving.
+ mNameEditText.setOnTouchListener(mTouchListener);
+ mBreedEditText.setOnTouchListener(mTouchListener);
+ mWeightEditText.setOnTouchListener(mTouchListener);
+ mGenderSpinner.setOnTouchListener(mTouchListener);
+
setupSpinner();
}
@@ -111,21 +171,24 @@ public void onNothingSelected(AdapterView> parent) {
}
/**
- * Get user input from editor and save new pet into database.
+ * Get user input from editor and save pet into database.
*/
- private void insertPet() {
+ private void savePet() {
// Read from input fields
// Use trim to eliminate leading or trailing white space
String nameString = mNameEditText.getText().toString().trim();
String breedString = mBreedEditText.getText().toString().trim();
String weightString = mWeightEditText.getText().toString().trim();
- int weight = Integer.parseInt(weightString);
- // Create database helper
- PetDbHelper mDbHelper = new PetDbHelper(this);
-
- // Gets the database in write mode
- SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ // Check if this is supposed to be a new pet
+ // and check if all the fields in the editor are blank
+ if (mCurrentPetUri == null &&
+ TextUtils.isEmpty(nameString) && TextUtils.isEmpty(breedString) &&
+ TextUtils.isEmpty(weightString) && mGender == PetEntry.GENDER_UNKNOWN) {
+ // Since no fields were modified, we can return early without creating a new pet.
+ // No need to create ContentValues and no need to do any ContentProvider operations.
+ return;
+ }
// Create a ContentValues object where column names are the keys,
// and pet attributes from the editor are the values.
@@ -133,18 +196,47 @@ private void insertPet() {
values.put(PetEntry.COLUMN_PET_NAME, nameString);
values.put(PetEntry.COLUMN_PET_BREED, breedString);
values.put(PetEntry.COLUMN_PET_GENDER, mGender);
+ // If the weight is not provided by the user, don't try to parse the string into an
+ // integer value. Use 0 by default.
+ int weight = 0;
+ if (!TextUtils.isEmpty(weightString)) {
+ weight = Integer.parseInt(weightString);
+ }
values.put(PetEntry.COLUMN_PET_WEIGHT, weight);
- // Insert a new row for pet in the database, returning the ID of that new row.
- long newRowId = db.insert(PetEntry.TABLE_NAME, null, values);
+ // Determine if this is a new or existing pet by checking if mCurrentPetUri is null or not
+ if (mCurrentPetUri == null) {
+ // This is a NEW pet, so insert a new pet into the provider,
+ // returning the content URI for the new pet.
+ Uri newUri = getContentResolver().insert(PetEntry.CONTENT_URI, values);
- // Show a toast message depending on whether or not the insertion was successful
- if (newRowId == -1) {
- // If the row ID is -1, then there was an error with insertion.
- Toast.makeText(this, "Error with saving pet", Toast.LENGTH_SHORT).show();
+ // Show a toast message depending on whether or not the insertion was successful.
+ if (newUri == null) {
+ // If the new content URI is null, then there was an error with insertion.
+ Toast.makeText(this, getString(R.string.editor_insert_pet_failed),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ // Otherwise, the insertion was successful and we can display a toast.
+ Toast.makeText(this, getString(R.string.editor_insert_pet_successful),
+ Toast.LENGTH_SHORT).show();
+ }
} else {
- // Otherwise, the insertion was successful and we can display a toast with the row ID.
- Toast.makeText(this, "Pet saved with row id: " + newRowId, Toast.LENGTH_SHORT).show();
+ // Otherwise this is an EXISTING pet, so update the pet with content URI: mCurrentPetUri
+ // and pass in the new ContentValues. Pass in null for the selection and selection args
+ // because mCurrentPetUri will already identify the correct row in the database that
+ // we want to modify.
+ int rowsAffected = getContentResolver().update(mCurrentPetUri, values, null, null);
+
+ // Show a toast message depending on whether or not the update was successful.
+ if (rowsAffected == 0) {
+ // If no rows were affected, then there was an error with the update.
+ Toast.makeText(this, getString(R.string.editor_update_pet_failed),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ // Otherwise, the update was successful and we can display a toast.
+ Toast.makeText(this, getString(R.string.editor_update_pet_successful),
+ Toast.LENGTH_SHORT).show();
+ }
}
}
@@ -156,6 +248,21 @@ public boolean onCreateOptionsMenu(Menu menu) {
return true;
}
+ /**
+ * This method is called after invalidateOptionsMenu(), so that the
+ * menu can be updated (some menu items can be hidden or made visible).
+ */
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ // If this is a new pet, hide the "Delete" menu item.
+ if (mCurrentPetUri == null) {
+ MenuItem menuItem = menu.findItem(R.id.action_delete);
+ menuItem.setVisible(false);
+ }
+ return true;
+ }
+
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// User clicked on a menu option in the app bar overflow menu
@@ -163,20 +270,224 @@ public boolean onOptionsItemSelected(MenuItem item) {
// Respond to a click on the "Save" menu option
case R.id.action_save:
// Save pet to database
- insertPet();
+ savePet();
// Exit activity
finish();
return true;
// Respond to a click on the "Delete" menu option
case R.id.action_delete:
- // Do nothing for now
+ // Pop up confirmation dialog for deletion
+ showDeleteConfirmationDialog();
return true;
// Respond to a click on the "Up" arrow button in the app bar
case android.R.id.home:
- // Navigate back to parent activity (CatalogActivity)
- NavUtils.navigateUpFromSameTask(this);
+ // If the pet hasn't changed, continue with navigating up to parent activity
+ // which is the {@link CatalogActivity}.
+ if (!mPetHasChanged) {
+ NavUtils.navigateUpFromSameTask(EditorActivity.this);
+ return true;
+ }
+
+ // Otherwise if there are unsaved changes, setup a dialog to warn the user.
+ // Create a click listener to handle the user confirming that
+ // changes should be discarded.
+ DialogInterface.OnClickListener discardButtonClickListener =
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ // User clicked "Discard" button, navigate to parent activity.
+ NavUtils.navigateUpFromSameTask(EditorActivity.this);
+ }
+ };
+
+ // Show a dialog that notifies the user they have unsaved changes
+ showUnsavedChangesDialog(discardButtonClickListener);
return true;
}
return super.onOptionsItemSelected(item);
}
+
+ /**
+ * This method is called when the back button is pressed.
+ */
+ @Override
+ public void onBackPressed() {
+ // If the pet hasn't changed, continue with handling back button press
+ if (!mPetHasChanged) {
+ super.onBackPressed();
+ return;
+ }
+
+ // Otherwise if there are unsaved changes, setup a dialog to warn the user.
+ // Create a click listener to handle the user confirming that changes should be discarded.
+ DialogInterface.OnClickListener discardButtonClickListener =
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ // User clicked "Discard" button, close the current activity.
+ finish();
+ }
+ };
+
+ // Show dialog that there are unsaved changes
+ showUnsavedChangesDialog(discardButtonClickListener);
+ }
+
+ @Override
+ public Loader onCreateLoader(int i, Bundle bundle) {
+ // Since the editor shows all pet attributes, define a projection that contains
+ // all columns from the pet table
+ String[] projection = {
+ PetEntry._ID,
+ PetEntry.COLUMN_PET_NAME,
+ PetEntry.COLUMN_PET_BREED,
+ PetEntry.COLUMN_PET_GENDER,
+ PetEntry.COLUMN_PET_WEIGHT };
+
+ // This loader will execute the ContentProvider's query method on a background thread
+ return new CursorLoader(this, // Parent activity context
+ mCurrentPetUri, // Query the content URI for the current pet
+ projection, // Columns to include in the resulting Cursor
+ null, // No selection clause
+ null, // No selection arguments
+ null); // Default sort order
+ }
+
+ @Override
+ public void onLoadFinished(Loader loader, Cursor cursor) {
+ // Bail early if the cursor is null or there is less than 1 row in the cursor
+ if (cursor == null || cursor.getCount() < 1) {
+ return;
+ }
+
+ // Proceed with moving to the first row of the cursor and reading data from it
+ // (This should be the only row in the cursor)
+ if (cursor.moveToFirst()) {
+ // Find the columns of pet attributes that we're interested in
+ int nameColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_NAME);
+ int breedColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_BREED);
+ int genderColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_GENDER);
+ int weightColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_WEIGHT);
+
+ // Extract out the value from the Cursor for the given column index
+ String name = cursor.getString(nameColumnIndex);
+ String breed = cursor.getString(breedColumnIndex);
+ int gender = cursor.getInt(genderColumnIndex);
+ int weight = cursor.getInt(weightColumnIndex);
+
+ // Update the views on the screen with the values from the database
+ mNameEditText.setText(name);
+ mBreedEditText.setText(breed);
+ mWeightEditText.setText(Integer.toString(weight));
+
+ // Gender is a dropdown spinner, so map the constant value from the database
+ // into one of the dropdown options (0 is Unknown, 1 is Male, 2 is Female).
+ // Then call setSelection() so that option is displayed on screen as the current selection.
+ switch (gender) {
+ case PetEntry.GENDER_MALE:
+ mGenderSpinner.setSelection(1);
+ break;
+ case PetEntry.GENDER_FEMALE:
+ mGenderSpinner.setSelection(2);
+ break;
+ default:
+ mGenderSpinner.setSelection(0);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader loader) {
+ // If the loader is invalidated, clear out all the data from the input fields.
+ mNameEditText.setText("");
+ mBreedEditText.setText("");
+ mWeightEditText.setText("");
+ mGenderSpinner.setSelection(0); // Select "Unknown" gender
+ }
+
+ /**
+ * Show a dialog that warns the user there are unsaved changes that will be lost
+ * if they continue leaving the editor.
+ *
+ * @param discardButtonClickListener is the click listener for what to do when
+ * the user confirms they want to discard their changes
+ */
+ private void showUnsavedChangesDialog(
+ DialogInterface.OnClickListener discardButtonClickListener) {
+ // Create an AlertDialog.Builder and set the message, and click listeners
+ // for the postivie and negative buttons on the dialog.
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.unsaved_changes_dialog_msg);
+ builder.setPositiveButton(R.string.discard, discardButtonClickListener);
+ builder.setNegativeButton(R.string.keep_editing, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // User clicked the "Keep editing" button, so dismiss the dialog
+ // and continue editing the pet.
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ }
+ });
+
+ // Create and show the AlertDialog
+ AlertDialog alertDialog = builder.create();
+ alertDialog.show();
+ }
+
+ /**
+ * Prompt the user to confirm that they want to delete this pet.
+ */
+ private void showDeleteConfirmationDialog() {
+ // Create an AlertDialog.Builder and set the message, and click listeners
+ // for the postivie and negative buttons on the dialog.
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.delete_dialog_msg);
+ builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // User clicked the "Delete" button, so delete the pet.
+ deletePet();
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // User clicked the "Cancel" button, so dismiss the dialog
+ // and continue editing the pet.
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ }
+ });
+
+ // Create and show the AlertDialog
+ AlertDialog alertDialog = builder.create();
+ alertDialog.show();
+ }
+
+ /**
+ * Perform the deletion of the pet in the database.
+ */
+ private void deletePet() {
+ // Only perform the delete if this is an existing pet.
+ if (mCurrentPetUri != null) {
+ // Call the ContentResolver to delete the pet at the given content URI.
+ // Pass in null for the selection and selection args because the mCurrentPetUri
+ // content URI already identifies the pet that we want.
+ int rowsDeleted = getContentResolver().delete(mCurrentPetUri, null, null);
+
+ // Show a toast message depending on whether or not the delete was successful.
+ if (rowsDeleted == 0) {
+ // If no rows were deleted, then there was an error with the delete.
+ Toast.makeText(this, getString(R.string.editor_delete_pet_failed),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ // Otherwise, the delete was successful and we can display a toast.
+ Toast.makeText(this, getString(R.string.editor_delete_pet_successful),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // Close the activity
+ finish();
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/android/pets/PetCursorAdapter.java b/app/src/main/java/com/example/android/pets/PetCursorAdapter.java
new file mode 100644
index 00000000..a1ae1bca
--- /dev/null
+++ b/app/src/main/java/com/example/android/pets/PetCursorAdapter.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.example.android.pets;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.TextView;
+
+import com.example.android.pets.data.PetContract.PetEntry;
+
+/**
+ * {@link PetCursorAdapter} is an adapter for a list or grid view
+ * that uses a {@link Cursor} of pet data as its data source. This adapter knows
+ * how to create list items for each row of pet data in the {@link Cursor}.
+ */
+public class PetCursorAdapter extends CursorAdapter {
+
+ /**
+ * Constructs a new {@link PetCursorAdapter}.
+ *
+ * @param context The context
+ * @param c The cursor from which to get the data.
+ */
+ public PetCursorAdapter(Context context, Cursor c) {
+ super(context, c, 0 /* flags */);
+ }
+
+ /**
+ * Makes a new blank list item view. No data is set (or bound) to the views yet.
+ *
+ * @param context app context
+ * @param cursor The cursor from which to get the data. The cursor is already
+ * moved to the correct position.
+ * @param parent The parent to which the new view is attached to
+ * @return the newly created list item view.
+ */
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ // Inflate a list item view using the layout specified in list_item.xml
+ return LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
+ }
+
+ /**
+ * This method binds the pet data (in the current row pointed to by cursor) to the given
+ * list item layout. For example, the name for the current pet can be set on the name TextView
+ * in the list item layout.
+ *
+ * @param view Existing view, returned earlier by newView() method
+ * @param context app context
+ * @param cursor The cursor from which to get the data. The cursor is already moved to the
+ * correct row.
+ */
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ // Find individual views that we want to modify in the list item layout
+ TextView nameTextView = (TextView) view.findViewById(R.id.name);
+ TextView summaryTextView = (TextView) view.findViewById(R.id.summary);
+
+ // Find the columns of pet attributes that we're interested in
+ int nameColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_NAME);
+ int breedColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_BREED);
+
+ // Read the pet attributes from the Cursor for the current pet
+ String petName = cursor.getString(nameColumnIndex);
+ String petBreed = cursor.getString(breedColumnIndex);
+
+ // If the pet breed is empty string or null, then use some default text
+ // that says "Unknown breed", so the TextView isn't blank.
+ if (TextUtils.isEmpty(petBreed)) {
+ petBreed = context.getString(R.string.unknown_breed);
+ }
+
+ // Update the TextViews with the attributes for the current pet
+ nameTextView.setText(petName);
+ summaryTextView.setText(petBreed);
+ }
+}
diff --git a/app/src/main/java/com/example/android/pets/data/PetContract.java b/app/src/main/java/com/example/android/pets/data/PetContract.java
index 8fe4990d..e1de0978 100755
--- a/app/src/main/java/com/example/android/pets/data/PetContract.java
+++ b/app/src/main/java/com/example/android/pets/data/PetContract.java
@@ -15,6 +15,8 @@
*/
package com.example.android.pets.data;
+import android.net.Uri;
+import android.content.ContentResolver;
import android.provider.BaseColumns;
/**
@@ -26,12 +28,49 @@ public final class PetContract {
// give it an empty constructor.
private PetContract() {}
+ /**
+ * The "Content authority" is a name for the entire content provider, similar to the
+ * relationship between a domain name and its website. A convenient string to use for the
+ * content authority is the package name for the app, which is guaranteed to be unique on the
+ * device.
+ */
+ public static final String CONTENT_AUTHORITY = "com.example.android.pets";
+
+ /**
+ * Use CONTENT_AUTHORITY to create the base of all URI's which apps will use to contact
+ * the content provider.
+ */
+ public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
+
+ /**
+ * Possible path (appended to base content URI for possible URI's)
+ * For instance, content://com.example.android.pets/pets/ is a valid path for
+ * looking at pet data. content://com.example.android.pets/staff/ will fail,
+ * as the ContentProvider hasn't been given any information on what to do with "staff".
+ */
+ public static final String PATH_PETS = "pets";
+
/**
* Inner class that defines constant values for the pets database table.
* Each entry in the table represents a single pet.
*/
public static final class PetEntry implements BaseColumns {
+ /** The content URI to access the pet data in the provider */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, PATH_PETS);
+
+ /**
+ * The MIME type of the {@link #CONTENT_URI} for a list of pets.
+ */
+ public static final String CONTENT_LIST_TYPE =
+ ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PETS;
+
+ /**
+ * The MIME type of the {@link #CONTENT_URI} for a single pet.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PETS;
+
/** Name of database table for pets */
public final static String TABLE_NAME = "pets";
@@ -79,6 +118,17 @@ public static final class PetEntry implements BaseColumns {
public static final int GENDER_UNKNOWN = 0;
public static final int GENDER_MALE = 1;
public static final int GENDER_FEMALE = 2;
+
+ /**
+ * Returns whether or not the given gender is {@link #GENDER_UNKNOWN}, {@link #GENDER_MALE},
+ * or {@link #GENDER_FEMALE}.
+ */
+ public static boolean isValidGender(int gender) {
+ if (gender == GENDER_UNKNOWN || gender == GENDER_MALE || gender == GENDER_FEMALE) {
+ return true;
+ }
+ return false;
+ }
}
}
diff --git a/app/src/main/java/com/example/android/pets/data/PetProvider.java b/app/src/main/java/com/example/android/pets/data/PetProvider.java
new file mode 100644
index 00000000..3e75c0b7
--- /dev/null
+++ b/app/src/main/java/com/example/android/pets/data/PetProvider.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.example.android.pets.data;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.util.Log;
+
+import com.example.android.pets.data.PetContract.PetEntry;
+
+/**
+ * {@link ContentProvider} for Pets app.
+ */
+public class PetProvider extends ContentProvider {
+
+ /** Tag for the log messages */
+ public static final String LOG_TAG = PetProvider.class.getSimpleName();
+
+ /** URI matcher code for the content URI for the pets table */
+ private static final int PETS = 100;
+
+ /** URI matcher code for the content URI for a single pet in the pets table */
+ private static final int PET_ID = 101;
+
+ /**
+ * UriMatcher object to match a content URI to a corresponding code.
+ * The input passed into the constructor represents the code to return for the root URI.
+ * It's common to use NO_MATCH as the input for this case.
+ */
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ // Static initializer. This is run the first time anything is called from this class.
+ static {
+ // The calls to addURI() go here, for all of the content URI patterns that the provider
+ // should recognize. All paths added to the UriMatcher have a corresponding code to return
+ // when a match is found.
+
+ // The content URI of the form "content://com.example.android.pets/pets" will map to the
+ // integer code {@link #PETS}. This URI is used to provide access to MULTIPLE rows
+ // of the pets table.
+ sUriMatcher.addURI(PetContract.CONTENT_AUTHORITY, PetContract.PATH_PETS, PETS);
+
+ // The content URI of the form "content://com.example.android.pets/pets/#" will map to the
+ // integer code {@link #PET_ID}. This URI is used to provide access to ONE single row
+ // of the pets table.
+ //
+ // In this case, the "#" wildcard is used where "#" can be substituted for an integer.
+ // For example, "content://com.example.android.pets/pets/3" matches, but
+ // "content://com.example.android.pets/pets" (without a number at the end) doesn't match.
+ sUriMatcher.addURI(PetContract.CONTENT_AUTHORITY, PetContract.PATH_PETS + "/#", PET_ID);
+ }
+
+ /** Database helper object */
+ private PetDbHelper mDbHelper;
+
+ @Override
+ public boolean onCreate() {
+ mDbHelper = new PetDbHelper(getContext());
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ // Get readable database
+ SQLiteDatabase database = mDbHelper.getReadableDatabase();
+
+ // This cursor will hold the result of the query
+ Cursor cursor;
+
+ // Figure out if the URI matcher can match the URI to a specific code
+ int match = sUriMatcher.match(uri);
+ switch (match) {
+ case PETS:
+ // For the PETS code, query the pets table directly with the given
+ // projection, selection, selection arguments, and sort order. The cursor
+ // could contain multiple rows of the pets table.
+ cursor = database.query(PetEntry.TABLE_NAME, projection, selection, selectionArgs,
+ null, null, sortOrder);
+ break;
+ case PET_ID:
+ // For the PET_ID code, extract out the ID from the URI.
+ // For an example URI such as "content://com.example.android.pets/pets/3",
+ // the selection will be "_id=?" and the selection argument will be a
+ // String array containing the actual ID of 3 in this case.
+ //
+ // For every "?" in the selection, we need to have an element in the selection
+ // arguments that will fill in the "?". Since we have 1 question mark in the
+ // selection, we have 1 String in the selection arguments' String array.
+ selection = PetEntry._ID + "=?";
+ selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
+
+ // This will perform a query on the pets table where the _id equals 3 to return a
+ // Cursor containing that row of the table.
+ cursor = database.query(PetEntry.TABLE_NAME, projection, selection, selectionArgs,
+ null, null, sortOrder);
+ break;
+ default:
+ throw new IllegalArgumentException("Cannot query unknown URI " + uri);
+ }
+
+ // Set notification URI on the Cursor,
+ // so we know what content URI the Cursor was created for.
+ // If the data at this URI changes, then we know we need to update the Cursor.
+ cursor.setNotificationUri(getContext().getContentResolver(), uri);
+
+ // Return the cursor
+ return cursor;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues contentValues) {
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case PETS:
+ return insertPet(uri, contentValues);
+ default:
+ throw new IllegalArgumentException("Insertion is not supported for " + uri);
+ }
+ }
+
+ /**
+ * Insert a pet into the database with the given content values. Return the new content URI
+ * for that specific row in the database.
+ */
+ private Uri insertPet(Uri uri, ContentValues values) {
+ // Check that the name is not null
+ String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
+ if (name == null) {
+ throw new IllegalArgumentException("Pet requires a name");
+ }
+
+ // Check that the gender is valid
+ Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);
+ if (gender == null || !PetEntry.isValidGender(gender)) {
+ throw new IllegalArgumentException("Pet requires valid gender");
+ }
+
+ // If the weight is provided, check that it's greater than or equal to 0 kg
+ Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);
+ if (weight != null && weight < 0) {
+ throw new IllegalArgumentException("Pet requires valid weight");
+ }
+
+ // No need to check the breed, any value is valid (including null).
+
+ // Get writeable database
+ SQLiteDatabase database = mDbHelper.getWritableDatabase();
+
+ // Insert the new pet with the given values
+ long id = database.insert(PetEntry.TABLE_NAME, null, values);
+ // If the ID is -1, then the insertion failed. Log an error and return null.
+ if (id == -1) {
+ Log.e(LOG_TAG, "Failed to insert row for " + uri);
+ return null;
+ }
+
+ // Notify all listeners that the data has changed for the pet content URI
+ getContext().getContentResolver().notifyChange(uri, null);
+
+ // Return the new URI with the ID (of the newly inserted row) appended at the end
+ return ContentUris.withAppendedId(uri, id);
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues contentValues, String selection,
+ String[] selectionArgs) {
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case PETS:
+ return updatePet(uri, contentValues, selection, selectionArgs);
+ case PET_ID:
+ // For the PET_ID code, extract out the ID from the URI,
+ // so we know which row to update. Selection will be "_id=?" and selection
+ // arguments will be a String array containing the actual ID.
+ selection = PetEntry._ID + "=?";
+ selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
+ return updatePet(uri, contentValues, selection, selectionArgs);
+ default:
+ throw new IllegalArgumentException("Update is not supported for " + uri);
+ }
+ }
+
+ /**
+ * Update pets in the database with the given content values. Apply the changes to the rows
+ * specified in the selection and selection arguments (which could be 0 or 1 or more pets).
+ * Return the number of rows that were successfully updated.
+ */
+ private int updatePet(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ // If the {@link PetEntry#COLUMN_PET_NAME} key is present,
+ // check that the name value is not null.
+ if (values.containsKey(PetEntry.COLUMN_PET_NAME)) {
+ String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
+ if (name == null) {
+ throw new IllegalArgumentException("Pet requires a name");
+ }
+ }
+
+ // If the {@link PetEntry#COLUMN_PET_GENDER} key is present,
+ // check that the gender value is valid.
+ if (values.containsKey(PetEntry.COLUMN_PET_GENDER)) {
+ Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);
+ if (gender == null || !PetEntry.isValidGender(gender)) {
+ throw new IllegalArgumentException("Pet requires valid gender");
+ }
+ }
+
+ // If the {@link PetEntry#COLUMN_PET_WEIGHT} key is present,
+ // check that the weight value is valid.
+ if (values.containsKey(PetEntry.COLUMN_PET_WEIGHT)) {
+ // Check that the weight is greater than or equal to 0 kg
+ Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);
+ if (weight != null && weight < 0) {
+ throw new IllegalArgumentException("Pet requires valid weight");
+ }
+ }
+
+ // No need to check the breed, any value is valid (including null).
+
+ // If there are no values to update, then don't try to update the database
+ if (values.size() == 0) {
+ return 0;
+ }
+
+ // Otherwise, get writeable database to update the data
+ SQLiteDatabase database = mDbHelper.getWritableDatabase();
+
+ // Perform the update on the database and get the number of rows affected
+ int rowsUpdated = database.update(PetEntry.TABLE_NAME, values, selection, selectionArgs);
+
+ // If 1 or more rows were updated, then notify all listeners that the data at the
+ // given URI has changed
+ if (rowsUpdated != 0) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ }
+
+ // Return the number of rows updated
+ return rowsUpdated;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ // Get writeable database
+ SQLiteDatabase database = mDbHelper.getWritableDatabase();
+
+ // Track the number of rows that were deleted
+ int rowsDeleted;
+
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case PETS:
+ // Delete all rows that match the selection and selection args
+ rowsDeleted = database.delete(PetEntry.TABLE_NAME, selection, selectionArgs);
+ break;
+ case PET_ID:
+ // Delete a single row given by the ID in the URI
+ selection = PetEntry._ID + "=?";
+ selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
+ rowsDeleted = database.delete(PetEntry.TABLE_NAME, selection, selectionArgs);
+ break;
+ default:
+ throw new IllegalArgumentException("Deletion is not supported for " + uri);
+ }
+
+ // If 1 or more rows were deleted, then notify all listeners that the data at the
+ // given URI has changed
+ if (rowsDeleted != 0) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ }
+
+ // Return the number of rows deleted
+ return rowsDeleted;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case PETS:
+ return PetEntry.CONTENT_LIST_TYPE;
+ case PET_ID:
+ return PetEntry.CONTENT_ITEM_TYPE;
+ default:
+ throw new IllegalStateException("Unknown URI " + uri + " with match " + match);
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_catalog.xml b/app/src/main/res/layout/activity_catalog.xml
index 94f45e32..94b0a93d 100755
--- a/app/src/main/res/layout/activity_catalog.xml
+++ b/app/src/main/res/layout/activity_catalog.xml
@@ -18,11 +18,48 @@
android:layout_height="match_parent"
tools:context=".CatalogActivity">
-
+ android:layout_height="match_parent"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c086a8e5..b2540136 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -20,15 +20,60 @@
Delete All Pets
+
+ It\'s a bit lonely here...
+
+
+ Get started by adding a pet
+
Add a Pet
+
+ Edit Pet
+
Save
Delete
+
+ Pet saved
+
+
+ Error with saving pet
+
+
+ Pet updated
+
+
+ Error with updating pet
+
+
+ Discard your changes and quit editing?
+
+
+ Discard
+
+
+ Keep Editing
+
+
+ Pet deleted
+
+
+ Error with deleting pet
+
+
+ Delete this pet?
+
+
+ Delete
+
+
+ Cancel
+
Overview
@@ -58,4 +103,7 @@
Female
+
+
+ Unknown breed