package com.oreilly.demo.android.pa.finchvideo.provider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.provider.BaseColumns; import android.text.TextUtils; import com.finchframework.finch.rest.FileHandlerFactory; import com.finchframework.finch.rest.RESTfulContentProvider; import com.finchframework.finch.rest.ResponseHandler; import java.io.File; import java.io.FileNotFoundException; /** * Content provider that loads video video data from the google YouTube API. The * following uri documents the RESTful gdata video web service: * * http://gdata.video.com/ */ public class FinchVideoContentProvider extends RESTfulContentProvider { public static final String VIDEO = "video"; public static final String DATABASE_NAME = VIDEO + ".db"; static int DATABASE_VERSION = 2; public static final String VIDEOS_TABLE_NAME = "video"; private static final String FINCH_VIDEO_FILE_CACHE = "finch_video_file_cache"; private static final int VIDEOS = 1; private static final int VIDEO_ID = 2; private static final int THUMB_VIDEO_ID = 3; private static final int THUMB_ID = 4; private static UriMatcher sUriMatcher; // Statically construct a uri matcher that can detect URIs referencing // more than 1 video, a single video, or a single thumb nail image. static { sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); sUriMatcher.addURI(FinchVideo.AUTHORITY, FinchVideo.Videos.VIDEO, VIDEOS); // use of the hash character indicates matching of an id sUriMatcher.addURI(FinchVideo.AUTHORITY, FinchVideo.Videos.VIDEO + "/#", VIDEO_ID); sUriMatcher.addURI(FinchVideo.AUTHORITY, FinchVideo.Videos.THUMB + "/#", THUMB_VIDEO_ID); sUriMatcher.addURI(FinchVideo.AUTHORITY, FinchVideo.Videos.THUMB + "/*", THUMB_ID); } /** uri for querying video, expects appending keywords. */ private static final String QUERY_URI = "http://gdata.youtube.com/feeds/api/videos?" + "max-results=15&format=1&q="; private DatabaseHelper mOpenHelper; private SQLiteDatabase mDb; private static class DatabaseHelper extends SQLiteOpenHelper { private DatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) { super(context, name, factory, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { createTable(sqLiteDatabase); } private void createTable(SQLiteDatabase sqLiteDatabase) { String createvideoTable = "CREATE TABLE " + VIDEOS_TABLE_NAME + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + FinchVideo.Videos.TITLE + " TEXT, " + FinchVideo.Videos.DESCRIPTION + " TEXT, " + FinchVideo.Videos.THUMB_URI_NAME + " TEXT," + FinchVideo.Videos.THUMB_WIDTH_NAME + " TEXT," + FinchVideo.Videos.THUMB_HEIGHT_NAME + " TEXT," + FinchVideo.Videos.TIMESTAMP + " TEXT, " + FinchVideo.Videos.QUERY_TEXT_NAME + " TEXT, " + FinchVideo.Videos.MEDIA_ID_NAME + " TEXT UNIQUE," + FinchVideo.Videos.THUMB_CONTENT_URI_NAME + " TEXT UNIQUE," + FinchVideo.Videos._DATA + " TEXT UNIQUE" + ");"; sqLiteDatabase.execSQL(createvideoTable); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldv, int newv) { sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + VIDEOS_TABLE_NAME + ";"); createTable(sqLiteDatabase); } } public FinchVideoContentProvider() { } public FinchVideoContentProvider(Context context) { } @Override public boolean onCreate() { FileHandlerFactory fileHandlerFactory = new FileHandlerFactory(new File(getContext().getFilesDir(), FINCH_VIDEO_FILE_CACHE)); setFileHandlerFactory(fileHandlerFactory); mOpenHelper = new DatabaseHelper(getContext(), DATABASE_NAME, null); mDb = mOpenHelper.getWritableDatabase(); return true; } @Override public SQLiteDatabase getDatabase() { return mDb; } /** * Content provider query method that converts its parameters into a YouTube * RESTful search query. * * @param uri a reference to the query for videos, the query string can * contain, "q='key_words'". The keywords are sent to the google YouTube * API where they are used to search the YouTube video database. * @param projection * @param where not used in this provider. * @param whereArgs not used in this provider. * @param sortOrder not used in this provider. * @return a cursor containing the results of a YouTube search query. */ @Override public Cursor query(Uri uri, String[] projection, String where, String[] whereArgs, String sortOrder) { Cursor queryCursor; int match = sUriMatcher.match(uri); switch (match) { case VIDEOS: // the query is passed out of band of other information passed // to this method -- its not an argument. String queryText = uri. getQueryParameter(FinchVideo.Videos.QUERY_PARAM_NAME); if (queryText == null) { // A null cursor is an acceptable argument to the method, // CursorAdapter.changeCursor(Cursor c), which interprets // the value by canceling all adapter state so that the // component for which the cursor is adapting data will // display no content. return null; } String select = FinchVideo.Videos.QUERY_TEXT_NAME + " = '" + queryText + "'"; // quickly return already matching data queryCursor = mDb.query(VIDEOS_TABLE_NAME, projection, select, whereArgs, null, null, sortOrder); // make the cursor observe the requested query queryCursor.setNotificationUri( getContext().getContentResolver(), uri); /** * Always try to update results with the latest data from the * network. * * Spawning an asynchronous load task thread, guarantees that * the load has no chance to block any content provider method, * and therefore no chance to block the UI thread. * * While the request loads, we return the cursor with existing * data to the client. * * If the existing cursor is empty, the UI will render no * content until it receives URI notification. * * Content updates that arrive when the asynchronous network * request completes will appear in the already returned cursor, * since that cursor query will match that of * newly arrived items. */ if (!"".equals(queryText)) { asyncQueryRequest(queryText, QUERY_URI + encode(queryText)); } break; case VIDEO_ID: case THUMB_VIDEO_ID: long videoID = ContentUris.parseId(uri); queryCursor = mDb.query(VIDEOS_TABLE_NAME, projection, BaseColumns._ID + " = " + videoID, whereArgs, null, null, null); queryCursor.setNotificationUri( getContext().getContentResolver(), uri); break; case THUMB_ID: String uriString = uri.toString(); int lastSlash = uriString.lastIndexOf("/"); String mediaID = uriString.substring(lastSlash + 1); queryCursor = mDb.query(VIDEOS_TABLE_NAME, projection, FinchVideo.Videos.MEDIA_ID_NAME + " = " + mediaID, whereArgs, null, null, null); queryCursor.setNotificationUri( getContext().getContentResolver(), uri); break; default: throw new IllegalArgumentException("unsupported uri: " + QUERY_URI); } return queryCursor; } /** * Provides a handler that can parse YouTube gData RSS content. * * @param requestTag unique tag identifying this request. * @return a YouTubeHandler object. */ @Override protected ResponseHandler newResponseHandler(String requestTag) { return new YouTubeHandler(this, requestTag); } /** * Provides read only access to files that have been downloaded and stored * in the provider cache. Specifically, in this provider, clients can * access the files of downloaded thumbnail images. */ @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { // only support read only files if (!"r".equals(mode.toLowerCase())) { throw new FileNotFoundException("Unsupported mode, " + mode + ", for uri: " + uri); } return openFileHelper(uri, mode); } @Override public String getType(Uri uri) { switch (sUriMatcher.match(uri)) { case VIDEOS: return FinchVideo.Videos.CONTENT_TYPE; case VIDEO_ID: return FinchVideo.Videos.CONTENT_VIDEO_TYPE; case THUMB_ID: return FinchVideo.Videos.CONTENT_THUMB_TYPE; default: throw new IllegalArgumentException("Unknown video type: " + uri); } } @Override public Uri insert(Uri uri, ContentValues initialValues) { // Validate the requested uri if (sUriMatcher.match(uri) != VIDEOS) { throw new IllegalArgumentException("Unknown URI " + uri); } ContentValues values; if (initialValues != null) { values = new ContentValues(initialValues); } else { values = new ContentValues(); } SQLiteDatabase db = getDatabase(); return insert(uri, initialValues, db); } private void verifyValues(ContentValues values) { if (!values.containsKey(FinchVideo.Videos.TITLE)) { Resources r = Resources.getSystem(); values.put(FinchVideo.Videos.TITLE, r.getString(android.R.string.untitled)); } if (!values.containsKey(FinchVideo.Videos.DESCRIPTION)) { Resources r = Resources.getSystem(); values.put(FinchVideo.Videos.DESCRIPTION, r.getString(android.R.string.untitled)); } if (!values.containsKey(FinchVideo.Videos.THUMB_URI_NAME)) { throw new IllegalArgumentException("Thumb uri not specified: " + values); } if (!values.containsKey(FinchVideo.Videos.THUMB_WIDTH_NAME)) { throw new IllegalArgumentException("Thumb width not specified: " + values); } if (!values.containsKey(FinchVideo.Videos.THUMB_HEIGHT_NAME)) { throw new IllegalArgumentException("Thumb height not specified: " + values); } // Make sure that the fields are all set if (!values.containsKey(FinchVideo.Videos.TIMESTAMP)) { Long now = System.currentTimeMillis(); values.put(FinchVideo.Videos.TIMESTAMP, now); } if (!values.containsKey(FinchVideo.Videos.QUERY_TEXT_NAME)) { throw new IllegalArgumentException("Query Text not specified: " + values); } if (!values.containsKey(FinchVideo.Videos.MEDIA_ID_NAME)) { throw new IllegalArgumentException("Media ID not specified: " + values); } } /** * The delegate insert method, which also takes a database parameter. Note * that this method is a direct implementation of a content provider method. */ @Override public Uri insert(Uri uri, ContentValues values, SQLiteDatabase db) { verifyValues(values); // Validate the requested uri int m = sUriMatcher.match(uri); if (m != VIDEOS) { throw new IllegalArgumentException("Unknown URI " + uri); } // insert the values into a new database row String mediaID = (String) values.get(FinchVideo.Videos.MEDIA_ID_NAME); Long rowID = mediaExists(db, mediaID); if (rowID == null) { long time = System.currentTimeMillis(); values.put(FinchVideo.Videos.TIMESTAMP, time); long rowId = db.insert(VIDEOS_TABLE_NAME, FinchVideo.Videos.VIDEO, values); if (rowId >= 0) { Uri insertUri = ContentUris.withAppendedId( FinchVideo.Videos.CONTENT_URI, rowId); getContext().getContentResolver().notifyChange(insertUri, null); return insertUri; } throw new IllegalStateException("could not insert " + "content values: " + values); } return ContentUris.withAppendedId(FinchVideo.Videos.CONTENT_URI, rowID); } private Long mediaExists(SQLiteDatabase db, String mediaID) { Cursor cursor = null; Long rowID = null; try { cursor = db.query(VIDEOS_TABLE_NAME, null, FinchVideo.Videos.MEDIA_ID_NAME + " = '" + mediaID + "'", null, null, null, null); if (cursor.moveToFirst()) { rowID = cursor.getLong(FinchVideo.ID_COLUMN); } } finally { if (cursor != null) { cursor.close(); } } return rowID; } @Override public int delete(Uri uri, String where, String[] whereArgs) { int match = sUriMatcher.match(uri); int affected; SQLiteDatabase db = mOpenHelper.getWritableDatabase(); switch (match) { case VIDEOS: affected = db.delete(VIDEOS_TABLE_NAME, (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs); break; case VIDEO_ID: long videoId = ContentUris.parseId(uri); affected = db.delete(VIDEOS_TABLE_NAME, BaseColumns._ID + "=" + videoId + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs); getContext().getContentResolver().notifyChange(uri, null); break; default: throw new IllegalArgumentException("unknown video element: " + uri); } return affected; } @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { getContext().getContentResolver().notifyChange(uri, null); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; switch (sUriMatcher.match(uri)) { case VIDEOS: count = db.update(VIDEOS_TABLE_NAME, values, where, whereArgs); break; case VIDEO_ID: String videoId = uri.getPathSegments().get(1); count = db.update(VIDEOS_TABLE_NAME, values, BaseColumns._ID + "=" + videoId + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } }