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