步骤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. */@Overridepublic 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. */@Overrideprotected 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方法分成两部分,是因为响应处理程序是内容提供者的一部分,而且不需要将内容解析程序再定向到其本身:
@Overridepublic 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. */@Overridepublic 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; }}