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