diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7b6a6914..5ba2a60a 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,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..b34e1828 100755 --- a/app/src/main/java/com/example/android/pets/CatalogActivity.java +++ b/app/src/main/java/com/example/android/pets/CatalogActivity.java @@ -18,7 +18,7 @@ import android.content.ContentValues; import android.content.Intent; 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; @@ -28,16 +28,12 @@ import android.widget.TextView; 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 { - /** Database helper that will provide us access to the database */ - private PetDbHelper mDbHelper; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -52,10 +48,6 @@ public void onClick(View view) { startActivity(intent); } }); - - // To access our database, we instantiate our subclass of SQLiteOpenHelper - // and pass the context, which is the current activity. - mDbHelper = new PetDbHelper(this); } @Override @@ -69,9 +61,6 @@ protected void onStart() { * the pets database. */ private void displayDatabaseInfo() { - // Create and/or open a database to read from it - SQLiteDatabase db = mDbHelper.getReadableDatabase(); - // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { @@ -81,15 +70,14 @@ private void displayDatabaseInfo() { 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 + // Perform a query on the provider using the ContentResolver. + // Use the {@link PetEntry#CONTENT_URI} to access the pet data. + Cursor cursor = getContentResolver().query( + PetEntry.CONTENT_URI, // The content URI of the words table + projection, // The columns to return for each row + null, // Selection criteria + null, // Selection criteria + null); // The sort order for the returned rows TextView displayView = (TextView) findViewById(R.id.text_view_pet); @@ -142,9 +130,6 @@ private void displayDatabaseInfo() { * 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 +138,11 @@ 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); } @Override 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..0127757b 100755 --- a/app/src/main/java/com/example/android/pets/EditorActivity.java +++ b/app/src/main/java/com/example/android/pets/EditorActivity.java @@ -16,7 +16,7 @@ package com.example.android.pets; import android.content.ContentValues; -import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; import android.os.Bundle; import android.support.v4.app.NavUtils; import android.support.v7.app.AppCompatActivity; @@ -31,7 +31,6 @@ 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. @@ -121,12 +120,6 @@ private void insertPet() { 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(); - // Create a ContentValues object where column names are the keys, // and pet attributes from the editor are the values. ContentValues values = new ContentValues(); @@ -135,16 +128,18 @@ private void insertPet() { values.put(PetEntry.COLUMN_PET_GENDER, mGender); 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); + // 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(); + 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 with the row ID. - Toast.makeText(this, "Pet saved with row id: " + newRowId, Toast.LENGTH_SHORT).show(); + // 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(); } } 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..376d2826 --- /dev/null +++ b/app/src/main/java/com/example/android/pets/data/PetProvider.java @@ -0,0 +1,272 @@ +/* + * 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); + } + 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; + } + + // 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(); + + // Returns the number of database rows affected by the update statement + return database.update(PetEntry.TABLE_NAME, values, selection, selectionArgs); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + // Get writeable database + SQLiteDatabase database = mDbHelper.getWritableDatabase(); + + final int match = sUriMatcher.match(uri); + switch (match) { + case PETS: + // Delete all rows that match the selection and selection args + return database.delete(PetEntry.TABLE_NAME, selection, selectionArgs); + 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)) }; + return database.delete(PetEntry.TABLE_NAME, selection, selectionArgs); + default: + throw new IllegalArgumentException("Deletion is not supported for " + uri); + } + } + + @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/values/strings.xml b/app/src/main/res/values/strings.xml index c086a8e5..47e68684 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,12 @@ Delete + + Pet saved + + + Error with saving pet + Overview