blob: 93f8d650b918f6bb85eb07d7d4ec2861dc71472b [file] [log] [blame]
/*
* 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.android.providers.downloads;
import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST;
import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME;
import static android.provider.Downloads.Impl.STATUS_FILE_ERROR;
import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR;
import static android.provider.Downloads.Impl.STATUS_SUCCESS;
import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static com.android.providers.downloads.Constants.TAG;
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_PARTIAL;
import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.drm.DrmManagerClient;
import android.drm.DrmOutputStream;
import android.net.ConnectivityManager;
import android.net.INetworkPolicyListener;
import android.net.NetworkInfo;
import android.net.NetworkPolicyManager;
import android.net.TrafficStats;
import android.os.FileUtils;
import android.os.PowerManager;
import android.os.Process;
import android.os.SystemClock;
import android.os.WorkSource;
import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.android.providers.downloads.DownloadInfo.NetworkState;
import libcore.io.IoUtils;
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.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
/**
* Task which executes a given {@link DownloadInfo}: making network requests,
* persisting data to disk, and updating {@link DownloadProvider}.
*/
public class DownloadThread implements Runnable {
// 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 mContext;
private final DownloadInfo mInfo;
private final SystemFacade mSystemFacade;
private final StorageManager mStorageManager;
private final DownloadNotifier mNotifier;
private volatile boolean mPolicyDirty;
public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
StorageManager storageManager, DownloadNotifier notifier) {
mContext = context;
mSystemFacade = systemFacade;
mInfo = info;
mStorageManager = storageManager;
mNotifier = notifier;
}
/**
* Returns the user agent provided by the initiating app, or use the default one
*/
private String userAgent() {
String userAgent = mInfo.mUserAgent;
if (userAgent == null) {
userAgent = Constants.DEFAULT_USER_AGENT;
}
return userAgent;
}
/**
* State for the entire run() method.
*/
static class State {
public String mFilename;
public String mMimeType;
public int mRetryAfter = 0;
public boolean mGotData = false;
public String mRequestUri;
public long mTotalBytes = -1;
public long mCurrentBytes = 0;
public String mHeaderETag;
public boolean mContinuingDownload = false;
public long mBytesNotified = 0;
public long mTimeLastNotification = 0;
public int mNetworkType = ConnectivityManager.TYPE_NONE;
/** Historical bytes/second speed of this download. */
public long mSpeed;
/** Time when current sample started. */
public long mSpeedSampleStart;
/** Bytes transferred since current sample started. */
public long mSpeedSampleBytes;
public long mContentLength = -1;
public String mContentDisposition;
public String mContentLocation;
public int mRedirectionCount;
public URL mUrl;
public State(DownloadInfo info) {
mMimeType = Intent.normalizeMimeType(info.mMimeType);
mRequestUri = info.mUri;
mFilename = info.mFileName;
mTotalBytes = info.mTotalBytes;
mCurrentBytes = info.mCurrentBytes;
}
public void resetBeforeExecute() {
// Reset any state from previous execution
mContentLength = -1;
mContentDisposition = null;
mContentLocation = null;
mRedirectionCount = 0;
}
}
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
runInternal();
} finally {
mNotifier.notifyDownloadSpeed(mInfo.mId, 0);
}
}
private void runInternal() {
// Skip when download already marked as finished; this download was
// probably started again while racing with UpdateThread.
if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId)
== Downloads.Impl.STATUS_SUCCESS) {
Log.d(TAG, "Download " + mInfo.mId + " already finished; skipping");
return;
}
State state = new State(mInfo);
PowerManager.WakeLock wakeLock = null;
int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
int numFailed = mInfo.mNumFailed;
String errorMsg = null;
final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
try {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
wakeLock.setWorkSource(new WorkSource(mInfo.mUid));
wakeLock.acquire();
// while performing download, register for rules updates
netPolicy.registerListener(mPolicyListener);
Log.i(Constants.TAG, "Download " + mInfo.mId + " starting");
// Remember which network this download started on; used to
// determine if errors were due to network changes.
final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
if (info != null) {
state.mNetworkType = info.getType();
}
// Network traffic on this thread should be counted against the
// requesting UID, and is tagged with well-known value.
TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
TrafficStats.setThreadStatsUid(mInfo.mUid);
try {
// TODO: migrate URL sanity checking into client side of API
state.mUrl = new URL(state.mRequestUri);
} catch (MalformedURLException e) {
throw new StopRequestException(STATUS_BAD_REQUEST, e);
}
executeDownload(state);
finalizeDestinationFile(state);
finalStatus = Downloads.Impl.STATUS_SUCCESS;
} catch (StopRequestException error) {
// remove the cause before printing, in case it contains PII
errorMsg = error.getMessage();
String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
Log.w(Constants.TAG, msg);
if (Constants.LOGV) {
Log.w(Constants.TAG, msg, error);
}
finalStatus = error.getFinalStatus();
// Nobody below our level should request retries, since we handle
// failure counts at this level.
if (finalStatus == STATUS_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.mGotData) {
numFailed = 1;
} else {
numFailed += 1;
}
if (numFailed < Constants.MAX_RETRIES) {
final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
if (info != null && info.getType() == state.mNetworkType
&& info.isConnected()) {
// Underlying network is still intact, use normal backoff
finalStatus = STATUS_WAITING_TO_RETRY;
} else {
// Network changed, retry on any next available
finalStatus = STATUS_WAITING_FOR_NETWORK;
}
}
}
// fall through to finally block
} catch (Throwable ex) {
errorMsg = ex.getMessage();
String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
Log.w(Constants.TAG, msg, ex);
finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
// falls through to the code that reports an error
} finally {
if (finalStatus == STATUS_SUCCESS) {
TrafficStats.incrementOperationCount(1);
}
TrafficStats.clearThreadStatsTag();
TrafficStats.clearThreadStatsUid();
cleanupDestination(state, finalStatus);
notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed);
Log.i(Constants.TAG, "Download " + mInfo.mId + " finished with status "
+ Downloads.Impl.statusToString(finalStatus));
netPolicy.unregisterListener(mPolicyListener);
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
}
}
mStorageManager.incrementNumDownloadsSoFar();
}
/**
* 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);
// skip when already finished; remove after fixing race in 5217390
if (state.mCurrentBytes == state.mTotalBytes) {
Log.i(Constants.TAG, "Skipping initiating request for download " +
mInfo.mId + "; already completed");
return;
}
while (state.mRedirectionCount++ < 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.mUrl.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.mContinuingDownload) {
throw new StopRequestException(
STATUS_CANNOT_RESUME, "Expected partial, but received OK");
}
processResponseHeaders(state, conn);
transferData(state, conn);
return;
case HTTP_PARTIAL:
if (!state.mContinuingDownload) {
throw new StopRequestException(
STATUS_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.mUrl = new URL(state.mUrl, location);
if (responseCode == HTTP_MOVED_PERM) {
// Push updated URL back to database
state.mRequestUri = state.mUrl.toString();
}
continue;
case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
throw new StopRequestException(
STATUS_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 (IOException e) {
// Trouble with low-level sockets
throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
} finally {
if (conn != null) conn.disconnect();
}
}
throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
}
/**
* 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(STATUS_HTTP_DATA_ERROR, e);
}
try {
if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
drmClient = new DrmManagerClient(mContext);
final RandomAccessFile file = new RandomAccessFile(
new File(state.mFilename), "rw");
out = new DrmOutputStream(drmClient, file, state.mMimeType);
outFd = file.getFD();
} else {
out = new FileOutputStream(state.mFilename, true);
outFd = ((FileOutputStream) out).getFD();
}
} catch (IOException e) {
throw new StopRequestException(STATUS_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);
}
} finally {
if (drmClient != null) {
drmClient.release();
}
IoUtils.closeQuietly(in);
try {
if (out != null) out.flush();
if (outFd != null) outFd.sync();
} catch (IOException e) {
} finally {
IoUtils.closeQuietly(out);
}
}
}
/**
* Check if current connectivity is valid for this request.
*/
private void checkConnectivity() throws StopRequestException {
// checking connectivity will apply current policy
mPolicyDirty = false;
final NetworkState networkUsable = mInfo.checkCanUseNetwork();
if (networkUsable != NetworkState.OK) {
int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) {
status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
mInfo.notifyPauseDueToSize(true);
} else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
mInfo.notifyPauseDueToSize(false);
}
throw new StopRequestException(status, networkUsable.name());
}
}
/**
* 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 {
final byte data[] = new byte[Constants.BUFFER_SIZE];
for (;;) {
int bytesRead = readFromResponse(state, data, in);
if (bytesRead == -1) { // success, end of stream already reached
handleEndOfStream(state);
return;
}
state.mGotData = true;
writeDataToDestination(state, data, bytesRead, out);
state.mCurrentBytes += bytesRead;
reportProgress(state);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
+ mInfo.mUri);
}
checkPausedOrCanceled(state);
}
}
/**
* Called after a successful completion to take any necessary action on the downloaded file.
*/
private void finalizeDestinationFile(State state) {
if (state.mFilename != null) {
// make sure the file is readable
FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
}
}
/**
* 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.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
if (Constants.LOGVV) {
Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
}
new File(state.mFilename).delete();
state.mFilename = null;
}
}
/**
* Check if the download has been paused or canceled, stopping the request appropriately if it
* has been.
*/
private void checkPausedOrCanceled(State state) throws StopRequestException {
synchronized (mInfo) {
if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
throw new StopRequestException(
Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
}
if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED || mInfo.mDeleted) {
throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
}
}
// if policy has been changed, trigger connectivity check
if (mPolicyDirty) {
checkConnectivity();
}
}
/**
* Report download progress through the database if necessary.
*/
private void reportProgress(State state) {
final long now = SystemClock.elapsedRealtime();
final long sampleDelta = now - state.mSpeedSampleStart;
if (sampleDelta > 500) {
final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000)
/ sampleDelta;
if (state.mSpeed == 0) {
state.mSpeed = sampleSpeed;
} else {
state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4;
}
// Only notify once we have a full sample window
if (state.mSpeedSampleStart != 0) {
mNotifier.notifyDownloadSpeed(mInfo.mId, state.mSpeed);
}
state.mSpeedSampleStart = now;
state.mSpeedSampleBytes = state.mCurrentBytes;
}
if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
state.mBytesNotified = state.mCurrentBytes;
state.mTimeLastNotification = now;
}
}
/**
* Write a data buffer to the destination file.
* @param data buffer containing the data to write
* @param bytesRead how many bytes to write from the buffer
*/
private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out)
throws StopRequestException {
mStorageManager.verifySpaceBeforeWritingToFile(
mInfo.mDestination, state.mFilename, bytesRead);
boolean forceVerified = false;
while (true) {
try {
out.write(data, 0, bytesRead);
return;
} catch (IOException ex) {
// TODO: better differentiate between DRM and disk failures
if (!forceVerified) {
// couldn't write to file. are we out of space? check.
mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
forceVerified = true;
} else {
throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
"Failed to write data: " + ex);
}
}
}
}
/**
* 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 {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
if (state.mContentLength == -1) {
values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
}
mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
final boolean lengthMismatched = (state.mContentLength != -1)
&& (state.mCurrentBytes != state.mContentLength);
if (lengthMismatched) {
if (cannotResume(state)) {
throw new StopRequestException(STATUS_CANNOT_RESUME,
"mismatched content length; unable to resume");
} else {
throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
"closed socket before end of file");
}
}
}
private boolean cannotResume(State state) {
return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null)
|| DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType);
}
/**
* Read some data from the HTTP response stream, handling I/O errors.
* @param data buffer to use to read data
* @param entityStream stream for reading the HTTP response entity
* @return the number of bytes actually read or -1 if the end of the stream has been reached
*/
private int readFromResponse(State state, byte[] data, InputStream entityStream)
throws StopRequestException {
try {
return entityStream.read(data);
} catch (IOException ex) {
// TODO: handle stream errors the same as other retries
if ("unexpected end of stream".equals(ex.getMessage())) {
return -1;
}
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
if (cannotResume(state)) {
throw new StopRequestException(STATUS_CANNOT_RESUME,
"Failed reading response: " + ex + "; unable to resume", ex);
} else {
throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
"Failed reading response: " + ex, ex);
}
}
}
/**
* 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.mFilename = Helpers.generateSaveFile(
mContext,
mInfo.mUri,
mInfo.mHint,
state.mContentDisposition,
state.mContentLocation,
state.mMimeType,
mInfo.mDestination,
state.mContentLength,
mStorageManager);
updateDatabaseFromHeaders(state);
// check connectivity again now that we know the total size
checkConnectivity();
}
/**
* Update necessary database fields based on values of HTTP response headers that have been
* read.
*/
private void updateDatabaseFromHeaders(State state) {
ContentValues values = new ContentValues();
values.put(Downloads.Impl._DATA, state.mFilename);
if (state.mHeaderETag != null) {
values.put(Constants.ETAG, state.mHeaderETag);
}
if (state.mMimeType != null) {
values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
}
values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
}
/**
* Read headers from the HTTP response and store them into local state.
*/
private void readResponseHeaders(State state, HttpURLConnection conn)
throws StopRequestException {
state.mContentDisposition = conn.getHeaderField("Content-Disposition");
state.mContentLocation = conn.getHeaderField("Content-Location");
if (state.mMimeType == null) {
state.mMimeType = Intent.normalizeMimeType(conn.getContentType());
}
state.mHeaderETag = conn.getHeaderField("ETag");
final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
if (transferEncoding == null) {
state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1);
} else {
Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined");
state.mContentLength = -1;
}
state.mTotalBytes = state.mContentLength;
mInfo.mTotalBytes = state.mContentLength;
final boolean noSizeInfo = state.mContentLength == -1
&& (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
if (!mInfo.mNoIntegrity && noSizeInfo) {
throw new StopRequestException(STATUS_CANNOT_RESUME,
"can't know size of download, giving up");
}
}
private void parseRetryAfterHeaders(State state, HttpURLConnection conn) {
state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1);
if (state.mRetryAfter < 0) {
state.mRetryAfter = 0;
} else {
if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
state.mRetryAfter = Constants.MIN_RETRY_AFTER;
} else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
state.mRetryAfter = Constants.MAX_RETRY_AFTER;
}
state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
state.mRetryAfter *= 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.mFilename)) { // only true if we've already run a thread for this download
if (Constants.LOGV) {
Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
", and state.mFilename: " + state.mFilename);
}
if (!Helpers.isFilenameValid(state.mFilename,
mStorageManager.getDownloadDataDirectory())) {
// this should never happen
throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
"found invalid internal destination filename");
}
// We're resuming a download that got interrupted
File f = new File(state.mFilename);
if (f.exists()) {
if (Constants.LOGV) {
Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
", and state.mFilename: " + state.mFilename);
}
long fileLength = f.length();
if (fileLength == 0) {
// The download hadn't actually started, we can restart from scratch
if (Constants.LOGVV) {
Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
+ state.mFilename);
}
f.delete();
state.mFilename = null;
if (Constants.LOGV) {
Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
", BUT starting from scratch again: ");
}
} else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
// This should've been caught upon failure
if (Constants.LOGVV) {
Log.d(TAG, "setupDestinationFile() unable to resume download, deleting "
+ state.mFilename);
}
f.delete();
throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
"Trying to resume a download that can't be resumed");
} else {
// All right, we'll be able to resume this download
if (Constants.LOGV) {
Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
", and starting with file of length: " + fileLength);
}
state.mCurrentBytes = (int) fileLength;
if (mInfo.mTotalBytes != -1) {
state.mContentLength = mInfo.mTotalBytes;
}
state.mHeaderETag = mInfo.mETag;
state.mContinuingDownload = true;
if (Constants.LOGV) {
Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
", state.mCurrentBytes: " + state.mCurrentBytes +
", and setting mContinuingDownload to true: ");
}
}
}
}
}
/**
* Add custom headers for this download to the HTTP request.
*/
private void addRequestHeaders(State state, HttpURLConnection conn) {
for (Pair<String, String> header : mInfo.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.mContinuingDownload) {
if (state.mHeaderETag != null) {
conn.addRequestProperty("If-Match", state.mHeaderETag);
}
conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
}
}
/**
* 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 (Downloads.Impl.isStatusCompleted(finalStatus)) {
mInfo.sendIntentIfRequested();
}
}
private void notifyThroughDatabase(
State state, int finalStatus, String errorMsg, int numFailed) {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_STATUS, finalStatus);
values.put(Downloads.Impl._DATA, state.mFilename);
values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, numFailed);
values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, state.mRetryAfter);
if (!TextUtils.equals(mInfo.mUri, state.mRequestUri)) {
values.put(Downloads.Impl.COLUMN_URI, state.mRequestUri);
}
// save the error message. could be useful to developers.
if (!TextUtils.isEmpty(errorMsg)) {
values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
}
mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
}
private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
@Override
public void onUidRulesChanged(int uid, int uidRules) {
// caller is NPMS, since we only register with them
if (uid == mInfo.mUid) {
mPolicyDirty = true;
}
}
@Override
public void onMeteredIfacesChanged(String[] meteredIfaces) {
// caller is NPMS, since we only register with them
mPolicyDirty = true;
}
@Override
public void onRestrictBackgroundChanged(boolean restrictBackground) {
// caller is NPMS, since we only register with them
mPolicyDirty = true;
}
};
public 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 android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}.
*/
public static boolean isStatusRetryable(int status) {
switch (status) {
case STATUS_HTTP_DATA_ERROR:
case HTTP_UNAVAILABLE:
case HTTP_INTERNAL_ERROR:
return true;
default:
return false;
}
}
}