步骤4比目前为止的其他几个步骤要复杂一些。这里需要像类SimpleFinchVideo-ContentProvider一样逐步说明RESTful FinchVideoContentProvider类。首先,FinchVideoContentProvider扩展了RESTfulContentProvider,RESTfulContentProvider又扩展了ContentProvider:
FinchVideoContentProvider extend RESTfulContentProvider {
RESTfulContentProvider提供异步REST操作,它支持Finch提供者植入定制的请求-响应处理器组件。在探讨升级query方法时,将详细解释这一点。
常量和初始化
FinchVideoContentProvider初始化和简单视频应用的内容提供者很相似。对于简单版的FinchVideoContentProvider,我们设置了一个URI匹配器,其唯一的任务是支持匹配特定的缩略图。没有添加匹配多个缩略图的支持,因为这个视图活动不需要这个功能——它只需要加载单个缩略图:
sUriMatcher.addURI(FinchVideo.AUTHORITY,
FinchVideo.Videos.THUMB + "/#", THUMB_ID);
创建数据库
在Java代码中使用下面的这个SQL语句创建Finch视频数据库:
CREATE TABLE video (_ID INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT, description TEXT, thumb_url TEXT,
thumb_width TEXT, thumb_height TEXT, timestamp TEXT,
query_text TEXT, media_id TEXT UNIQUE);
注意,相对于简单版本,我们增加了以下属性:
thumb_url,thumb_width,thumb_height
它们分别是给定视频的缩略图的URL、宽度和高度。
timestamp
当插入一条新的视频记录时,给它添加当前时间戳。
query_text
在数据库中保存查询文本或查询关键字以及每条查询结果。
media_id
这是从GData API中接收的每个视频响应的唯一值。视频项的media_id必须唯一。
网络Query方法
以下方式是我们所倡导的:在FinchYouTubeProvider查询方法的实现中连接网络以满足YouTube数据的查询请求。它是通过调用它的超类中的方法RESTfulContentProvider.asyncQueryRequest(String queryTag,String queryUri)实现这个功能。在这里,queryTag是唯一字符串,它支持合理地拒绝重复的处理请求,queryUri是完整的需要异步下载的URI。而且,在附加了从应用搜索文本框字段中获取的URLEncoder.encoded查询参数后,URI调用请求如下所示:
/** URI for querying video, expects appended keywords. */
private static final String QUERY_URI =
"http://gdata.youtube.com/feeds/api/videos?" +
"max-results=15&format=1&q=";
注意:你可以很容易学会如何创建满足应用需求的GData YouTube URI。Google在http://gdata.youtube.com创建了beta版的工具。如果你在浏览器中访问该页面,它会显示包含了很多选项的Web UI,你可以通过定制这个UI的方式来创建如前一个代码列表中给出的URI。我们使用该UI选择15项结果,并且选择使用移动视频格式。
我们的网络查询方法执行了URI匹配,并增加了以下任务,即操作序列中的“第4步:实现RESTful请求”:
/**
* Content provider query method that converts its parameters into a YouTube
* RESTful search query.
*
* @param uri a reference to the query URI. It may contain "q=
* which 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 -- it's 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,
FinchVideo.Videos._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;
}
以下是关于代码的一些说明:
① 从输入的URI中提取查询参数。只需要把URI中的查询参数传递给query方法,而URI中的其他参数不需要传递,因为它们在query方法中的功能不同,不能用于保存查询关键字。
② 首先检查和查询关键字匹配的本地数据库中已有的数据。
③ 设置通知URI,当提供者改变数据时,query方法返回的游标会接收到更新事件。该操作会启动第6步,当提供者发起数据变化的事件通知时,会触发视图更新。一旦接收到通知,当UI重新绘制时会执行第7步。注意,第6步和第7步没有给出描述,但是这里可以讨论这些步骤,因为它们和URI通知及查询相关。
④ 扩展异步查询,下载给定查询URI。asyncQueryRequest方法封装了每次请求创建的新的线程连接服务。注意,在我们给出的图中,这是第5步;异步请求会扩展线程,从而真正初始化网络通信,YouTube服务会返回响应。
RESTfulContentProvider:REST helper
现在,我们来分析FinchVideoProvider,它继承了RESTful ContentProvider以便执行RESTful请求。首先,要考虑的是给定YouTube请求的行为。正如我们看到的,查询请求和主线程异步运行。RESTful提供者需要处理一些特殊情况,例如某个用户查找“Funny Cats”,而另一个用户正在查询同样的关键字,提供者会删掉第二次请求。另一方面,例如某个用户查找“dogs”,并且在“dogs”查找完成之前又查找了“cats”,provider支持“dogs”查询和“cats”查询并发运行,因为用户可能还会搜索“dogs”,这样就可以复用之前搜索的缓存。
RESTfulContentProvider支持子类扩展异步请求,而且当请求数据到达时,支持使用简单的名为ResponseHandler的插件来自定义处理方式。子类应该覆盖抽象方法RESTfulContentProvider.newResponseHandler,以返回专门用于解析由宿主提供者所请求的响应数据的处理程序。每个处理程序覆盖ResponseHandler.handleResponse(HttpResponse)方法,提供自定义的处理或包含在传递的HttpResponse对象中的HttpEntitys。例如,提供者使用YouTubeHandler来解析YouTube RSS订阅,把读取的每个数据项插入到数据库视频记录中。后面将详细说明这一点。
此外,RESTfulContentProvider类支持子类轻松地执行异步请求,并拒绝重复请求。RESTfulContentProvider通过唯一标签跟踪每个请求,支持子类丢弃重复查询。Finch VideoContentProvider以用户的查询关键字作为请求标签,因为它们能唯一标识某个给定的搜索请求。
FinchVideoContentProvider重写了newResponseHandler方法,如下:
/**
* 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);
}
现在,探讨RESTfulContentProvider的实现,解释它提供给子类的操作。类UriRequestTask提供了runnable接口,可以异步执行REST请求。RESTfulContentProvider使用map mRequestsInProgress,以字符串作为关键字来保证请求的唯一性:
/**
* Encapsulates functions for asynchronous RESTful requests so that subclass
* content providers can use them for initiating requests while still using
* custom methods for interpreting REST-based content such as RSS, ATOM,
* JSON, etc.
*/
public abstract class RESTfulContentProvider extends ContentProvider {
protected FileHandlerFactory mFileHandlerFactory;
private Map<String, UriRequestTask> mRequestsInProgress =
new HashMap<String, UriRequestTask>;
public RESTfulContentProvider(FileHandlerFactory fileHandlerFactory) {
mFileHandlerFactory = fileHandlerFactory;
}
public abstract Uri insert(Uri uri, ContentValues cv, SQLiteDatabase db);
private UriRequestTask getRequestTask(String queryText) {
return mRequestsInProgress.get(queryText);
①
}
/**
* Allows the subclass to define the database used by a response handler.
*
* @return database passed to response handler.
*/
public abstract SQLiteDatabase getDatabase;
public void requestComplete(String mQueryText) {
synchronized (mRequestsInProgress) {
mRequestsInProgress.remove(mQueryText);
②
}
}
/**
* Abstract method that allows a subclass to define the type of handler
* that should be used to parse the response of a given request.
*
* @param requestTag unique tag identifying this request.
* @return The response handler created by a subclass used to parse the
* request response.
*/
protected abstract ResponseHandler newResponseHandler(String requestTag);
UriRequestTask newQueryTask(String requestTag, String url) {
UriRequestTask requestTask;
final HttpGet get = new HttpGet(url);
ResponseHandler handler = newResponseHandler(requestTag);
requestTask = new UriRequestTask(requestTag, this, get,
③
handler, getContext);
mRequestsInProgress.put(requestTag, requestTask);
return requestTask;
}
/**
* Creates a new worker thread to carry out a RESTful network invocation.
*
* @param queryTag unique tag that identifies this request.
*
* @param queryUri the complete URI that should be accessed by this request.
*/
public void asyncQueryRequest(String queryTag, String queryUri) {
synchronized (mRequestsInProgress) {
UriRequestTask requestTask = getRequestTask(queryTag);
if (requestTask == null) {
requestTask = newQueryTask(queryTag, queryUri);
④
Thread t = new Thread(requestTask);
// allows other requests to run in parallel.
t.start;
}
}
}
...
}
以下是关于上述代码的一些说明:
① getRequestTask方法使用mRequestsInProgress方法访问正在执行的请求,看是否有相同的请求,它允许asyncQueryRequest通过简单的if语句阻塞重复请求。
② 请求会在ResponseHandler.handleResponse方法返回后完成,RESTfulContentProvider删除mRequestsInProgress。
③ newQueryTask,创建UriRequestTask实例,UriRequestTask是Runnable实例,会打开HTTP连接,然后在合适的handler上调用handleResponse。
④ 最后,代码包含了一个唯一的请求,创建任务以运行它,然后在线程中封装任务用于异步执行。
虽然RESTfulContentProvider是可重用的任务系统的核心,但为了完整性,我们还要对框架中的其他组件进行介绍。
UriRequestTask。UriRequestTask封装了处理REST请求的异步操作。它是一个简单的类,支持在run方法中执行RESTful GET方法。该操作是步骤4的一部分,即操作序列中的“实现RESTful请求”。正如我们所讨论的,一旦UriRequestTask接收到响应,它会把该响应传递给ResponseHandler.handleResponse方法。我们期望handleResponse方法会执行数据库插入操作,在YouTubeHandler中将看到这一功能:
/**
* Provides a runnable that uses an HttpClient to asynchronously load a given
* URI. After the network content is loaded, the task delegates handling of the
* request to a ResponseHandler specialized to handle the given content.
*/
public class UriRequestTask implements Runnable {
private HttpUriRequest mRequest;
private ResponseHandler mHandler;
protected Context mAppContext;
private RESTfulContentProvider mSiteProvider;
private String mRequestTag;
private int mRawResponse = -1;
public UriRequestTask(HttpUriRequest request,
ResponseHandler handler, Context appContext)
{
this(null, null, request, handler, appContext);
}
public UriRequestTask(String requestTag,
RESTfulContentProvider siteProvider,
HttpUriRequest request,
ResponseHandler handler, Context appContext)
{
mRequestTag = requestTag;
mSiteProvider = siteProvider;
mRequest = request;
mHandler = handler;
mAppContext = appContext;
}
public void setRawResponse(int rawResponse) {
mRawResponse = rawResponse;
}
/**
* Carries out the request on the complete URI as indicated by the protocol,
* host, and port contained in the configuration, and the URI supplied to
* the constructor.
*/
public void run {
HttpResponse response;
try {
response = execute(mRequest);
mHandler.handleResponse(response, getUri);
} catch (IOException e) {
Log.w(Finch.LOG_TAG, "exception processing asynch request", e);
} finally {
if (mSiteProvider != null) {
mSiteProvider.requestComplete(mRequestTag);
}
}
}
private HttpResponse execute(HttpUriRequest mRequest) throws IOException {
if (mRawResponse >= 0) {
return new RawResponse(mAppContext, mRawResponse);
} else {
HttpClient client = new DefaultHttpClient;
return client.execute(mRequest);
}
}
public Uri getUri {
return Uri.parse(mRequest.getURI.toString);
}
}
YouTubeHandler。正如在抽象方法RESTfulContentProvider.newResponseHandler中一样,FinchVideoContentProvider方法返回YouTubeHandler来处理YouTube RSS订阅。YouTubeHandler在内存中使用XML Pull解析器解析输入的数据,遍历获取到的XML RSS数据并处理。YouTubeHandler包含一些复杂特性,但是总体而言,它只是根据需要匹配XML标签来创建ContentValues对象,该对象可以插入到FinchVideoContentProvider的数据库中。当处理程序把解析出的结果都插入提供者数据库时,会执行第5步的一部分。
/**
* Parses YouTube Entity data and inserts it into the finch video content
* provider.
*/
public class YouTubeHandler implements ResponseHandler {
public static final String MEDIA = "media";
public static final String GROUP = "group";
public static final String DESCRIPTION = "description";
public static final String THUMBNAIL = "thumbnail";
public static final String TITLE = "title";
public static final String CONTENT = "content";
public static final String WIDTH = "width";
public static final String HEIGHT = "height";
public static final String YT = "yt";
public static final String DURATION = "duration";
public static final String FORMAT = "format";
public static final String URI = "uri";
public static final String THUMB_URI = "thumb_uri";
public static final String MOBILE_FORMAT = "1";
public static final String ENTRY = "entry";
public static final String ID = "id";
private static final String FLUSH_TIME = "5 minutes";
private RESTfulContentProvider mFinchVideoProvider;
private String mQueryText;
private boolean isEntry;
public YouTubeHandler(RESTfulContentProvider restfulProvider,
String queryText)
{
mFinchVideoProvider = restfulProvider;
mQueryText = queryText;
}
/*
* Handles the response from the YouTube GData server, which is in the form
* of an RSS feed containing references to YouTube videos.
*/
public void handleResponse(HttpResponse response, Uri uri)
throws IOException
{
try {
int newCount = parseYoutubeEntity(response.getEntity);
①
// only flush old state now that new state has arrived
if (newCount > 0) {
deleteOld;
}
} catch (IOException e) {
// use the exception to avoid clearing old state, if we cannot
// get new state. This way we leave the application with some
// data to work with in absence of network connectivity.
// we could retry the request for data in the hope that the network
// might return.
}
}
private void deleteOld {
// delete any old elements, not just ones that match the current query.
Cursor old = null;
try {
SQLiteDatabase db = mFinchVideoProvider.getDatabase;
old = db.query(FinchVideo.Videos.VIDEO, null,
"video." + FinchVideo.Videos.TIMESTAMP +
" < strftime('%s', 'now', '-" + FLUSH_TIME + "')",
null, null, null, null);
int c = old.getCount;
if (old.getCount > 0) {
StringBuffer sb = new StringBuffer;
boolean next;
if (old.moveToNext) {
do {
String ID = old.getString(FinchVideo.ID_COLUMN);
sb.append(FinchVideo.Videos._ID);
sb.append(" = ");
sb.append(ID);
// get rid of associated cached thumb files
mFinchVideoProvider.deleteFile(ID);
next = old.moveToNext;
if (next) {
sb.append(" OR ");
}
} while (next);
}
String where = sb.toString;
db.delete(FinchVideo.Videos.VIDEO, where, null);
Log.d(Finch.LOG_TAG, "flushed old query results: " + c);
}
} finally {
if (old != null) {
old.close;
}
}
}
private int parseYoutubeEntity(HttpEntity entity) throws IOException {
InputStream youTubeContent = entity.getContent;
InputStreamReader inputReader = new InputStreamReader(youTubeContent);
int inserted = 0;
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance;
factory.setNamespaceAware(false);
XmlPullParser xpp = factory.newPullParser;
xpp.setInput(inputReader);
int eventType = xpp.getEventType;
String startName = null;
ContentValues mediaEntry = null;
// iterative pull parsing is a useful way to extract data from
// streams, since we don't have to hold the DOM model in memory
// during the parsing step.
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_DOCUMENT) {
} else if (eventType == XmlPullParser.END_DOCUMENT) {
} else if (eventType == XmlPullParser.START_TAG) {
startName = xpp.getName;
if ((startName != null)) {
if ((ENTRY).equals(startName))
{
mediaEntry = new ContentValues;
mediaEntry.put(FinchVideo.Videos.QUERY_TEXT_NAME,
mQueryText);
}
if ((MEDIA + ":" + CONTENT).equals(startName)) {
int c = xpp.getAttributeCount;
String mediaUri = null;
boolean isMobileFormat = false;
for (int i = 0; i < c; i++) {
String attrName = xpp.getAttributeName(i);
String attrValue = xpp.getAttributeValue(i);
if ((attrName != null) &&
URI.equals(attrName))
{
mediaUri = attrValue;
}
if ((attrName != null) && (YT + ":" + FORMAT).
equals(MOBILE_FORMAT))
{
isMobileFormat = true;
}
}
if (isMobileFormat && (mediaUri != null)) {
mediaEntry.put(URI, mediaUri);
}
}
if ((MEDIA + ":" + THUMBNAIL).equals(startName)) {
int c = xpp.getAttributeCount;
for (int i = 0; i < c; i++) {
String attrName = xpp.getAttributeName(i);
String attrValue = xpp.getAttributeValue(i);
if (attrName != null) {
if ("url".equals(attrName)) {
mediaEntry.put(
FinchVideo.Videos.
THUMB_URI_NAME,
attrValue);
} else if (WIDTH.equals(attrName))
{
mediaEntry.put(
FinchVideo.Videos.
THUMB_WIDTH_NAME,
attrValue);
} else if (HEIGHT.equals(attrName))
{
mediaEntry.put(
FinchVideo.Videos.
THUMB_HEIGHT_NAME,
attrValue);
}
}
}
}
if (ENTRY.equals(startName)) {
isEntry = true;
}
}
} else if(eventType == XmlPullParser.END_TAG) {
String endName = xpp.getName;
if (endName != null) {
if (ENTRY.equals(endName)) {
isEntry = false;
} else if (endName.equals(MEDIA + ":" + GROUP)) {
// insert the complete media group
inserted++;
// Directly invoke insert on the finch video
// provider, without using content resolver. We
// would not want the content provider to sync this
// data back to itself.
SQLiteDatabase db =
mFinchVideoProvider.getDatabase;
String mediaID = (String) mediaEntry.get(
FinchVideo.Videos.MEDIA_ID_NAME);
// insert thumb uri
String thumbContentUri =
FinchVideo.Videos.THUMB_URI + "/" + mediaID;
mediaEntry.put(FinchVideo.Videos.
THUMB_CONTENT_URI_NAME,
thumbContentUri);
String cacheFileName =
mFinchVideoProvider.getCacheName(mediaID);
mediaEntry.put(FinchVideo.Videos._DATA,
cacheFileName);
Uri providerUri = mFinchVideoProvider.
insert(FinchVideo.Videos.CONTENT_URI,
mediaEntry, db);
②
if (providerUri != null) {
String thumbUri = (String) mediaEntry.
get(FinchVideo.Videos.THUMB_URI_NAME);
// We might consider lazily downloading the
// image so that it was only downloaded on
// viewing. Downloading more aggressively
// could also improve performance.
mFinchVideoProvider.
cacheUri2File(String.valueOf(ID),
thumbUrl);
③
}
}
}
} else if (eventType == XmlPullParser.TEXT) {
// newline can turn into an extra text event
String text = xpp.getText;
if (text != null) {
text = text.trim;
if ((startName != null) && (!"".equals(text))){
if (ID.equals(startName) && isEntry) {
int lastSlash = text.lastIndexOf("/");
String entryId =
text.substring(lastSlash + 1);
mediaEntry.put(FinchVideo.Videos.MEDIA_ID_NAME,
entryId);
} else if ((MEDIA + ":" + TITLE).
equals(startName))
{
mediaEntry.put(TITLE, text);
} else if ((MEDIA + ":" +
DESCRIPTION).equals(startName))
{
mediaEntry.put(DESCRIPTION, text);
}
}
}
}
eventType = xpp.next;
}
// an alternate notification scheme might be to notify only after
// all entries have been inserted.
} catch (XmlPullParserException e) {
Log.d(Ch11.LOG_TAG,
"could not parse video feed", e);
} catch (IOException e) {
Log.d(Ch11.LOG_TAG,
"could not process video stream", e);
}
return inserted;
}
}
以下是关于上述代码的一些说明:
① 处理程序通过在parseYoutubeEntity方法中解析YouTube HTTP实体实现了handleResponse,parseYoutubeEntity方法会插入新的视频数据。然后,处理程序查询出一段时间之前的元素并删除。
② 处理程序完成了媒体元素的解析,使用其包含的内容提供者插入新解析的ContentValues对象。注意,这个操作在我们描述的操作序列中属于步骤5“响应处理程序将元素添加到本地缓存”。
③ 提供者在插入一条新的媒体项后,会初始化自身的异步请求,下载缩略图内容。后面将很快解释提供者的这个特性。
插入和ResponseHandlers
下面详细探讨步骤5,Finch视频提供者中insert的实现方式和简单的视频提供者的几乎相同。此外,正如我们在应用中看到的,视频插入是query方法的副产品。值得一提的是,insert方法可以分成两部分,内容提供者客户端调用第一部分,响应处理程序调用第二部分,其实现代码如下所示。第一种方式委托给第二种方式。我们把insert方法分成两部分,是因为响应处理程序是内容提供者的一部分,而且不需要将内容解析程序再定向到其本身:
@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);
}
YouTubeHandler使用以下方式,直接把记录插入到简单的视频数据库中。注意,如果数据库中已经包含准备插入的媒体的mediaID,就不需要插入该记录。通过这种方式可以避免视频项重复,当把新的数据和老的且尚未过期的数据集成起来时,可能会出现视频项重复:
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);
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);
mContentResolver.notifyChange(insertUri, null);
return insertUri;
} else {
throw new IllegalStateException("could not insert " +
"content values: " + values);
}
}
return ContentUris.withAppendedId(FinchVideo.Videos.CONTENT_URI, rowID);
}
文件管理:缩略图存储
现在,我们已经了解了RESTful提供者框架是如何运作的,接下来将解释提供者是如何处理缩略图的。
前面描述了ContentResolver.openInputStream方法作为内容提供者为客户端打开文件的方式。在Finch视频实例中,我们使用该特征提供缩略图服务。把图像保存成文件使得我们能够避免使用数据库的blob类型及其带来的性能开销,并且当客户端请求这些图片时,可以只下载这些图片。如果内容提供者要提供文件服务,必须重写ContentProvider.openFile方法,ContentProvider.openFile方法会打开要提供服务的文件描述符。该方法最简单的实现方式是调用openFileHelper,执行一些便捷的功能,支持ContentResolver读取_data变量,加载其引用的文件。如果provider没有重写该方法,你会看到如下异常:"No files supported by provider at..."。这种简单的实现方式只支持“只读”访问方式,如下所示:
/**
* 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);
}
最后,通过ResponseHandler的FileHandler实现,从每个媒体程序对应的YouTube缩略图URL下载图像数据。该FileHandlerFactory支持管理在特定的缓存目录下保存的缓存文件,而且该FileHandlerFactory支持选择在哪里保存这些文件:
/**
* Creates instances of FileHandler objects that use a common cache directory.
* The cache directory is set in the constructor to the file handler factory.
*/
public class FileHandlerFactory {
private String mCacheDir;
public FileHandlerFactory(String cacheDir) {
mCacheDir = cacheDir;
init;
}
private void init {
File cacheDir = new File(mCacheDir);
if (!cacheDir.exists) {
cacheDir.mkdir;
}
}
public FileHandler newFileHandler(String id) {
return new FileHandler(mCacheDir, id);
}
// not really used since ContentResolver uses _data field.
public File getFile(String ID) {
String cachePath = getFileName(ID);
File cacheFile = new File(cachePath);
if (cacheFile.exists) {
return cacheFile;
}
return null;
}
public void delete(String ID) {
String cachePath = mCacheDir + "/" + ID;
File cacheFile = new File(cachePath);
if (cacheFile.exists) {
cacheFile.delete;
}
}
public String getFileName(String ID) {
return mCacheDir + "/" + ID;
}
}
/**
* Writes data from URLs into a local file cache that can be referenced by a
* database ID.
*/
public class FileHandler implements ResponseHandler {
private String mId;
private String mCacheDir;
public FileHandler(String cacheDir, String id) {
mCacheDir = cacheDir;
mId = id;
}
public
String getFileName(String ID) {
return mCacheDir + "/" + ID;
}
public void handleResponse(HttpResponse response, Uri uri)
throws IOException
{
InputStream urlStream = response.getEntity.getContent;
FileOutputStream fout =
new FileOutputStream(getFileName(mId));
byte bytes = new byte[256];
int r = 0;
do {
r = urlStream.read(bytes);
if (r >= 0) {
fout.write(bytes, 0, r);
}
} while (r >= 0);
urlStream.close;
fout.close;
}
}
