讀古今文學網 > Android程序設計:第2版 > 步驟4:實現RESTful請求 >

步驟4:實現RESTful請求

步驟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;
    }
}