/* * Copyright (C) 2007 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.notepad; import com.example.android.notepad.NotePad; import android.content.ClipDescription; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.content.ContentProvider.PipeDataWriter; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.provider.LiveFolders; import android.text.TextUtils; import android.util.Log; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.util.HashMap; /** * Provides access to a database of notes. Each note has a title, the note * itself, a creation date and a modified data. */ public class NotePadProvider extends ContentProvider implements PipeDataWriter<Cursor> { // Used for debugging and logging private static final String TAG = "NotePadProvider"; /** * The database that the provider uses as its underlying data store */ private static final String DATABASE_NAME = "note_pad.db"; /** * The database version */ private static final int DATABASE_VERSION = 2; /** * A projection map used to select columns from the database */ private static HashMap<String, String> sNotesProjectionMap; /** * A projection map used to select columns from the database */ private static HashMap<String, String> sLiveFolderProjectionMap; /** * Standard projection for the interesting columns of a normal note. */ private static final String[] READ_NOTE_PROJECTION = new String[] { NotePad.Notes._ID, // Projection position 0, the note's id NotePad.Notes.COLUMN_NAME_NOTE, // Projection position 1, the note's content NotePad.Notes.COLUMN_NAME_TITLE, // Projection position 2, the note's title }; private static final int READ_NOTE_NOTE_INDEX = 1; private static final int READ_NOTE_TITLE_INDEX = 2; /* * Constants used by the Uri matcher to choose an action based on the pattern * of the incoming URI */ // The incoming URI matches the Notes URI pattern private static final int NOTES = 1; // The incoming URI matches the Note ID URI pattern private static final int NOTE_ID = 2; // The incoming URI matches the Live Folder URI pattern private static final int LIVE_FOLDER_NOTES = 3; /** * A UriMatcher instance */ private static final UriMatcher sUriMatcher; // Handle to a new DatabaseHelper. private DatabaseHelper mOpenHelper; /** * A block that instantiates and sets static objects */ static { /* * Creates and initializes the URI matcher */ // Create a new instance sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); // Add a pattern that routes URIs terminated with "notes" to a NOTES operation sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES); // Add a pattern that routes URIs terminated with "notes" plus an integer // to a note ID operation sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID); // Add a pattern that routes URIs terminated with live_folders/notes to a // live folder operation sUriMatcher.addURI(NotePad.AUTHORITY, "live_folders/notes", LIVE_FOLDER_NOTES); /* * Creates and initializes a projection map that returns all columns */ // Creates a new projection map instance. The map returns a column name // given a string. The two are usually equal. sNotesProjectionMap = new HashMap<String, String>(); // Maps the string "_ID" to the column name "_ID" sNotesProjectionMap.put(NotePad.Notes._ID, NotePad.Notes._ID); // Maps "title" to "title" sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_TITLE, NotePad.Notes.COLUMN_NAME_TITLE); // Maps "note" to "note" sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_NOTE, NotePad.Notes.COLUMN_NAME_NOTE); // Maps "created" to "created" sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE, NotePad.Notes.COLUMN_NAME_CREATE_DATE); // Maps "modified" to "modified" sNotesProjectionMap.put( NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE); /* * Creates an initializes a projection map for handling Live Folders */ // Creates a new projection map instance sLiveFolderProjectionMap = new HashMap<String, String>(); // Maps "_ID" to "_ID AS _ID" for a live folder sLiveFolderProjectionMap.put(LiveFolders._ID, NotePad.Notes._ID + " AS " + LiveFolders._ID); // Maps "NAME" to "title AS NAME" sLiveFolderProjectionMap.put(LiveFolders.NAME, NotePad.Notes.COLUMN_NAME_TITLE + " AS " + LiveFolders.NAME); } /** * * This class helps open, create, and upgrade the database file. Set to package visibility * for testing purposes. */ static class DatabaseHelper extends SQLiteOpenHelper { DatabaseHelper(Context context) { // calls the super constructor, requesting the default cursor factory. super(context, DATABASE_NAME, null, DATABASE_VERSION); } /** * * Creates the underlying database with table name and column names taken from the * NotePad class. */ @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + NotePad.Notes.TABLE_NAME + " (" + NotePad.Notes._ID + " INTEGER PRIMARY KEY," + NotePad.Notes.COLUMN_NAME_TITLE + " TEXT," + NotePad.Notes.COLUMN_NAME_NOTE + " TEXT," + NotePad.Notes.COLUMN_NAME_CREATE_DATE + " INTEGER," + NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE + " INTEGER" + ");"); } /** * * Demonstrates that the provider must consider what happens when the * underlying datastore is changed. In this sample, the database is upgraded the database * by destroying the existing data. * A real application should upgrade the database in place. */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // Logs that the database is being upgraded Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); // Kills the table and existing data db.execSQL("DROP TABLE IF EXISTS notes"); // Recreates the database with a new version onCreate(db); } } /** * * Initializes the provider by creating a new DatabaseHelper. onCreate() is called * automatically when Android creates the provider in response to a resolver request from a * client. */ @Override public boolean onCreate() { // Creates a new helper object. Note that the database itself isn't opened until // something tries to access it, and it's only created if it doesn't already exist. mOpenHelper = new DatabaseHelper(getContext()); // Assumes that any failures will be reported by a thrown exception. return true; } /** * This method is called when a client calls * {@link android.content.ContentResolver#query(Uri, String[], String, String[], String)}. * Queries the database and returns a cursor containing the results. * * @return A cursor containing the results of the query. The cursor exists but is empty if * the query returns no results or an exception occurs. * @throws IllegalArgumentException if the incoming URI pattern is invalid. */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // Constructs a new query builder and sets its table name SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(NotePad.Notes.TABLE_NAME); /** * Choose the projection and adjust the "where" clause based on URI pattern-matching. */ switch (sUriMatcher.match(uri)) { // If the incoming URI is for notes, chooses the Notes projection case NOTES: qb.setProjectionMap(sNotesProjectionMap); break; /* If the incoming URI is for a single note identified by its ID, chooses the * note ID projection, and appends "_ID = <noteID>" to the where clause, so that * it selects that single note */ case NOTE_ID: qb.setProjectionMap(sNotesProjectionMap); qb.appendWhere( NotePad.Notes._ID + // the name of the ID column "=" + // the position of the note ID itself in the incoming URI uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION)); break; case LIVE_FOLDER_NOTES: // If the incoming URI is from a live folder, chooses the live folder projection. qb.setProjectionMap(sLiveFolderProjectionMap); break; default: // If the URI doesn't match any of the known patterns, throw an exception. throw new IllegalArgumentException("Unknown URI " + uri); } String orderBy; // If no sort order is specified, uses the default if (TextUtils.isEmpty(sortOrder)) { orderBy = NotePad.Notes.DEFAULT_SORT_ORDER; } else { // otherwise, uses the incoming sort order orderBy = sortOrder; } // Opens the database object in "read" mode, since no writes need to be done. SQLiteDatabase db = mOpenHelper.getReadableDatabase(); /* * Performs the query. If no problems occur trying to read the database, then a Cursor * object is returned; otherwise, the cursor variable contains null. If no records were * selected, then the Cursor object is empty, and Cursor.getCount() returns 0. */ Cursor c = qb.query( db, // The database to query projection, // The columns to return from the query selection, // The columns for the where clause selectionArgs, // The values for the where clause null, // don't group the rows null, // don't filter by row groups orderBy // The sort order ); // Tells the Cursor what URI to watch, so it knows when its source data changes c.setNotificationUri(getContext().getContentResolver(), uri); return c; } /** * This is called when a client calls {@link android.content.ContentResolver#getType(Uri)}. * Returns the MIME data type of the URI given as a parameter. * * @param uri The URI whose MIME type is desired. * @return The MIME type of the URI. * @throws IllegalArgumentException if the incoming URI pattern is invalid. */ @Override public String getType(Uri uri) { /** * Chooses the MIME type based on the incoming URI pattern */ switch (sUriMatcher.match(uri)) { // If the pattern is for notes or live folders, returns the general content type. case NOTES: case LIVE_FOLDER_NOTES: return NotePad.Notes.CONTENT_TYPE; // If the pattern is for note IDs, returns the note ID content type. case NOTE_ID: return NotePad.Notes.CONTENT_ITEM_TYPE; // If the URI pattern doesn't match any permitted patterns, throws an exception. default: throw new IllegalArgumentException("Unknown URI " + uri); } } /** * This describes the MIME types that are supported for opening a note * URI as a stream. */ static ClipDescription NOTE_STREAM_TYPES = new ClipDescription(null, new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN }); /** * Returns the types of available data streams. URIs to specific notes are supported. * The application can convert such a note to a plain text stream. * * @param uri the URI to analyze * @param mimeTypeFilter The MIME type to check for. This method only returns a data stream * type for MIME types that match the filter. Currently, only text/plain MIME types match. * @return a data stream MIME type. Currently, only text/plan is returned. * @throws IllegalArgumentException if the URI pattern doesn't match any supported patterns. */ @Override public String[] getStreamTypes(Uri uri, String mimeTypeFilter) { /** * Chooses the data stream type based on the incoming URI pattern. */ switch (sUriMatcher.match(uri)) { // If the pattern is for notes or live folders, return null. Data streams are not // supported for this type of URI. case NOTES: case LIVE_FOLDER_NOTES: return null; // If the pattern is for note IDs and the MIME filter is text/plain, then return // text/plain case NOTE_ID: return NOTE_STREAM_TYPES.filterMimeTypes(mimeTypeFilter); // If the URI pattern doesn't match any permitted patterns, throws an exception. default: throw new IllegalArgumentException("Unknown URI " + uri); } } /** * Returns a stream of data for each supported stream type. This method does a query on the * incoming URI, then uses * {@link android.content.ContentProvider#openPipeHelper(Uri, String, Bundle, Object, * PipeDataWriter)} to start another thread in which to convert the data into a stream. * * @param uri The URI pattern that points to the data stream * @param mimeTypeFilter A String containing a MIME type. This method tries to get a stream of * data with this MIME type. * @param opts Additional options supplied by the caller. Can be interpreted as * desired by the content provider. * @return AssetFileDescriptor A handle to the file. * @throws FileNotFoundException if there is no file associated with the incoming URI. */ @Override public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) throws FileNotFoundException { // Checks to see if the MIME type filter matches a supported MIME type. String[] mimeTypes = getStreamTypes(uri, mimeTypeFilter); // If the MIME type is supported if (mimeTypes != null) { // Retrieves the note for this URI. Uses the query method defined for this provider, // rather than using the database query method. Cursor c = query( uri, // The URI of a note READ_NOTE_PROJECTION, // Gets a projection containing the note's ID, title, // and contents null, // No WHERE clause, get all matching records null, // Since there is no WHERE clause, no selection criteria null // Use the default sort order (modification date, // descending ); // If the query fails or the cursor is empty, stop if (c == null || !c.moveToFirst()) { // If the cursor is empty, simply close the cursor and return if (c != null) { c.close(); } // If the cursor is null, throw an exception throw new FileNotFoundException("Unable to query " + uri); } // Start a new thread that pipes the stream data back to the caller. return new AssetFileDescriptor( openPipeHelper(uri, mimeTypes[0], opts, c, this), 0, AssetFileDescriptor.UNKNOWN_LENGTH); } // If the MIME type is not supported, return a read-only handle to the file. return super.openTypedAssetFile(uri, mimeTypeFilter, opts); } /** * Implementation of {@link android.content.ContentProvider.PipeDataWriter} * to perform the actual work of converting the data in one of cursors to a * stream of data for the client to read. */ @Override public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType, Bundle opts, Cursor c) { // We currently only support conversion-to-text from a single note entry, // so no need for cursor data type checking here. FileOutputStream fout = new FileOutputStream(output.getFileDescriptor()); PrintWriter pw = null; try { pw = new PrintWriter(new OutputStreamWriter(fout, "UTF-8")); pw.println(c.getString(READ_NOTE_TITLE_INDEX)); pw.println(""); pw.println(c.getString(READ_NOTE_NOTE_INDEX)); } catch (UnsupportedEncodingException e) { Log.w(TAG, "Ooops", e); } finally { c.close(); if (pw != null) { pw.flush(); } try { fout.close(); } catch (IOException e) { } } } /** * This is called when a client calls * {@link android.content.ContentResolver#insert(Uri, ContentValues)}. * Inserts a new row into the database. This method sets up default values for any * columns that are not included in the incoming map. * If rows were inserted, then listeners are notified of the change. * @return The row ID of the inserted row. * @throws SQLException if the insertion fails. */ @Override public Uri insert(Uri uri, ContentValues initialValues) { // Validates the incoming URI. Only the full provider URI is allowed for inserts. if (sUriMatcher.match(uri) != NOTES) { throw new IllegalArgumentException("Unknown URI " + uri); } // A map to hold the new record's values. ContentValues values; // If the incoming values map is not null, uses it for the new values. if (initialValues != null) { values = new ContentValues(initialValues); } else { // Otherwise, create a new value map values = new ContentValues(); } // Gets the current system time in milliseconds Long now = Long.valueOf(System.currentTimeMillis()); // If the values map doesn't contain the creation date, sets the value to the current time. if (values.containsKey(NotePad.Notes.COLUMN_NAME_CREATE_DATE) == false) { values.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE, now); } // If the values map doesn't contain the modification date, sets the value to the current // time. if (values.containsKey(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE) == false) { values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, now); } // If the values map doesn't contain a title, sets the value to the default title. if (values.containsKey(NotePad.Notes.COLUMN_NAME_TITLE) == false) { Resources r = Resources.getSystem(); values.put(NotePad.Notes.COLUMN_NAME_TITLE, r.getString(android.R.string.untitled)); } // If the values map doesn't contain note text, sets the value to an empty string. if (values.containsKey(NotePad.Notes.COLUMN_NAME_NOTE) == false) { values.put(NotePad.Notes.COLUMN_NAME_NOTE, ""); } // Opens the database object in "write" mode. SQLiteDatabase db = mOpenHelper.getWritableDatabase(); // Performs the insert and returns the ID of the new note. long rowId = db.insert( NotePad.Notes.TABLE_NAME, // The table to insert into. NotePad.Notes.COLUMN_NAME_NOTE, // A hack, SQLite sets this column value to null // if values is empty. values // A map of column names, and the values to insert // into the columns. ); // If the insert succeeded, the row ID exists. if (rowId > 0) { // Creates a URI with the note ID pattern and the new row ID appended to it. Uri noteUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_ID_URI_BASE, rowId); // Notifies observers registered against this provider that the data changed. getContext().getContentResolver().notifyChange(noteUri, null); return noteUri; } // If the insert didn't succeed, then the rowID is <= 0. Throws an exception. throw new SQLException("Failed to insert row into " + uri); } /** * This is called when a client calls * {@link android.content.ContentResolver#delete(Uri, String, String[])}. * Deletes records from the database. If the incoming URI matches the note ID URI pattern, * this method deletes the one record specified by the ID in the URI. Otherwise, it deletes a * a set of records. The record or records must also match the input selection criteria * specified by where and whereArgs. * * If rows were deleted, then listeners are notified of the change. * @return If a "where" clause is used, the number of rows affected is returned, otherwise * 0 is returned. To delete all rows and get a row count, use "1" as the where clause. * @throws IllegalArgumentException if the incoming URI pattern is invalid. */ @Override public int delete(Uri uri, String where, String[] whereArgs) { // Opens the database object in "write" mode. SQLiteDatabase db = mOpenHelper.getWritableDatabase(); String finalWhere; int count; // Does the delete based on the incoming URI pattern. switch (sUriMatcher.match(uri)) { // If the incoming pattern matches the general pattern for notes, does a delete // based on the incoming "where" columns and arguments. case NOTES: count = db.delete( NotePad.Notes.TABLE_NAME, // The database table name where, // The incoming where clause column names whereArgs // The incoming where clause values ); break; // If the incoming URI matches a single note ID, does the delete based on the // incoming data, but modifies the where clause to restrict it to the // particular note ID. case NOTE_ID: /* * Starts a final WHERE clause by restricting it to the * desired note ID. */ finalWhere = NotePad.Notes._ID + // The ID column name " = " + // test for equality uri.getPathSegments(). // the incoming note ID get(NotePad.Notes.NOTE_ID_PATH_POSITION) ; // If there were additional selection criteria, append them to the final // WHERE clause if (where != null) { finalWhere = finalWhere + " AND " + where; } // Performs the delete. count = db.delete( NotePad.Notes.TABLE_NAME, // The database table name. finalWhere, // The final WHERE clause whereArgs // The incoming where clause values. ); break; // If the incoming pattern is invalid, throws an exception. default: throw new IllegalArgumentException("Unknown URI " + uri); } /*Gets a handle to the content resolver object for the current context, and notifies it * that the incoming URI changed. The object passes this along to the resolver framework, * and observers that have registered themselves for the provider are notified. */ getContext().getContentResolver().notifyChange(uri, null); // Returns the number of rows deleted. return count; } /** * This is called when a client calls * {@link android.content.ContentResolver#update(Uri,ContentValues,String,String[])} * Updates records in the database. The column names specified by the keys in the values map * are updated with new data specified by the values in the map. If the incoming URI matches the * note ID URI pattern, then the method updates the one record specified by the ID in the URI; * otherwise, it updates a set of records. The record or records must match the input * selection criteria specified by where and whereArgs. * If rows were updated, then listeners are notified of the change. * * @param uri The URI pattern to match and update. * @param values A map of column names (keys) and new values (values). * @param where An SQL "WHERE" clause that selects records based on their column values. If this * is null, then all records that match the URI pattern are selected. * @param whereArgs An array of selection criteria. If the "where" param contains value * placeholders ("?"), then each placeholder is replaced by the corresponding element in the * array. * @return The number of rows updated. * @throws IllegalArgumentException if the incoming URI pattern is invalid. */ @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { // Opens the database object in "write" mode. SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; String finalWhere; // Does the update based on the incoming URI pattern switch (sUriMatcher.match(uri)) { // If the incoming URI matches the general notes pattern, does the update based on // the incoming data. case NOTES: // Does the update and returns the number of rows updated. count = db.update( NotePad.Notes.TABLE_NAME, // The database table name. values, // A map of column names and new values to use. where, // The where clause column names. whereArgs // The where clause column values to select on. ); break; // If the incoming URI matches a single note ID, does the update based on the incoming // data, but modifies the where clause to restrict it to the particular note ID. case NOTE_ID: // From the incoming URI, get the note ID String noteId = uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION); /* * Starts creating the final WHERE clause by restricting it to the incoming * note ID. */ finalWhere = NotePad.Notes._ID + // The ID column name " = " + // test for equality uri.getPathSegments(). // the incoming note ID get(NotePad.Notes.NOTE_ID_PATH_POSITION) ; // If there were additional selection criteria, append them to the final WHERE // clause if (where !=null) { finalWhere = finalWhere + " AND " + where; } // Does the update and returns the number of rows updated. count = db.update( NotePad.Notes.TABLE_NAME, // The database table name. values, // A map of column names and new values to use. finalWhere, // The final WHERE clause to use // placeholders for whereArgs whereArgs // The where clause column values to select on, or // null if the values are in the where argument. ); break; // If the incoming pattern is invalid, throws an exception. default: throw new IllegalArgumentException("Unknown URI " + uri); } /*Gets a handle to the content resolver object for the current context, and notifies it * that the incoming URI changed. The object passes this along to the resolver framework, * and observers that have registered themselves for the provider are notified. */ getContext().getContentResolver().notifyChange(uri, null); // Returns the number of rows updated. return count; } /** * A test package can call this to get a handle to the database underlying NotePadProvider, * so it can insert test data into the database. The test case class is responsible for * instantiating the provider in a test context; {@link android.test.ProviderTestCase2} does * this during the call to setUp() * * @return a handle to the database helper object for the provider's data. */ DatabaseHelper getOpenHelperForTest() { return mOpenHelper; } }