名著阅读 > 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);
①
            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;
    }
}