/******************************************************************************* * This file is part of RedReader. * * RedReader is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * RedReader is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with RedReader. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package org.quantumbadger.redreader.cache; import android.content.Context; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.util.Log; import org.quantumbadger.redreader.account.RedditAccount; import org.quantumbadger.redreader.activities.BugReportActivity; import org.quantumbadger.redreader.cache.downloadstrategy.DownloadStrategy; import org.quantumbadger.redreader.common.PrioritisedCachedThreadPool; import org.quantumbadger.redreader.common.RRError; import org.quantumbadger.redreader.http.HTTPBackend; import org.quantumbadger.redreader.jsonwrap.JsonValue; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.URI; import java.util.List; import java.util.UUID; public abstract class CacheRequest implements Comparable<CacheRequest> { public static final int DOWNLOAD_QUEUE_REDDIT_API = 0; public static final int DOWNLOAD_QUEUE_IMGUR_API = 1; public static final int DOWNLOAD_QUEUE_IMMEDIATE = 2; public static final int DOWNLOAD_QUEUE_IMAGE_PRECACHE = 3; public static final int REQUEST_FAILURE_CONNECTION = 0; public static final int REQUEST_FAILURE_REQUEST = 1; public static final int REQUEST_FAILURE_STORAGE = 2; public static final int REQUEST_FAILURE_CACHE_MISS = 3; public static final int REQUEST_FAILURE_CANCELLED = 4; public static final int REQUEST_FAILURE_MALFORMED_URL = 5; public static final int REQUEST_FAILURE_PARSE = 6; public static final int REQUEST_FAILURE_DISK_SPACE = 7; public static final int REQUEST_FAILURE_REDDIT_REDIRECT = 8; public static final int REQUEST_FAILURE_PARSE_IMGUR = 9; public static final int REQUEST_FAILURE_UPLOAD_FAIL_IMGUR = 10; public static final int REQUEST_FAILURE_CACHE_DIR_DOES_NOT_EXIST = 11; @IntDef({DOWNLOAD_QUEUE_REDDIT_API, DOWNLOAD_QUEUE_IMGUR_API, DOWNLOAD_QUEUE_IMMEDIATE, DOWNLOAD_QUEUE_IMAGE_PRECACHE}) @Retention(RetentionPolicy.SOURCE) public @interface DownloadQueueType {} @IntDef({REQUEST_FAILURE_CONNECTION, REQUEST_FAILURE_REQUEST, REQUEST_FAILURE_STORAGE, REQUEST_FAILURE_CACHE_MISS, REQUEST_FAILURE_CANCELLED, REQUEST_FAILURE_MALFORMED_URL, REQUEST_FAILURE_PARSE, REQUEST_FAILURE_DISK_SPACE, REQUEST_FAILURE_REDDIT_REDIRECT, REQUEST_FAILURE_PARSE_IMGUR, REQUEST_FAILURE_UPLOAD_FAIL_IMGUR, REQUEST_FAILURE_CACHE_DIR_DOES_NOT_EXIST}) @Retention(RetentionPolicy.SOURCE) public @interface RequestFailureType {} private static final PrioritisedCachedThreadPool JSON_NOTIFY_THREADS = new PrioritisedCachedThreadPool(2, "JSON notify"); public final URI url; public final RedditAccount user; public final UUID requestSession; public final int priority; public final int listId; @NonNull public final DownloadStrategy downloadStrategy; public final int fileType; public final @DownloadQueueType int queueType; public final boolean isJson; public final List<HTTPBackend.PostField> postFields; public final boolean cache; private CacheDownload download; private boolean cancelled; public final Context context; // Called by CacheDownload synchronized boolean setDownload(final CacheDownload download) { if (cancelled) return false; this.download = download; return true; } // Can be called to cancel the request public synchronized void cancel() { cancelled = true; if (download != null) { download.cancel(); download = null; } } protected CacheRequest(final URI url, final RedditAccount user, final UUID requestSession, final int priority, final int listId, @NonNull final DownloadStrategy downloadStrategy, final int fileType, final @DownloadQueueType int queueType, final boolean isJson, final boolean cancelExisting, final Context context) { this(url, user, requestSession, priority, listId, downloadStrategy, fileType, queueType, isJson, null, true, cancelExisting, context); } // TODO remove this huge constructor, make mutable protected CacheRequest(final URI url, final RedditAccount user, final UUID requestSession, final int priority, final int listId, @NonNull final DownloadStrategy downloadStrategy, final int fileType, final @DownloadQueueType int queueType, final boolean isJson, final List<HTTPBackend.PostField> postFields, final boolean cache, final boolean cancelExisting, final Context context) { this.context = context; if (user == null) throw new NullPointerException("User was null - set to empty string for anonymous"); if (!downloadStrategy.shouldDownloadWithoutCheckingCache() && postFields != null) throw new IllegalArgumentException("Should not perform cache lookup for POST requests"); if (!isJson && postFields != null) throw new IllegalArgumentException("POST requests must be for JSON values"); if (cache && postFields != null) throw new IllegalArgumentException("Cannot cache a POST request"); if (!cache && !isJson) throw new IllegalArgumentException("Must cache non-JSON requests"); this.url = url; this.user = user; this.requestSession = requestSession; this.priority = priority; this.listId = listId; this.downloadStrategy = downloadStrategy; this.fileType = fileType; this.queueType = queueType; this.isJson = isJson; this.postFields = postFields; this.cache = cache; if (url == null) { notifyFailure(REQUEST_FAILURE_MALFORMED_URL, null, null, "Malformed URL"); cancel(); } } // Queue helpers public final boolean isHigherPriorityThan(final CacheRequest another) { if (priority != another.priority) { return priority < another.priority; } else { return listId < another.listId; } } public int compareTo(final CacheRequest another) { return isHigherPriorityThan(another) ? -1 : (another.isHigherPriorityThan(this) ? 1 : 0); } // Callbacks protected abstract void onCallbackException(Throwable t); protected abstract void onDownloadNecessary(); protected abstract void onDownloadStarted(); protected abstract void onFailure(@RequestFailureType int type, Throwable t, Integer httpStatus, String readableMessage); protected abstract void onProgress(boolean authorizationInProgress, long bytesRead, long totalBytes); protected abstract void onSuccess(CacheManager.ReadableCacheFile cacheFile, long timestamp, UUID session, boolean fromCache, String mimetype); public void onJsonParseStarted(final JsonValue result, final long timestamp, final UUID session, final boolean fromCache) { throw new RuntimeException("CacheRequest method has not been overridden"); } public final void notifyFailure(final @RequestFailureType int type, final Throwable t, final Integer httpStatus, final String readableMessage) { try { onFailure(type, t, httpStatus, readableMessage); } catch (Throwable t1) { Log.e("CacheRequest", "Exception thrown by onFailure", t1); try { onCallbackException(t1); } catch (Throwable t2) { Log.e("CacheRequest", "Exception thrown by onCallbackException", t2); BugReportActivity.addGlobalError(new RRError(null, null, t1)); BugReportActivity.handleGlobalError(context, t2); } } } public final void notifyProgress(final boolean authorizationInProgress, final long bytesRead, final long totalBytes) { try { onProgress(authorizationInProgress, bytesRead, totalBytes); } catch (Throwable t1) { Log.e("CacheRequest", "Exception thrown by onProgress", t1); try { onCallbackException(t1); } catch (Throwable t2) { Log.e("CacheRequest", "Exception thrown by onCallbackException", t2); BugReportActivity.addGlobalError(new RRError(null, null, t1)); BugReportActivity.handleGlobalError(context, t2); } } } public final void notifySuccess(final CacheManager.ReadableCacheFile cacheFile, final long timestamp, final UUID session, final boolean fromCache, final String mimetype) { try { onSuccess(cacheFile, timestamp, session, fromCache, mimetype); } catch (Throwable t1) { Log.e("CacheRequest", "Exception thrown by onSuccess", t1); try { onCallbackException(t1); } catch (Throwable t2) { Log.e("CacheRequest", "Exception thrown by onCallbackException", t2); BugReportActivity.addGlobalError(new RRError(null, null, t1)); BugReportActivity.handleGlobalError(context, t2); } } } public final void notifyJsonParseStarted(final JsonValue result, final long timestamp, final UUID session, final boolean fromCache) { JSON_NOTIFY_THREADS.add(new PrioritisedCachedThreadPool.Task() { @Override public int getPrimaryPriority() { return priority; } @Override public int getSecondaryPriority() { return listId; } @Override public void run() { try { onJsonParseStarted(result, timestamp, session, fromCache); } catch (Throwable t1) { Log.e("CacheRequest", "Exception thrown by onJsonParseStarted", t1); try { onCallbackException(t1); } catch (Throwable t2) { Log.e("CacheRequest", "Exception thrown by onCallbackException", t2); BugReportActivity.addGlobalError(new RRError(null, null, t1)); BugReportActivity.handleGlobalError(context, t2); } } } }); } public final void notifyDownloadNecessary() { try { onDownloadNecessary(); } catch (Throwable t1) { Log.e("CacheRequest", "Exception thrown by onDownloadNecessary", t1); try { onCallbackException(t1); } catch (Throwable t2) { Log.e("CacheRequest", "Exception thrown by onCallbackException", t2); BugReportActivity.addGlobalError(new RRError(null, null, t1)); BugReportActivity.handleGlobalError(context, t2); } } } public final void notifyDownloadStarted() { try { onDownloadStarted(); } catch (Throwable t1) { Log.e("CacheRequest", "Exception thrown by onDownloadStarted", t1); try { onCallbackException(t1); } catch (Throwable t2) { Log.e("CacheRequest", "Exception thrown by onCallbackException", t2); BugReportActivity.addGlobalError(new RRError(null, null, t1)); BugReportActivity.handleGlobalError(context, t2); } } } }