步驟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); 1 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); 2 // make the cursor observe the requested query queryCursor.setNotificationUri( getContext.getContentResolver, uri); 3 /* * 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)); 4 } 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; }
以下是關於代碼的一些說明:
1 從輸入的URI中提取查詢參數。只需要把URI中的查詢參數傳遞給query方法,而URI中的其他參數不需要傳遞,因為它們在query方法中的功能不同,不能用於保存查詢關鍵字。
2 首先檢查和查詢關鍵字匹配的本地數據庫中已有的數據。
3 設置通知URI,當提供者改變數據時,query方法返回的游標會接收到更新事件。該操作會啟動第6步,當提供者發起數據變化的事件通知時,會觸發視圖更新。一旦接收到通知,當UI重新繪製時會執行第7步。注意,第6步和第7步沒有給出描述,但是這裡可以討論這些步驟,因為它們和URI通知及查詢相關。
4 擴展異步查詢,下載給定查詢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); 1 } /** * 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); 2 } } /** * 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, 3 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); 4 Thread t = new Thread(requestTask); // allows other requests to run in parallel. t.start; } } } ... }
以下是關於上述代碼的一些說明:
1 getRequestTask方法使用mRequestsInProgress方法訪問正在執行的請求,看是否有相同的請求,它允許asyncQueryRequest通過簡單的if語句阻塞重複請求。
2 請求會在ResponseHandler.handleResponse方法返回後完成,RESTfulContentProvider刪除mRequestsInProgress。
3 newQueryTask,創建UriRequestTask實例,UriRequestTask是Runnable實例,會打開HTTP連接,然後在合適的handler上調用handleResponse。
4 最後,代碼包含了一個唯一的請求,創建任務以運行它,然後在線程中封裝任務用於異步執行。
雖然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); 1 // 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); 2 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); 3 } } } } 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; } }
以下是關於上述代碼的一些說明:
1 處理程序通過在parseYoutubeEntity方法中解析YouTube HTTP實體實現了handleResponse,parseYoutubeEntity方法會插入新的視頻數據。然後,處理程序查詢出一段時間之前的元素並刪除。
2 處理程序完成了媒體元素的解析,使用其包含的內容提供者插入新解析的ContentValues對象。注意,這個操作在我們描述的操作序列中屬於步驟5「響應處理程序將元素添加到本地緩存」。
3 提供者在插入一條新的媒體項後,會初始化自身的異步請求,下載縮略圖內容。後面將很快解釋提供者的這個特性。
插入和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; } }