1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.notepad;
18 
19 import com.example.android.notepad.NotePad;
20 
21 import android.content.ClipDescription;
22 import android.content.ContentProvider;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.UriMatcher;
27 import android.content.ContentProvider.PipeDataWriter;
28 import android.content.res.AssetFileDescriptor;
29 import android.content.res.Resources;
30 import android.database.Cursor;
31 import android.database.SQLException;
32 import android.database.sqlite.SQLiteDatabase;
33 import android.database.sqlite.SQLiteOpenHelper;
34 import android.database.sqlite.SQLiteQueryBuilder;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.ParcelFileDescriptor;
38 import android.provider.LiveFolders;
39 import android.text.TextUtils;
40 import android.util.Log;
41 
42 import java.io.FileNotFoundException;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.io.OutputStreamWriter;
46 import java.io.PrintWriter;
47 import java.io.UnsupportedEncodingException;
48 import java.util.HashMap;
49 
50 /**
51  * Provides access to a database of notes. Each note has a title, the note
52  * itself, a creation date and a modified data.
53  */
54 public class NotePadProvider extends ContentProvider implements PipeDataWriter<Cursor> {
55     // Used for debugging and logging
56     private static final String TAG = "NotePadProvider";
57 
58     /**
59      * The database that the provider uses as its underlying data store
60      */
61     private static final String DATABASE_NAME = "note_pad.db";
62 
63     /**
64      * The database version
65      */
66     private static final int DATABASE_VERSION = 2;
67 
68     /**
69      * A projection map used to select columns from the database
70      */
71     private static HashMap<String, String> sNotesProjectionMap;
72 
73     /**
74      * Standard projection for the interesting columns of a normal note.
75      */
76     private static final String[] READ_NOTE_PROJECTION = new String[] {
77             NotePad.Notes._ID,               // Projection position 0, the note's id
78             NotePad.Notes.COLUMN_NAME_NOTE,  // Projection position 1, the note's content
79             NotePad.Notes.COLUMN_NAME_TITLE, // Projection position 2, the note's title
80     };
81     private static final int READ_NOTE_NOTE_INDEX = 1;
82     private static final int READ_NOTE_TITLE_INDEX = 2;
83 
84     /*
85      * Constants used by the Uri matcher to choose an action based on the pattern
86      * of the incoming URI
87      */
88     // The incoming URI matches the Notes URI pattern
89     private static final int NOTES = 1;
90 
91     // The incoming URI matches the Note ID URI pattern
92     private static final int NOTE_ID = 2;
93 
94     /**
95      * A UriMatcher instance
96      */
97     private static final UriMatcher sUriMatcher;
98 
99     // Handle to a new DatabaseHelper.
100     private DatabaseHelper mOpenHelper;
101 
102 
103     /**
104      * A block that instantiates and sets static objects
105      */
106     static {
107 
108         /*
109          * Creates and initializes the URI matcher
110          */
111         // Create a new instance
112         sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
113 
114         // Add a pattern that routes URIs terminated with "notes" to a NOTES operation
sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES)115         sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
116 
117         // Add a pattern that routes URIs terminated with "notes" plus an integer
118         // to a note ID operation
sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID)119         sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);
120 
121         /*
122          * Creates and initializes a projection map that returns all columns
123          */
124 
125         // Creates a new projection map instance. The map returns a column name
126         // given a string. The two are usually equal.
127         sNotesProjectionMap = new HashMap<String, String>();
128 
129         // Maps the string "_ID" to the column name "_ID"
sNotesProjectionMap.put(NotePad.Notes._ID, NotePad.Notes._ID)130         sNotesProjectionMap.put(NotePad.Notes._ID, NotePad.Notes._ID);
131 
132         // Maps "title" to "title"
sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_TITLE, NotePad.Notes.COLUMN_NAME_TITLE)133         sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_TITLE, NotePad.Notes.COLUMN_NAME_TITLE);
134 
135         // Maps "note" to "note"
sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_NOTE, NotePad.Notes.COLUMN_NAME_NOTE)136         sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_NOTE, NotePad.Notes.COLUMN_NAME_NOTE);
137 
138         // Maps "created" to "created"
sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE, NotePad.Notes.COLUMN_NAME_CREATE_DATE)139         sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE,
140                 NotePad.Notes.COLUMN_NAME_CREATE_DATE);
141 
142         // Maps "modified" to "modified"
sNotesProjectionMap.put( NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE)143         sNotesProjectionMap.put(
144                 NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE,
145                 NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE);
146     }
147 
148     /**
149     *
150     * This class helps open, create, and upgrade the database file. Set to package visibility
151     * for testing purposes.
152     */
153    static class DatabaseHelper extends SQLiteOpenHelper {
154 
DatabaseHelper(Context context)155        DatabaseHelper(Context context) {
156 
157            // calls the super constructor, requesting the default cursor factory.
158            super(context, DATABASE_NAME, null, DATABASE_VERSION);
159        }
160 
161        /**
162         *
163         * Creates the underlying database with table name and column names taken from the
164         * NotePad class.
165         */
166        @Override
onCreate(SQLiteDatabase db)167        public void onCreate(SQLiteDatabase db) {
168            db.execSQL("CREATE TABLE " + NotePad.Notes.TABLE_NAME + " ("
169                    + NotePad.Notes._ID + " INTEGER PRIMARY KEY,"
170                    + NotePad.Notes.COLUMN_NAME_TITLE + " TEXT,"
171                    + NotePad.Notes.COLUMN_NAME_NOTE + " TEXT,"
172                    + NotePad.Notes.COLUMN_NAME_CREATE_DATE + " INTEGER,"
173                    + NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE + " INTEGER"
174                    + ");");
175        }
176 
177        /**
178         *
179         * Demonstrates that the provider must consider what happens when the
180         * underlying datastore is changed. In this sample, the database is upgraded the database
181         * by destroying the existing data.
182         * A real application should upgrade the database in place.
183         */
184        @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)185        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
186 
187            // Logs that the database is being upgraded
188            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
189                    + newVersion + ", which will destroy all old data");
190 
191            // Kills the table and existing data
192            db.execSQL("DROP TABLE IF EXISTS notes");
193 
194            // Recreates the database with a new version
195            onCreate(db);
196        }
197    }
198 
199    /**
200     *
201     * Initializes the provider by creating a new DatabaseHelper. onCreate() is called
202     * automatically when Android creates the provider in response to a resolver request from a
203     * client.
204     */
205    @Override
onCreate()206    public boolean onCreate() {
207 
208        // Creates a new helper object. Note that the database itself isn't opened until
209        // something tries to access it, and it's only created if it doesn't already exist.
210        mOpenHelper = new DatabaseHelper(getContext());
211 
212        // Assumes that any failures will be reported by a thrown exception.
213        return true;
214    }
215 
216    /**
217     * This method is called when a client calls
218     * {@link android.content.ContentResolver#query(Uri, String[], String, String[], String)}.
219     * Queries the database and returns a cursor containing the results.
220     *
221     * @return A cursor containing the results of the query. The cursor exists but is empty if
222     * the query returns no results or an exception occurs.
223     * @throws IllegalArgumentException if the incoming URI pattern is invalid.
224     */
225    @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)226    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
227            String sortOrder) {
228 
229        // Constructs a new query builder and sets its table name
230        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
231        qb.setTables(NotePad.Notes.TABLE_NAME);
232 
233        /**
234         * Choose the projection and adjust the "where" clause based on URI pattern-matching.
235         */
236        switch (sUriMatcher.match(uri)) {
237            // If the incoming URI is for notes, chooses the Notes projection
238            case NOTES:
239                qb.setProjectionMap(sNotesProjectionMap);
240                break;
241 
242            /* If the incoming URI is for a single note identified by its ID, chooses the
243             * note ID projection, and appends "_ID = <noteID>" to the where clause, so that
244             * it selects that single note
245             */
246            case NOTE_ID:
247                qb.setProjectionMap(sNotesProjectionMap);
248                qb.appendWhere(
249                    NotePad.Notes._ID +    // the name of the ID column
250                    "=" +
251                    // the position of the note ID itself in the incoming URI
252                    uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION));
253                break;
254 
255            default:
256                // If the URI doesn't match any of the known patterns, throw an exception.
257                throw new IllegalArgumentException("Unknown URI " + uri);
258        }
259 
260 
261        String orderBy;
262        // If no sort order is specified, uses the default
263        if (TextUtils.isEmpty(sortOrder)) {
264            orderBy = NotePad.Notes.DEFAULT_SORT_ORDER;
265        } else {
266            // otherwise, uses the incoming sort order
267            orderBy = sortOrder;
268        }
269 
270        // Opens the database object in "read" mode, since no writes need to be done.
271        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
272 
273        /*
274         * Performs the query. If no problems occur trying to read the database, then a Cursor
275         * object is returned; otherwise, the cursor variable contains null. If no records were
276         * selected, then the Cursor object is empty, and Cursor.getCount() returns 0.
277         */
278        Cursor c = qb.query(
279            db,            // The database to query
280            projection,    // The columns to return from the query
281            selection,     // The columns for the where clause
282            selectionArgs, // The values for the where clause
283            null,          // don't group the rows
284            null,          // don't filter by row groups
285            orderBy        // The sort order
286        );
287 
288        // Tells the Cursor what URI to watch, so it knows when its source data changes
289        c.setNotificationUri(getContext().getContentResolver(), uri);
290        return c;
291    }
292 
293    /**
294     * This is called when a client calls {@link android.content.ContentResolver#getType(Uri)}.
295     * Returns the MIME data type of the URI given as a parameter.
296     *
297     * @param uri The URI whose MIME type is desired.
298     * @return The MIME type of the URI.
299     * @throws IllegalArgumentException if the incoming URI pattern is invalid.
300     */
301    @Override
getType(Uri uri)302    public String getType(Uri uri) {
303 
304        /**
305         * Chooses the MIME type based on the incoming URI pattern
306         */
307        switch (sUriMatcher.match(uri)) {
308 
309            // If the pattern is for notes or live folders, returns the general content type.
310            case NOTES:
311                return NotePad.Notes.CONTENT_TYPE;
312 
313            // If the pattern is for note IDs, returns the note ID content type.
314            case NOTE_ID:
315                return NotePad.Notes.CONTENT_ITEM_TYPE;
316 
317            // If the URI pattern doesn't match any permitted patterns, throws an exception.
318            default:
319                throw new IllegalArgumentException("Unknown URI " + uri);
320        }
321     }
322 
323 //BEGIN_INCLUDE(stream)
324     /**
325      * This describes the MIME types that are supported for opening a note
326      * URI as a stream.
327      */
328     static ClipDescription NOTE_STREAM_TYPES = new ClipDescription(null,
329             new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN });
330 
331     /**
332      * Returns the types of available data streams.  URIs to specific notes are supported.
333      * The application can convert such a note to a plain text stream.
334      *
335      * @param uri the URI to analyze
336      * @param mimeTypeFilter The MIME type to check for. This method only returns a data stream
337      * type for MIME types that match the filter. Currently, only text/plain MIME types match.
338      * @return a data stream MIME type. Currently, only text/plan is returned.
339      * @throws IllegalArgumentException if the URI pattern doesn't match any supported patterns.
340      */
341     @Override
getStreamTypes(Uri uri, String mimeTypeFilter)342     public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
343         /**
344          *  Chooses the data stream type based on the incoming URI pattern.
345          */
346         switch (sUriMatcher.match(uri)) {
347 
348             // If the pattern is for notes or live folders, return null. Data streams are not
349             // supported for this type of URI.
350             case NOTES:
351                 return null;
352 
353             // If the pattern is for note IDs and the MIME filter is text/plain, then return
354             // text/plain
355             case NOTE_ID:
356                 return NOTE_STREAM_TYPES.filterMimeTypes(mimeTypeFilter);
357 
358                 // If the URI pattern doesn't match any permitted patterns, throws an exception.
359             default:
360                 throw new IllegalArgumentException("Unknown URI " + uri);
361             }
362     }
363 
364 
365     /**
366      * Returns a stream of data for each supported stream type. This method does a query on the
367      * incoming URI, then uses
368      * {@link android.content.ContentProvider#openPipeHelper(Uri, String, Bundle, Object,
369      * PipeDataWriter)} to start another thread in which to convert the data into a stream.
370      *
371      * @param uri The URI pattern that points to the data stream
372      * @param mimeTypeFilter A String containing a MIME type. This method tries to get a stream of
373      * data with this MIME type.
374      * @param opts Additional options supplied by the caller.  Can be interpreted as
375      * desired by the content provider.
376      * @return AssetFileDescriptor A handle to the file.
377      * @throws FileNotFoundException if there is no file associated with the incoming URI.
378      */
379     @Override
openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)380     public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
381             throws FileNotFoundException {
382 
383         // Checks to see if the MIME type filter matches a supported MIME type.
384         String[] mimeTypes = getStreamTypes(uri, mimeTypeFilter);
385 
386         // If the MIME type is supported
387         if (mimeTypes != null) {
388 
389             // Retrieves the note for this URI. Uses the query method defined for this provider,
390             // rather than using the database query method.
391             Cursor c = query(
392                     uri,                    // The URI of a note
393                     READ_NOTE_PROJECTION,   // Gets a projection containing the note's ID, title,
394                                             // and contents
395                     null,                   // No WHERE clause, get all matching records
396                     null,                   // Since there is no WHERE clause, no selection criteria
397                     null                    // Use the default sort order (modification date,
398                                             // descending
399             );
400 
401 
402             // If the query fails or the cursor is empty, stop
403             if (c == null || !c.moveToFirst()) {
404 
405                 // If the cursor is empty, simply close the cursor and return
406                 if (c != null) {
407                     c.close();
408                 }
409 
410                 // If the cursor is null, throw an exception
411                 throw new FileNotFoundException("Unable to query " + uri);
412             }
413 
414             // Start a new thread that pipes the stream data back to the caller.
415             return new AssetFileDescriptor(
416                     openPipeHelper(uri, mimeTypes[0], opts, c, this), 0,
417                     AssetFileDescriptor.UNKNOWN_LENGTH);
418         }
419 
420         // If the MIME type is not supported, return a read-only handle to the file.
421         return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
422     }
423 
424     /**
425      * Implementation of {@link android.content.ContentProvider.PipeDataWriter}
426      * to perform the actual work of converting the data in one of cursors to a
427      * stream of data for the client to read.
428      */
429     @Override
writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType, Bundle opts, Cursor c)430     public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType,
431             Bundle opts, Cursor c) {
432         // We currently only support conversion-to-text from a single note entry,
433         // so no need for cursor data type checking here.
434         FileOutputStream fout = new FileOutputStream(output.getFileDescriptor());
435         PrintWriter pw = null;
436         try {
437             pw = new PrintWriter(new OutputStreamWriter(fout, "UTF-8"));
438             pw.println(c.getString(READ_NOTE_TITLE_INDEX));
439             pw.println("");
440             pw.println(c.getString(READ_NOTE_NOTE_INDEX));
441         } catch (UnsupportedEncodingException e) {
442             Log.w(TAG, "Ooops", e);
443         } finally {
444             c.close();
445             if (pw != null) {
446                 pw.flush();
447             }
448             try {
449                 fout.close();
450             } catch (IOException e) {
451             }
452         }
453     }
454 //END_INCLUDE(stream)
455 
456     /**
457      * This is called when a client calls
458      * {@link android.content.ContentResolver#insert(Uri, ContentValues)}.
459      * Inserts a new row into the database. This method sets up default values for any
460      * columns that are not included in the incoming map.
461      * If rows were inserted, then listeners are notified of the change.
462      * @return The row ID of the inserted row.
463      * @throws SQLException if the insertion fails.
464      */
465     @Override
insert(Uri uri, ContentValues initialValues)466     public Uri insert(Uri uri, ContentValues initialValues) {
467 
468         // Validates the incoming URI. Only the full provider URI is allowed for inserts.
469         if (sUriMatcher.match(uri) != NOTES) {
470             throw new IllegalArgumentException("Unknown URI " + uri);
471         }
472 
473         // A map to hold the new record's values.
474         ContentValues values;
475 
476         // If the incoming values map is not null, uses it for the new values.
477         if (initialValues != null) {
478             values = new ContentValues(initialValues);
479 
480         } else {
481             // Otherwise, create a new value map
482             values = new ContentValues();
483         }
484 
485         // Gets the current system time in milliseconds
486         Long now = Long.valueOf(System.currentTimeMillis());
487 
488         // If the values map doesn't contain the creation date, sets the value to the current time.
489         if (values.containsKey(NotePad.Notes.COLUMN_NAME_CREATE_DATE) == false) {
490             values.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE, now);
491         }
492 
493         // If the values map doesn't contain the modification date, sets the value to the current
494         // time.
495         if (values.containsKey(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE) == false) {
496             values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, now);
497         }
498 
499         // If the values map doesn't contain a title, sets the value to the default title.
500         if (values.containsKey(NotePad.Notes.COLUMN_NAME_TITLE) == false) {
501             Resources r = Resources.getSystem();
502             values.put(NotePad.Notes.COLUMN_NAME_TITLE, r.getString(android.R.string.untitled));
503         }
504 
505         // If the values map doesn't contain note text, sets the value to an empty string.
506         if (values.containsKey(NotePad.Notes.COLUMN_NAME_NOTE) == false) {
507             values.put(NotePad.Notes.COLUMN_NAME_NOTE, "");
508         }
509 
510         // Opens the database object in "write" mode.
511         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
512 
513         // Performs the insert and returns the ID of the new note.
514         long rowId = db.insert(
515             NotePad.Notes.TABLE_NAME,        // The table to insert into.
516             NotePad.Notes.COLUMN_NAME_NOTE,  // A hack, SQLite sets this column value to null
517                                              // if values is empty.
518             values                           // A map of column names, and the values to insert
519                                              // into the columns.
520         );
521 
522         // If the insert succeeded, the row ID exists.
523         if (rowId > 0) {
524             // Creates a URI with the note ID pattern and the new row ID appended to it.
525             Uri noteUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_ID_URI_BASE, rowId);
526 
527             // Notifies observers registered against this provider that the data changed.
528             getContext().getContentResolver().notifyChange(noteUri, null);
529             return noteUri;
530         }
531 
532         // If the insert didn't succeed, then the rowID is <= 0. Throws an exception.
533         throw new SQLException("Failed to insert row into " + uri);
534     }
535 
536     /**
537      * This is called when a client calls
538      * {@link android.content.ContentResolver#delete(Uri, String, String[])}.
539      * Deletes records from the database. If the incoming URI matches the note ID URI pattern,
540      * this method deletes the one record specified by the ID in the URI. Otherwise, it deletes a
541      * a set of records. The record or records must also match the input selection criteria
542      * specified by where and whereArgs.
543      *
544      * If rows were deleted, then listeners are notified of the change.
545      * @return If a "where" clause is used, the number of rows affected is returned, otherwise
546      * 0 is returned. To delete all rows and get a row count, use "1" as the where clause.
547      * @throws IllegalArgumentException if the incoming URI pattern is invalid.
548      */
549     @Override
delete(Uri uri, String where, String[] whereArgs)550     public int delete(Uri uri, String where, String[] whereArgs) {
551 
552         // Opens the database object in "write" mode.
553         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
554         String finalWhere;
555 
556         int count;
557 
558         // Does the delete based on the incoming URI pattern.
559         switch (sUriMatcher.match(uri)) {
560 
561             // If the incoming pattern matches the general pattern for notes, does a delete
562             // based on the incoming "where" columns and arguments.
563             case NOTES:
564                 count = db.delete(
565                     NotePad.Notes.TABLE_NAME,  // The database table name
566                     where,                     // The incoming where clause column names
567                     whereArgs                  // The incoming where clause values
568                 );
569                 break;
570 
571                 // If the incoming URI matches a single note ID, does the delete based on the
572                 // incoming data, but modifies the where clause to restrict it to the
573                 // particular note ID.
574             case NOTE_ID:
575                 /*
576                  * Starts a final WHERE clause by restricting it to the
577                  * desired note ID.
578                  */
579                 finalWhere =
580                         NotePad.Notes._ID +                              // The ID column name
581                         " = " +                                          // test for equality
582                         uri.getPathSegments().                           // the incoming note ID
583                             get(NotePad.Notes.NOTE_ID_PATH_POSITION)
584                 ;
585 
586                 // If there were additional selection criteria, append them to the final
587                 // WHERE clause
588                 if (where != null) {
589                     finalWhere = finalWhere + " AND " + where;
590                 }
591 
592                 // Performs the delete.
593                 count = db.delete(
594                     NotePad.Notes.TABLE_NAME,  // The database table name.
595                     finalWhere,                // The final WHERE clause
596                     whereArgs                  // The incoming where clause values.
597                 );
598                 break;
599 
600             // If the incoming pattern is invalid, throws an exception.
601             default:
602                 throw new IllegalArgumentException("Unknown URI " + uri);
603         }
604 
605         /* Gets a handle to the content resolver object for the current context, and notifies it
606          * that the incoming URI changed. The object passes this along to the resolver framework,
607          * and observers that have registered themselves for the provider are notified.
608          */
609         getContext().getContentResolver().notifyChange(uri, null);
610 
611         // Returns the number of rows deleted.
612         return count;
613     }
614 
615     /**
616      * This is called when a client calls
617      * {@link android.content.ContentResolver#update(Uri,ContentValues,String,String[])}
618      * Updates records in the database. The column names specified by the keys in the values map
619      * are updated with new data specified by the values in the map. If the incoming URI matches the
620      * note ID URI pattern, then the method updates the one record specified by the ID in the URI;
621      * otherwise, it updates a set of records. The record or records must match the input
622      * selection criteria specified by where and whereArgs.
623      * If rows were updated, then listeners are notified of the change.
624      *
625      * @param uri The URI pattern to match and update.
626      * @param values A map of column names (keys) and new values (values).
627      * @param where An SQL "WHERE" clause that selects records based on their column values. If this
628      * is null, then all records that match the URI pattern are selected.
629      * @param whereArgs An array of selection criteria. If the "where" param contains value
630      * placeholders ("?"), then each placeholder is replaced by the corresponding element in the
631      * array.
632      * @return The number of rows updated.
633      * @throws IllegalArgumentException if the incoming URI pattern is invalid.
634      */
635     @Override
update(Uri uri, ContentValues values, String where, String[] whereArgs)636     public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
637 
638         // Opens the database object in "write" mode.
639         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
640         int count;
641         String finalWhere;
642 
643         // Does the update based on the incoming URI pattern
644         switch (sUriMatcher.match(uri)) {
645 
646             // If the incoming URI matches the general notes pattern, does the update based on
647             // the incoming data.
648             case NOTES:
649 
650                 // Does the update and returns the number of rows updated.
651                 count = db.update(
652                     NotePad.Notes.TABLE_NAME, // The database table name.
653                     values,                   // A map of column names and new values to use.
654                     where,                    // The where clause column names.
655                     whereArgs                 // The where clause column values to select on.
656                 );
657                 break;
658 
659             // If the incoming URI matches a single note ID, does the update based on the incoming
660             // data, but modifies the where clause to restrict it to the particular note ID.
661             case NOTE_ID:
662                 // From the incoming URI, get the note ID
663                 String noteId = uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION);
664 
665                 /*
666                  * Starts creating the final WHERE clause by restricting it to the incoming
667                  * note ID.
668                  */
669                 finalWhere =
670                         NotePad.Notes._ID +                              // The ID column name
671                         " = " +                                          // test for equality
672                         uri.getPathSegments().                           // the incoming note ID
673                             get(NotePad.Notes.NOTE_ID_PATH_POSITION)
674                 ;
675 
676                 // If there were additional selection criteria, append them to the final WHERE
677                 // clause
678                 if (where !=null) {
679                     finalWhere = finalWhere + " AND " + where;
680                 }
681 
682 
683                 // Does the update and returns the number of rows updated.
684                 count = db.update(
685                     NotePad.Notes.TABLE_NAME, // The database table name.
686                     values,                   // A map of column names and new values to use.
687                     finalWhere,               // The final WHERE clause to use
688                                               // placeholders for whereArgs
689                     whereArgs                 // The where clause column values to select on, or
690                                               // null if the values are in the where argument.
691                 );
692                 break;
693             // If the incoming pattern is invalid, throws an exception.
694             default:
695                 throw new IllegalArgumentException("Unknown URI " + uri);
696         }
697 
698         /* Gets a handle to the content resolver object for the current context, and notifies it
699          * that the incoming URI changed. The object passes this along to the resolver framework,
700          * and observers that have registered themselves for the provider are notified.
701          */
702         getContext().getContentResolver().notifyChange(uri, null);
703 
704         // Returns the number of rows updated.
705         return count;
706     }
707 
708     /**
709      * A test package can call this to get a handle to the database underlying NotePadProvider,
710      * so it can insert test data into the database. The test case class is responsible for
711      * instantiating the provider in a test context; {@link android.test.ProviderTestCase2} does
712      * this during the call to setUp()
713      *
714      * @return a handle to the database helper object for the provider's data.
715      */
getOpenHelperForTest()716     DatabaseHelper getOpenHelperForTest() {
717         return mOpenHelper;
718     }
719 }
720