/******************************************************************************* * 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 org.quantumbadger.redreader.activities.BugReportActivity; import org.quantumbadger.redreader.common.PrioritisedCachedThreadPool; import org.quantumbadger.redreader.common.RRTime; import org.quantumbadger.redreader.common.TorCommon; import org.quantumbadger.redreader.http.HTTPBackend; import org.quantumbadger.redreader.jsonwrap.JsonValue; import org.quantumbadger.redreader.reddit.api.RedditOAuth; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; public final class CacheDownload extends PrioritisedCachedThreadPool.Task { private final CacheRequest mInitiator; private final CacheManager manager; private final UUID session; private volatile boolean mCancelled = false; private static final AtomicBoolean resetUserCredentials = new AtomicBoolean(false); private final HTTPBackend.Request mRequest; public CacheDownload(final CacheRequest initiator, final CacheManager manager, final PrioritisedDownloadQueue queue) { this.mInitiator = initiator; this.manager = manager; if(!initiator.setDownload(this)) { mCancelled = true; } if(initiator.requestSession != null) { session = initiator.requestSession; } else { session = UUID.randomUUID(); } mRequest = HTTPBackend.getBackend().prepareRequest( initiator.context, new HTTPBackend.RequestDetails(mInitiator.url, mInitiator.postFields)); } public synchronized void cancel() { mCancelled = true; new Thread() { @Override public void run() { if(mRequest != null) { mRequest.cancel(); mInitiator.notifyFailure(CacheRequest.REQUEST_FAILURE_CANCELLED, null, null, "Cancelled"); } } }.start(); } public void doDownload() { if(mCancelled) { return; } try { performDownload(mRequest); } catch(Throwable t) { BugReportActivity.handleGlobalError(mInitiator.context, t); } } public static void resetUserCredentialsOnNextRequest() { resetUserCredentials.set(true); } private void performDownload(final HTTPBackend.Request request) { if(mInitiator.queueType == CacheRequest.DOWNLOAD_QUEUE_REDDIT_API) { if(resetUserCredentials.getAndSet(false)) { mInitiator.user.setAccessToken(null); } RedditOAuth.AccessToken accessToken = mInitiator.user.getMostRecentAccessToken(); if(accessToken == null || accessToken.isExpired()) { mInitiator.notifyProgress(true, 0, 0); final RedditOAuth.FetchAccessTokenResult result; if(mInitiator.user.isAnonymous()) { result = RedditOAuth.fetchAnonymousAccessTokenSynchronous(mInitiator.context); } else { result = RedditOAuth.fetchAccessTokenSynchronous(mInitiator.context, mInitiator.user.refreshToken); } if(result.status != RedditOAuth.FetchAccessTokenResultStatus.SUCCESS) { mInitiator.notifyFailure(CacheRequest.REQUEST_FAILURE_REQUEST, result.error.t, result.error.httpStatus, result.error.title + ": " + result.error.message); return; } accessToken = result.accessToken; mInitiator.user.setAccessToken(accessToken); } request.addHeader("Authorization", "bearer " + accessToken.token); } if(mInitiator.queueType == CacheRequest.DOWNLOAD_QUEUE_IMGUR_API) { request.addHeader("Authorization", "Client-ID c3713d9e7674477"); } mInitiator.notifyDownloadStarted(); request.executeInThisThread(new HTTPBackend.Listener() { @Override public void onError(final @CacheRequest.RequestFailureType int failureType, final Throwable exception, final Integer httpStatus) { if(mInitiator.queueType == CacheRequest.DOWNLOAD_QUEUE_REDDIT_API && TorCommon.isTorEnabled()) { HTTPBackend.getBackend().recreateHttpBackend(); resetUserCredentialsOnNextRequest(); } mInitiator.notifyFailure(failureType, exception, httpStatus, ""); } @Override public void onSuccess(final String mimetype, final Long bodyBytes, final InputStream is) { final NotifyOutputStream cacheOs; final CacheManager.WritableCacheFile cacheFile; if(mInitiator.cache) { try { cacheFile = manager.openNewCacheFile(mInitiator, session, mimetype); cacheOs = cacheFile.getOutputStream(); } catch (IOException e) { e.printStackTrace(); final int failureType; if(manager.getPreferredCacheLocation().exists()) { failureType = CacheRequest.REQUEST_FAILURE_STORAGE; } else { failureType = CacheRequest.REQUEST_FAILURE_CACHE_DIR_DOES_NOT_EXIST; } mInitiator.notifyFailure( failureType, e, null, "Could not access the local cache"); return; } } else { cacheOs = null; cacheFile = null; } if(mInitiator.isJson) { final InputStream bis; if(mInitiator.cache) { bis = new BufferedInputStream(new CachingInputStream(is, cacheOs, new CachingInputStream.BytesReadListener() { public void onBytesRead(final long total) { if(bodyBytes != null) { mInitiator.notifyProgress(false, total, bodyBytes); } } }), 64 * 1024); } else { bis = new BufferedInputStream(is, 64 * 1024); } final JsonValue value; try { value = new JsonValue(bis); mInitiator.notifyJsonParseStarted(value, RRTime.utcCurrentTimeMillis(), session, false); value.buildInThisThread(); } catch (Throwable t) { t.printStackTrace(); mInitiator.notifyFailure(CacheRequest.REQUEST_FAILURE_PARSE, t, null, "Error parsing the JSON stream"); return; } if(mInitiator.cache && cacheFile != null) { try { mInitiator.notifySuccess(cacheFile.getReadableCacheFile(), RRTime.utcCurrentTimeMillis(), session, false, mimetype); } catch(IOException e) { if(e.getMessage().contains("ENOSPC")) { mInitiator.notifyFailure(CacheRequest.REQUEST_FAILURE_DISK_SPACE, e, null, "Out of disk space"); } else { mInitiator.notifyFailure(CacheRequest.REQUEST_FAILURE_STORAGE, e, null, "Cache file not found"); } } } } else { if(!mInitiator.cache) { BugReportActivity.handleGlobalError(mInitiator.context, "Cache disabled for non-JSON request"); return; } try { final byte[] buf = new byte[64 * 1024]; int bytesRead; long totalBytesRead = 0; while((bytesRead = is.read(buf)) > 0) { totalBytesRead += bytesRead; cacheOs.write(buf, 0, bytesRead); if(bodyBytes != null) { mInitiator.notifyProgress(false, totalBytesRead, bodyBytes); } } cacheOs.flush(); cacheOs.close(); try { mInitiator.notifySuccess(cacheFile.getReadableCacheFile(), RRTime.utcCurrentTimeMillis(), session, false, mimetype); } catch(IOException e) { if(e.getMessage().contains("ENOSPC")) { mInitiator.notifyFailure(CacheRequest.REQUEST_FAILURE_DISK_SPACE, e, null, "Out of disk space"); } else { mInitiator.notifyFailure(CacheRequest.REQUEST_FAILURE_STORAGE, e, null, "Cache file not found"); } } } catch(IOException e) { if(e.getMessage() != null && e.getMessage().contains("ENOSPC")) { mInitiator.notifyFailure(CacheRequest.REQUEST_FAILURE_STORAGE, e, null, "Out of disk space"); } else { e.printStackTrace(); mInitiator.notifyFailure(CacheRequest.REQUEST_FAILURE_CONNECTION, e, null, "The connection was interrupted"); } } catch(Throwable t) { t.printStackTrace(); mInitiator.notifyFailure(CacheRequest.REQUEST_FAILURE_CONNECTION, t, null, "The connection was interrupted"); } } } }); } @Override public int getPrimaryPriority() { return mInitiator.priority; } @Override public int getSecondaryPriority() { return mInitiator.listId; } @Override public void run() { doDownload(); } }