/*
* Copyright (C) 2008 The Android Open Source Project
*
* 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.novoda.downloadmanager.lib;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.drm.DrmManagerClient;
import android.net.NetworkInfo;
import android.net.TrafficStats;
import android.os.PowerManager;
import android.os.Process;
import android.text.TextUtils;
import android.util.Pair;
import com.novoda.downloadmanager.lib.logger.LLog;
import com.novoda.downloadmanager.notifications.DownloadNotifier;
import java.io.Closeable;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.util.Locale;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static com.novoda.downloadmanager.lib.Constants.UNKNOWN_BYTE_SIZE;
import static com.novoda.downloadmanager.lib.DownloadStatus.HTTP_DATA_ERROR;
import static com.novoda.downloadmanager.lib.DownloadStatus.QUEUED_DUE_CLIENT_RESTRICTIONS;
import static com.novoda.downloadmanager.lib.FileDownloadInfo.NetworkState;
import static com.novoda.downloadmanager.lib.IOHelpers.closeAfterWrite;
import static java.net.HttpURLConnection.*;
/**
* Task which executes a given {@link FileDownloadInfo}: making network requests,
* persisting data to disk, and updating {@link DownloadProvider}.
*/
class DownloadTask implements Runnable {
/**
* For intents used to notify the user that a download exceeds a size threshold, if this extra
* is true, WiFi is required for this download size; otherwise, it is only recommended.
*/
public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired";
private static final String TAG = "DownloadManager-DownloadTask";
// TODO: bind each download to a specific network interface to avoid state
// checking races once we have ConnectivityManager API
private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
private static final int HTTP_TEMP_REDIRECT = 307;
private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS);
private final Context context;
private final FileDownloadInfo originalDownloadInfo;
private final DownloadBatch originalDownloadBatch;
private final SystemFacade systemFacade;
private final StorageManager storageManager;
private final DownloadNotifier downloadNotifier;
private final BatchInformationBroadcaster batchInformationBroadcaster;
private final BatchRepository batchRepository;
private final DownloadsUriProvider downloadsUriProvider;
private final FileDownloadInfo.ControlStatus.Reader controlReader;
private final NetworkChecker networkChecker;
private final DownloadReadyChecker downloadReadyChecker;
private final Clock clock;
private final DownloadsRepository downloadsRepository;
public DownloadTask(Context context,
SystemFacade systemFacade,
FileDownloadInfo originalDownloadInfo,
DownloadBatch originalDownloadBatch,
StorageManager storageManager,
DownloadNotifier downloadNotifier,
BatchInformationBroadcaster batchInformationBroadcaster,
BatchRepository batchRepository,
DownloadsUriProvider downloadsUriProvider,
FileDownloadInfo.ControlStatus.Reader controlReader,
NetworkChecker networkChecker,
DownloadReadyChecker downloadReadyChecker,
Clock clock,
DownloadsRepository downloadsRepository) {
this.context = context;
this.systemFacade = systemFacade;
this.originalDownloadInfo = originalDownloadInfo;
this.originalDownloadBatch = originalDownloadBatch;
this.storageManager = storageManager;
this.downloadNotifier = downloadNotifier;
this.batchInformationBroadcaster = batchInformationBroadcaster;
this.batchRepository = batchRepository;
this.downloadsUriProvider = downloadsUriProvider;
this.controlReader = controlReader;
this.networkChecker = networkChecker;
this.downloadReadyChecker = downloadReadyChecker;
this.clock = clock;
this.downloadsRepository = downloadsRepository;
}
/**
* Returns the user agent provided by the initiating app, or use the default one
*/
private String userAgent() {
String userAgent = originalDownloadInfo.getUserAgent();
if (userAgent == null) {
userAgent = Constants.DEFAULT_USER_AGENT;
}
return userAgent;
}
/**
* State for the entire run() method.
*/
static class State {
public String filename;
public String mimeType;
public int retryAfter = 0;
public boolean gotData = false;
public String requestUri;
public long totalBytes = UNKNOWN_BYTE_SIZE;
public long currentBytes = 0;
public String headerETag;
public boolean continuingDownload = false;
public long bytesNotified = 0;
public long timeLastNotification = 0;
public int networkType = -1; //ConnectivityManager.TYPE_NONE;
/**
* Historical bytes/second speed of this download.
*/
public long speed;
/**
* Time when current sample started.
*/
public long speedSampleStart;
/**
* Bytes transferred since current sample started.
*/
public long speedSampleBytes;
public long contentLength = UNKNOWN_BYTE_SIZE;
public String contentDisposition;
public String contentLocation;
public int redirectionCount;
public URL url;
public boolean shouldPause;
public State(FileDownloadInfo info) {
mimeType = normalizeMimeType(info.getMimeType());
requestUri = info.getUri();
filename = info.getFileName();
totalBytes = info.getTotalBytes();
currentBytes = info.getCurrentBytes();
}
State() {
// This constructor is intentionally empty. Only used for tests.
}
public void resetBeforeExecute() {
// Reset any state from previous execution
contentLength = UNKNOWN_BYTE_SIZE;
contentDisposition = null;
contentLocation = null;
redirectionCount = 0;
}
}
private static String normalizeMimeType(String type) {
if (type == null) {
return null;
}
type = type.trim().toLowerCase(Locale.ROOT);
final int semicolonIndex = type.indexOf(';');
if (semicolonIndex != -1) {
type = type.substring(0, semicolonIndex);
}
return type;
}
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
runInternal();
} finally {
downloadNotifier.notifyDownloadSpeed(originalDownloadInfo.getId(), 0);
}
}
private void runInternal() {
// Skip when download already marked as finished; this download was probably started again while racing with UpdateThread.
int downloadStatus = downloadsRepository.getDownloadStatus(originalDownloadInfo.getId());
if (downloadStatus == DownloadStatus.SUCCESS) {
LLog.d("Download " + originalDownloadInfo.getId() + " already finished; skipping");
return;
}
if (DownloadStatus.isCancelled(downloadStatus)) {
LLog.d("Download " + originalDownloadInfo.getId() + " already cancelled; skipping");
return;
}
if (DownloadStatus.isError(downloadStatus)) {
LLog.d("Download " + originalDownloadInfo.getId() + " already failed: status = " + downloadStatus + "; skipping");
return;
}
if (DownloadStatus.isDeleting(downloadStatus)) {
LLog.d("Download " + originalDownloadInfo.getId() + " is deleting: status = " + downloadStatus + "; skipping");
return;
}
PowerManager.WakeLock wakeLock = null;
int finalStatus = DownloadStatus.UNKNOWN_ERROR;
int numFailed = originalDownloadInfo.getNumFailed();
String errorMsg = null;
State state = new State(originalDownloadInfo);
try {
checkDownloadCanProceed();
if (downloadStatus != DownloadStatus.RUNNING) {
downloadsRepository.setDownloadRunning(originalDownloadInfo);
updateBatchStatus(originalDownloadInfo.getBatchId(), originalDownloadInfo.getId());
}
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
wakeLock.acquire();
LLog.i("Download " + originalDownloadInfo.getId() + " starting");
// Remember which network this download started on; used to
// determine if errors were due to network changes.
final NetworkInfo networkInfo = systemFacade.getActiveNetworkInfo(); // Param downloadInfo.uid removed TODO
if (networkInfo != null) {
state.networkType = networkInfo.getType();
}
// Network traffic on this thread should be counted against the
// requesting UID, and is tagged with well-known value.
TrafficStats.setThreadStatsTag(0xFFFFFF01); // TrafficStats.TAG_SYSTEM_DOWNLOAD
// TrafficStats.setThreadStatsUid(downloadInfo.uid); Won't need this as we will be an Android library (doing own work)
try {
// TODO: migrate URL sanity checking into client side of API
state.url = new URL(state.requestUri);
} catch (MalformedURLException e) {
throw new StopRequestException(DownloadStatus.BAD_REQUEST, e);
}
executeDownload(state);
finalizeDestinationFile(state);
finalStatus = DownloadStatus.SUCCESS;
} catch (StopRequestException error) {
// remove the cause before printing, in case it contains PII
errorMsg = error.getMessage();
String msg = "Aborting request for download " + originalDownloadInfo.getId() + ": " + errorMsg;
LLog.w(msg, error);
finalStatus = error.getFinalStatus();
// Nobody below our level should request retries, since we handle
// failure counts at this level.
if (finalStatus == DownloadStatus.WAITING_TO_RETRY) {
throw new IllegalStateException("Execution should always throw final error codes");
}
// Some errors should be retryable, unless we fail too many times.
if (isStatusRetryable(finalStatus)) {
if (state.gotData) {
numFailed = 1;
} else {
numFailed += 1;
}
if (numFailed < Constants.MAX_RETRIES && finalStatus != QUEUED_DUE_CLIENT_RESTRICTIONS) {
final NetworkInfo info = systemFacade.getActiveNetworkInfo(); // Param downloadInfo.uid removed TODO
if (info != null && info.getType() == state.networkType && info.isConnected()) {
// Underlying network is still intact, use normal backoff
finalStatus = DownloadStatus.WAITING_TO_RETRY;
} else {
// Network changed, retry on any next available
finalStatus = DownloadStatus.WAITING_FOR_NETWORK;
}
}
}
// fall through to finally block
} catch (Throwable ex) {
errorMsg = ex.getMessage();
String msg = "Exception for id " + originalDownloadInfo.getId() + ": " + errorMsg;
LLog.w(msg, ex);
finalStatus = DownloadStatus.UNKNOWN_ERROR;
// falls through to the code that reports an error
} finally {
TrafficStats.clearThreadStatsTag();
cleanupDestination(state, finalStatus);
notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed);
hackToForceClientsRefreshRulesIfConnectionDropped(finalStatus);
LLog.i("Download " + originalDownloadInfo.getId() + " finished with status " + DownloadStatus.statusToString(finalStatus));
if (wakeLock != null) {
wakeLock.release();
}
}
storageManager.incrementNumDownloadsSoFar();
}
private void hackToForceClientsRefreshRulesIfConnectionDropped(int finalStatus) {
if (DownloadStatus.WAITING_FOR_NETWORK == finalStatus) {
try {
checkClientRules();
} catch (StopRequestException e) {
e.printStackTrace();
}
}
}
/**
* Fully execute a single download request. Setup and send the request,
* handle the response, and transfer the data to the destination file.
*/
private void executeDownload(State state) throws StopRequestException {
state.resetBeforeExecute();
setupDestinationFile(state);
if (originalDownloadInfo.shouldAllowTarUpdate(state.mimeType)) {
state.totalBytes = UNKNOWN_BYTE_SIZE;
}
// skip when already finished; remove after fixing race in 5217390
if (downloadAlreadyFinished(state)) {
LLog.i("Skipping initiating request for download " + originalDownloadInfo.getId() + "; already completed");
return;
}
while (state.redirectionCount++ < Constants.MAX_REDIRECTS) {
// Open connection and follow any redirects until we have a useful
// response with body.
HttpURLConnection conn = null;
try {
checkConnectivity();
conn = (HttpURLConnection) state.url.openConnection();
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(DEFAULT_TIMEOUT);
conn.setReadTimeout(DEFAULT_TIMEOUT);
addRequestHeaders(state, conn);
final int responseCode = conn.getResponseCode();
switch (responseCode) {
case HTTP_OK:
if (state.continuingDownload) {
throw new StopRequestException(DownloadStatus.CANNOT_RESUME, "Expected partial, but received OK");
}
processResponseHeaders(state, conn);
transferData(state, conn);
return;
case HTTP_PARTIAL:
if (!state.continuingDownload) {
throw new StopRequestException(DownloadStatus.CANNOT_RESUME, "Expected OK, but received partial");
}
transferData(state, conn);
return;
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
case HTTP_TEMP_REDIRECT:
final String location = conn.getHeaderField("Location");
state.url = new URL(state.url, location);
if (responseCode == HTTP_MOVED_PERM) {
// Push updated URL back to database
state.requestUri = state.url.toString();
}
continue;
case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
throw new StopRequestException(DownloadStatus.CANNOT_RESUME, "Requested range not satisfiable");
case HTTP_UNAVAILABLE:
parseRetryAfterHeaders(state, conn);
throw new StopRequestException(
HTTP_UNAVAILABLE, conn.getResponseMessage());
case HTTP_INTERNAL_ERROR:
throw new StopRequestException(
HTTP_INTERNAL_ERROR, conn.getResponseMessage());
default:
StopRequestException.throwUnhandledHttpError(responseCode, conn.getResponseMessage());
}
} catch (UnknownHostException e) {
// Unable to resolve host request
throw new StopRequestException(HTTP_NOT_FOUND, e);
} catch (IOException e) {
// Trouble with low-level sockets
throw new StopRequestException(HTTP_DATA_ERROR, e);
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
throw new StopRequestException(DownloadStatus.TOO_MANY_REDIRECTS, "Too many redirects");
}
/**
* Check if the download has been paused or canceled, stopping the request appropriately if it
* has been.
*/
private void checkDownloadCanProceed() throws StopRequestException {
if (clock.intervalLessThan(Clock.Interval.ONE_SECOND)) {
return;
}
clock.startInterval();
checkIsPausedOrCanceled();
checkClientRules();
}
private void checkIsPausedOrCanceled() throws StopRequestException {
FileDownloadInfo.ControlStatus controlStatus = controlReader.newControlStatus();
if (controlStatus.isPaused()) {
throw new StopRequestException(DownloadStatus.PAUSED_BY_APP, "download paused by owner");
}
if (controlStatus.isCanceled()) {
throw new StopRequestException(DownloadStatus.CANCELED, "download canceled");
}
}
private void checkClientRules() throws StopRequestException {
if (!downloadReadyChecker.clientAllowsToDownload(originalDownloadBatch)) {
throw new StopRequestException(DownloadStatus.QUEUED_DUE_CLIENT_RESTRICTIONS, "Cannot proceed because client denies");
}
}
private boolean downloadAlreadyFinished(State state) {
return (state.currentBytes == state.totalBytes) && !originalDownloadInfo.shouldAllowTarUpdate(state.mimeType);
}
/**
* Transfer data from the given connection to the destination file.
*/
private void transferData(State state, HttpURLConnection conn) throws StopRequestException {
DrmManagerClient drmClient = null;
InputStream in = null;
OutputStream out = null;
FileDescriptor outFd = null;
try {
try {
in = conn.getInputStream();
} catch (IOException e) {
throw new StopRequestException(HTTP_DATA_ERROR, e);
}
try {
if (DownloadDrmHelper.isDrmConvertNeeded(state.mimeType)) {
// drmClient = new DrmManagerClient(context);
// final RandomAccessFile file = new RandomAccessFile(new File(state.filename), "rw");
// out = new DrmOutputStream(drmClient, file, state.mimeType);
// outFd = file.getFD();
throw new IllegalStateException("DRM not supported atm");
} else {
out = new FileOutputStream(state.filename, true);
outFd = ((FileOutputStream) out).getFD();
}
} catch (IOException e) {
throw new StopRequestException(DownloadStatus.FILE_ERROR, e);
}
// Start streaming data, periodically watch for pause/cancel
// commands and checking disk space as needed.
transferData(state, in, out);
// try {
// if (out instanceof DrmOutputStream) {
// ((DrmOutputStream) out).finish();
// }
// } catch (IOException e) {
// throw new StopRequestException(STATUS_FILE_ERROR, e);
// }
} catch (StopRequestException exception) {
if (exception.getFinalStatus() == DownloadStatus.PAUSED_BY_APP) {
notifyThroughDatabase(state, DownloadStatus.PAUSING, exception.getMessage(), 0);
}
// We still have to throw the exception, otherwise the parent
// thinks that the download has been completed OK, when is not
// We should remove exceptions as a flow control in order to avoid this
throw exception;
} finally {
// if (drmClient != null) {
// drmClient.release();
// }
closeQuietly(in);
closeAfterWrite(out, outFd);
}
}
/**
* Check if current connectivity is valid for this request.
*/
private void checkConnectivity() throws StopRequestException {
// checking connectivity will apply current policy
final NetworkState networkUsable = networkChecker.checkCanUseNetwork(originalDownloadInfo);
if (networkUsable != NetworkState.OK) {
int status = DownloadStatus.WAITING_FOR_NETWORK;
if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) {
status = DownloadStatus.QUEUED_FOR_WIFI;
notifyPauseDueToSize(true);
} else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
status = DownloadStatus.QUEUED_FOR_WIFI;
notifyPauseDueToSize(false);
}
throw new StopRequestException(status, networkUsable.name());
}
}
void notifyPauseDueToSize(boolean isWifiRequired) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(originalDownloadInfo.getAllDownloadsUri());
intent.setClassName(SizeLimitActivity.class.getPackage().getName(), SizeLimitActivity.class.getName());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(EXTRA_IS_WIFI_REQUIRED, isWifiRequired);
context.startActivity(intent);
}
/**
* Transfer as much data as possible from the HTTP response to the
* destination file.
*/
private void transferData(State state, InputStream in, OutputStream out) throws StopRequestException {
StorageSpaceVerifier spaceVerifier = new StorageSpaceVerifier(storageManager, originalDownloadInfo.getDestination(), state.filename);
DataWriter checkedWriter = new CheckedWriter(spaceVerifier, out);
DataWriter dataWriter = new NotifierWriter(getContentResolver(),
checkedWriter,
downloadNotifier,
originalDownloadInfo,
checkOnWrite);
DataTransferer dataTransferer;
if (originalDownloadInfo.shouldAllowTarUpdate(state.mimeType)) {
dataTransferer = new TarTruncator(dataWriter);
} else {
dataTransferer = new RegularDataTransferer(dataWriter);
}
State newState = dataTransferer.transferData(state, in);
handleEndOfStream(newState);
}
private final NotifierWriter.WriteChunkListener checkOnWrite = new NotifierWriter.WriteChunkListener() {
@Override
public void chunkWritten(FileDownloadInfo downloadInfo) throws StopRequestException {
checkDownloadCanProceed();
}
};
/**
* Called after a successful completion to take any necessary action on the downloaded file.
*/
private void finalizeDestinationFile(State state) {
if (state.filename != null) {
// make sure the file is readable
setPermissions(state.filename, 0644, -1, -1);
// FileUtils.setPermission
//http://stackoverflow.com/questions/11408154/how-to-get-file-permission-mode-programmatically-in-java
}
}
/**
* Called just before the thread finishes, regardless of status, to take any necessary action on
* the downloaded file.
*/
private void cleanupDestination(State state, int finalStatus) {
if (state.filename != null && DownloadStatus.isError(finalStatus)) {
LLog.d("cleanupDestination() deleting " + state.filename);
boolean deleted = new File(state.filename).delete();
if (!deleted) {
LLog.e("File not deleted");
}
state.filename = null;
}
}
/**
* Called when we've reached the end of the HTTP response stream, to update the database and
* check for consistency.
*/
private void handleEndOfStream(State state) throws StopRequestException {
if (state.shouldPause) {
updateStatusAndPause(state);
return;
}
downloadsRepository.updateDownloadEndOfStream(originalDownloadInfo, state.currentBytes, state.contentLength);
final boolean lengthMismatched = (state.contentLength != UNKNOWN_BYTE_SIZE) && (state.currentBytes != state.contentLength);
if (lengthMismatched) {
if (cannotResume(state)) {
throw new StopRequestException(DownloadStatus.CANNOT_RESUME, "mismatched content length; unable to resume");
} else {
throw new StopRequestException(HTTP_DATA_ERROR, "closed socket before end of file");
}
}
}
private void updateStatusAndPause(State state) throws StopRequestException {
downloadsRepository.pauseDownloadWithSize(originalDownloadInfo, state.currentBytes, state.totalBytes);
throw new StopRequestException(DownloadStatus.PAUSED_BY_APP, "download paused by owner");
}
private boolean cannotResume(State state) {
return (state.currentBytes > 0 && !originalDownloadInfo.isResumable() || DownloadDrmHelper.isDrmConvertNeeded(state.mimeType));
}
/**
* Prepare target file based on given network response. Derives filename and
* target size as needed.
*/
private void processResponseHeaders(State state, HttpURLConnection conn) throws StopRequestException {
// TODO: fallocate the entire file if header gave us specific length
readResponseHeaders(state, conn);
state.filename = Helpers.generateSaveFile(
originalDownloadInfo.getUri(),
originalDownloadInfo.getHint(),
state.contentDisposition,
state.contentLocation,
state.mimeType,
originalDownloadInfo.getDestination(),
state.contentLength,
storageManager);
updateDownloadInfoFieldsFrom(state);
downloadsRepository.updateDatabaseFromHeaders(originalDownloadInfo, state.filename, state.headerETag, state.mimeType, state.totalBytes);
// check connectivity again now that we know the total size
checkConnectivity();
}
private void updateDownloadInfoFieldsFrom(State state) {
originalDownloadInfo.setETag(state.headerETag);
originalDownloadInfo.setMimeType(state.mimeType);
}
/**
* Read headers from the HTTP response and store them into local state.
*/
private void readResponseHeaders(State state, HttpURLConnection conn) throws StopRequestException {
state.contentDisposition = conn.getHeaderField("Content-Disposition");
state.contentLocation = conn.getHeaderField("Content-Location");
if (state.mimeType == null) {
state.mimeType = normalizeMimeType(conn.getContentType());
}
state.headerETag = conn.getHeaderField("ETag");
final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
if (transferEncoding == null) {
state.contentLength = getHeaderFieldLong(conn, "Content-Length", UNKNOWN_BYTE_SIZE);
} else {
LLog.i("Ignoring Content-Length since Transfer-Encoding is also defined");
state.contentLength = UNKNOWN_BYTE_SIZE;
}
state.totalBytes = state.contentLength;
final boolean noSizeInfo = state.contentLength == UNKNOWN_BYTE_SIZE && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
if (!originalDownloadInfo.isNoIntegrity() && noSizeInfo) {
throw new StopRequestException(DownloadStatus.CANNOT_RESUME, "can't know size of download, giving up");
}
}
private void parseRetryAfterHeaders(State state, HttpURLConnection conn) {
state.retryAfter = conn.getHeaderFieldInt("Retry-After", -1);
if (state.retryAfter < 0) {
state.retryAfter = 0;
} else {
if (state.retryAfter < Constants.MIN_RETRY_AFTER) {
state.retryAfter = Constants.MIN_RETRY_AFTER;
} else if (state.retryAfter > Constants.MAX_RETRY_AFTER) {
state.retryAfter = Constants.MAX_RETRY_AFTER;
}
state.retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
state.retryAfter *= 1000;
}
}
/**
* Prepare the destination file to receive data. If the file already exists, we'll set up
* appropriately for resumption.
*/
private void setupDestinationFile(State state) throws StopRequestException {
if (TextUtils.isEmpty(state.filename)) {
// only true if we've already run a thread for this download
return;
}
LLog.i("have run thread before for id: " + originalDownloadInfo.getId() + ", and state.filename: " + state.filename);
if (!Helpers.isFilenameValid(state.filename, storageManager.getDownloadDataDirectory())) {
LLog.d("Yeah we know we are bad for downloading to internal storage");
// throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR, "found invalid internal destination filename");
}
// We're resuming a download that got interrupted
File destinationFile = new File(state.filename);
if (destinationFile.exists()) {
LLog.i("resuming download for id: " + originalDownloadInfo.getId() + ", and state.filename: " + state.filename);
long fileLength = destinationFile.length();
if (fileLength == 0) {
// The download hadn't actually started, we can restart from scratch
LLog.d("setupDestinationFile() found fileLength=0, deleting " + state.filename);
destinationFile.delete();
state.filename = null;
LLog.i("resuming download for id: " + originalDownloadInfo.getId() + ", BUT starting from scratch again: ");
} else if (!originalDownloadInfo.isResumable()) {
// This should've been caught upon failure
LLog.d("setupDestinationFile() unable to resume download, deleting " + state.filename);
destinationFile.delete();
throw new StopRequestException(DownloadStatus.CANNOT_RESUME, "Trying to resume a download that can't be resumed");
} else {
// All right, we'll be able to resume this download
LLog.i("resuming download for id: " + originalDownloadInfo.getId() + ", and starting with file of length: " + fileLength);
state.currentBytes = (int) fileLength;
if (originalDownloadInfo.getTotalBytes() != UNKNOWN_BYTE_SIZE) {
state.contentLength = originalDownloadInfo.getTotalBytes();
}
state.headerETag = originalDownloadInfo.getETag();
state.continuingDownload = true;
LLog.i("resuming download for id: " + originalDownloadInfo.getId() + ", state.currentBytes: " + state.currentBytes + ", and setting continuingDownload to true: ");
}
}
}
/**
* Add custom headers for this download to the HTTP request.
*/
private void addRequestHeaders(State state, HttpURLConnection conn) {
for (Pair<String, String> header : originalDownloadInfo.getHeaders()) {
conn.addRequestProperty(header.first, header.second);
}
// Only splice in user agent when not already defined
if (conn.getRequestProperty("User-Agent") == null) {
conn.addRequestProperty("User-Agent", userAgent());
}
// Defeat transparent gzip compression, since it doesn't allow us to
// easily resume partial downloads.
conn.setRequestProperty("Accept-Encoding", "identity");
if (state.continuingDownload) {
if (state.headerETag != null) {
conn.addRequestProperty("If-Match", state.headerETag);
}
conn.addRequestProperty("Range", "bytes=" + state.currentBytes + "-");
}
}
/**
* Stores information about the completed download, and notifies the initiating application.
*/
private void notifyDownloadCompleted(State state, int finalStatus, String errorMsg, int numFailed) {
notifyThroughDatabase(state, finalStatus, errorMsg, numFailed);
if (DownloadStatus.isCompleted(finalStatus)) {
broadcastIntentDownloadComplete(finalStatus);
} else if (DownloadStatus.isInsufficientSpace(finalStatus)) {
broadcastIntentDownloadFailedInsufficientSpace();
}
}
public void broadcastIntentDownloadComplete(int finalStatus) {
Intent intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
intent.setPackage(getPackageName());
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, originalDownloadInfo.getId());
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_STATUS, finalStatus);
intent.setData(originalDownloadInfo.getMyDownloadsUri());
if (originalDownloadInfo.getExtras() != null) {
intent.putExtra(DownloadManager.EXTRA_EXTRA, originalDownloadInfo.getExtras());
}
context.sendBroadcast(intent);
}
public void broadcastIntentDownloadFailedInsufficientSpace() {
Intent intent = new Intent(DownloadManager.ACTION_DOWNLOAD_INSUFFICIENT_SPACE);
intent.setPackage(getPackageName());
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, originalDownloadInfo.getId());
intent.setData(originalDownloadInfo.getMyDownloadsUri());
if (originalDownloadInfo.getExtras() != null) {
intent.putExtra(DownloadManager.EXTRA_EXTRA, originalDownloadInfo.getExtras());
}
context.sendBroadcast(intent);
}
private String getPackageName() {
return context.getApplicationContext().getPackageName();
}
private void notifyThroughDatabase(State state, int finalStatus, String errorMsg, int numFailed) {
downloadsRepository.updateDownload(originalDownloadInfo, state.filename,
state.mimeType, state.retryAfter, state.requestUri, finalStatus, errorMsg, numFailed);
updateBatchStatus(originalDownloadInfo.getBatchId(), originalDownloadInfo.getId());
}
private ContentResolver getContentResolver() {
return context.getContentResolver();
}
private void updateBatchStatus(long batchId, long downloadId) {
int batchStatus = batchRepository.calculateBatchStatus(batchId);
batchRepository.updateBatchStatus(batchId, batchStatus);
if (DownloadStatus.isCancelled(batchStatus)) {
batchRepository.setBatchItemsCancelled(batchId);
} else if (DownloadStatus.isFailure(batchStatus)) {
batchRepository.setBatchItemsFailed(batchId, downloadId);
batchInformationBroadcaster.notifyBatchFailedFor(batchId);
} else if (DownloadStatus.isSuccess(batchStatus)) {
batchInformationBroadcaster.notifyBatchCompletedFor(batchId);
}
}
private static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
try {
return Long.parseLong(conn.getHeaderField(field));
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* Return if given status is eligible to be treated as
* {@link DownloadStatus#WAITING_TO_RETRY}.
*/
private static boolean isStatusRetryable(int status) {
switch (status) {
case HTTP_DATA_ERROR:
case HTTP_UNAVAILABLE:
case HTTP_INTERNAL_ERROR:
case QUEUED_DUE_CLIENT_RESTRICTIONS:
return true;
default:
return false;
}
}
private static void closeQuietly(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException ioe) {
// ignore
}
}
private void setPermissions(String fileName, int mode, int uid, int gid) {
try {
Class<?> fileUtils = Class.forName("android.os.FileUtils");
Method setPermissions = fileUtils.getMethod("setPermissions", String.class, int.class, int.class, int.class);
setPermissions.invoke(null, fileName, mode, uid, gid);
} catch (Exception e) {
LLog.e("Failed to set permissions. Unknown future behaviour.");
}
}
}