/** * Copyright 2012 Facebook * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.facebook; import android.content.Context; import android.os.Handler; import android.support.v4.content.Loader; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; class GraphObjectPagingLoader<T extends GraphObject> extends Loader<SimpleGraphObjectCursor<T>> { private final Class<T> graphObjectClass; private boolean skipRoundtripIfCached; private Request originalRequest; private Request currentRequest; private String nextLink; private OnErrorListener onErrorListener; private SimpleGraphObjectCursor<T> cursor; private boolean appendResults = false; private boolean loading = false; public interface OnErrorListener { public void onError(FacebookException error, GraphObjectPagingLoader<?> loader); } public GraphObjectPagingLoader(Context context, Class<T> graphObjectClass) { super(context); this.graphObjectClass = graphObjectClass; } public OnErrorListener getOnErrorListener() { return onErrorListener; } public void setOnErrorListener(OnErrorListener listener) { this.onErrorListener = listener; } public SimpleGraphObjectCursor<T> getCursor() { return cursor; } public void clearResults() { nextLink = null; originalRequest = null; currentRequest = null; deliverResult(null); } public boolean isLoading() { return loading; } public void startLoading(Request request, boolean skipRoundtripIfCached) { originalRequest = request; startLoading(request, skipRoundtripIfCached, 0); } public void refreshOriginalRequest(long afterDelay) { if (originalRequest == null) { throw new FacebookException( "refreshOriginalRequest may not be called until after startLoading has been called."); } startLoading(originalRequest, false, afterDelay); } public void followNextLink() { if (nextLink != null) { appendResults = true; currentRequest = Request.newGraphPathRequest(originalRequest.getSession(), null, new Request.Callback() { @Override public void onCompleted(Response response) { requestCompleted(response); } }); // We rely on the "next" link returned to us being in the right format to return the results we expect. HttpURLConnection connection = null; try { connection = Request.createConnection(new URL(nextLink)); } catch (IOException e) { if (onErrorListener != null) { onErrorListener.onError(new FacebookException(e), this); } return; } loading = true; RequestBatch batch = putRequestIntoBatch(currentRequest, skipRoundtripIfCached); batch.setCacheKey(nextLink.toString()); Request.executeConnectionAsync(connection, batch); } } @Override public void deliverResult(SimpleGraphObjectCursor<T> cursor) { SimpleGraphObjectCursor<T> oldCursor = this.cursor; this.cursor = cursor; if (isStarted()) { super.deliverResult(cursor); if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { oldCursor.close(); } } } @Override protected void onStartLoading() { super.onStartLoading(); if (cursor != null) { deliverResult(cursor); } } private void startLoading(Request request, boolean skipRoundtripIfCached, long afterDelay) { this.skipRoundtripIfCached = skipRoundtripIfCached; appendResults = false; nextLink = null; currentRequest = request; currentRequest.setCallback(new Request.Callback() { @Override public void onCompleted(Response response) { requestCompleted(response); } }); // We are considered loading even if we have a delay. loading = true; final RequestBatch batch = putRequestIntoBatch(request, skipRoundtripIfCached); Runnable r = new Runnable() { @Override public void run() { Request.executeBatchAsync(batch); } }; if (afterDelay == 0) { r.run(); } else { Handler handler = new Handler(); handler.postDelayed(r, afterDelay); } } private RequestBatch putRequestIntoBatch(Request request, boolean skipRoundtripIfCached) { // We just use the request URL as the cache key. RequestBatch batch = new RequestBatch(request); try { batch.setCacheKey(request.getUrlForSingleRequest().toString()); } catch (MalformedURLException e) { throw new FacebookException(e); } batch.setForceRoundTrip(!skipRoundtripIfCached); return batch; } private void requestCompleted(Response response) { Request request = response.getRequest(); if (request != currentRequest) { return; } loading = false; currentRequest = null; FacebookException error = response.getError(); PagedResults result = response.getGraphObjectAs(PagedResults.class); if (result == null && error == null) { error = new FacebookException("GraphObjectPagingLoader received neither a result nor an error."); } if (error != null) { nextLink = null; if (onErrorListener != null) { onErrorListener.onError(error, this); } } else { boolean fromCache = response.getIsFromCache(); addResults(result, fromCache); // Once we get any set of results NOT from the cache, stop trying to get any future ones // from it. if (!fromCache) { skipRoundtripIfCached = false; } } } private void addResults(PagedResults result, boolean fromCache) { SimpleGraphObjectCursor<T> cursorToModify = (cursor == null || !appendResults) ? new SimpleGraphObjectCursor<T>() : new SimpleGraphObjectCursor<T>(cursor); GraphObjectList<T> data = result.getData().castToListOf(graphObjectClass); boolean haveData = data.size() > 0; if (haveData) { PagingInfo paging = result.getPaging(); if (nextLink != null && nextLink.equals(paging.getNext())) { // We got the same "next" link as we just tried to retrieve. This could happen if cached // data is invalid. All we can do in this case is pretend we have finished. haveData = false; } else { nextLink = paging.getNext(); cursorToModify.addGraphObjects(data, fromCache); cursorToModify.setMoreObjectsAvailable(true); } } if (!haveData) { cursorToModify.setMoreObjectsAvailable(false); cursorToModify.setFromCache(fromCache); nextLink = null; } deliverResult(cursorToModify); } interface PagingInfo extends GraphObject { String getNext(); } interface PagedResults extends GraphObject { GraphObjectList<GraphObject> getData(); PagingInfo getPaging(); } }