summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads/DownloadThread.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers/downloads/DownloadThread.java')
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java228
1 files changed, 145 insertions, 83 deletions
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index 325b4eee..40194038 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -16,11 +16,18 @@
package com.android.providers.downloads;
+import static android.provider.Downloads.Impl.COLUMN_CONTROL;
+import static android.provider.Downloads.Impl.COLUMN_DELETED;
+import static android.provider.Downloads.Impl.COLUMN_STATUS;
+import static android.provider.Downloads.Impl.CONTROL_PAUSED;
import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST;
import static android.provider.Downloads.Impl.STATUS_CANCELED;
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_PAUSED_BY_APP;
+import static android.provider.Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
+import static android.provider.Downloads.Impl.STATUS_RUNNING;
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_UNHANDLED_HTTP_CODE;
@@ -28,7 +35,9 @@ import static android.provider.Downloads.Impl.STATUS_UNKNOWN_ERROR;
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;
@@ -38,6 +47,8 @@ import static java.net.HttpURLConnection.HTTP_PRECON_FAILED;
import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -45,24 +56,22 @@ import android.drm.DrmManagerClient;
import android.drm.DrmOutputStream;
import android.net.ConnectivityManager;
import android.net.INetworkPolicyListener;
+import android.net.Network;
import android.net.NetworkInfo;
import android.net.NetworkPolicyManager;
import android.net.TrafficStats;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
-import android.os.PowerManager;
import android.os.Process;
import android.os.SystemClock;
-import android.os.WorkSource;
import android.provider.Downloads;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
+import android.util.MathUtils;
import android.util.Pair;
-import com.android.providers.downloads.DownloadInfo.NetworkState;
-
import libcore.io.IoUtils;
import java.io.File;
@@ -76,6 +85,10 @@ import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
+import java.security.GeneralSecurityException;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
/**
* Task which executes a given {@link DownloadInfo}: making network requests,
@@ -88,7 +101,7 @@ import java.net.URLConnection;
* Failed network requests are retried several times before giving up. Local
* disk errors fail immediately and are not retried.
*/
-public class DownloadThread implements Runnable {
+public class DownloadThread extends Thread {
// TODO: bind each download to a specific network interface to avoid state
// checking races once we have ConnectivityManager API
@@ -103,6 +116,10 @@ public class DownloadThread implements Runnable {
private final Context mContext;
private final SystemFacade mSystemFacade;
private final DownloadNotifier mNotifier;
+ private final NetworkPolicyManager mNetworkPolicy;
+
+ private final DownloadJobService mJobService;
+ private final JobParameters mParams;
private final long mId;
@@ -133,6 +150,14 @@ public class DownloadThread implements Runnable {
public String mErrorMsg;
+ private static final String NOT_CANCELED = COLUMN_STATUS + " != '" + STATUS_CANCELED + "'";
+ private static final String NOT_DELETED = COLUMN_DELETED + " == '0'";
+ private static final String NOT_PAUSED = "(" + COLUMN_CONTROL + " IS NULL OR "
+ + COLUMN_CONTROL + " != '" + CONTROL_PAUSED + "')";
+
+ private static final String SELECTION_VALID = NOT_CANCELED + " AND " + NOT_DELETED + " AND "
+ + NOT_PAUSED;
+
public DownloadInfoDelta(DownloadInfo info) {
mUri = info.mUri;
mFileName = info.mFileName;
@@ -178,8 +203,12 @@ public class DownloadThread implements Runnable {
*/
public void writeToDatabaseOrThrow() throws StopRequestException {
if (mContext.getContentResolver().update(mInfo.getAllDownloadsUri(),
- buildContentValues(), Downloads.Impl.COLUMN_DELETED + " == '0'", null) == 0) {
- throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!");
+ buildContentValues(), SELECTION_VALID, null) == 0) {
+ if (mInfo.queryDownloadControl() == CONTROL_PAUSED) {
+ throw new StopRequestException(STATUS_PAUSED_BY_APP, "Download paused!");
+ } else {
+ throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!");
+ }
}
}
}
@@ -196,6 +225,9 @@ public class DownloadThread implements Runnable {
private long mLastUpdateBytes = 0;
private long mLastUpdateTime = 0;
+ private boolean mIgnoreBlocked;
+ private Network mNetwork;
+
private int mNetworkType = ConnectivityManager.TYPE_NONE;
/** Historical bytes/second speed of this download. */
@@ -205,11 +237,17 @@ public class DownloadThread implements Runnable {
/** Bytes transferred since current sample started. */
private long mSpeedSampleBytes;
- public DownloadThread(Context context, SystemFacade systemFacade, DownloadNotifier notifier,
- DownloadInfo info) {
- mContext = context;
- mSystemFacade = systemFacade;
- mNotifier = notifier;
+ /** Flag indicating that thread must be halted */
+ private volatile boolean mShutdownRequested;
+
+ public DownloadThread(DownloadJobService service, JobParameters params, DownloadInfo info) {
+ mContext = service;
+ mSystemFacade = Helpers.getSystemFacade(mContext);
+ mNotifier = Helpers.getDownloadNotifier(mContext);
+ mNetworkPolicy = mContext.getSystemService(NetworkPolicyManager.class);
+
+ mJobService = service;
+ mParams = params;
mId = info.mId;
mInfo = info;
@@ -222,29 +260,38 @@ public class DownloadThread implements Runnable {
// Skip when download already marked as finished; this download was
// probably started again while racing with UpdateThread.
- if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mId)
- == Downloads.Impl.STATUS_SUCCESS) {
+ if (mInfo.queryDownloadStatus() == Downloads.Impl.STATUS_SUCCESS) {
logDebug("Already finished; skipping");
return;
}
- final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
- PowerManager.WakeLock wakeLock = null;
- 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);
+ mNetworkPolicy.registerListener(mPolicyListener);
logDebug("Starting");
+ mInfoDelta.mStatus = STATUS_RUNNING;
+ mInfoDelta.writeToDatabase();
+
+ // If we're showing a foreground notification for the requesting
+ // app, the download isn't affected by the blocked status of the
+ // requesting app
+ mIgnoreBlocked = mInfo.isVisible();
+
+ // Use the caller's default network to make this connection, since
+ // they might be subject to restrictions that we shouldn't let them
+ // circumvent
+ mNetwork = mSystemFacade.getActiveNetwork(mInfo.mUid, mIgnoreBlocked);
+ if (mNetwork == null) {
+ throw new StopRequestException(STATUS_WAITING_FOR_NETWORK,
+ "No network associated with requesting UID");
+ }
+
// Remember which network this download started on; used to
// determine if errors were due to network changes.
- final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
+ final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid,
+ mIgnoreBlocked);
if (info != null) {
mNetworkType = info.getType();
}
@@ -287,7 +334,8 @@ public class DownloadThread implements Runnable {
}
if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) {
- final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
+ final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid,
+ mIgnoreBlocked);
if (info != null && info.getType() == mNetworkType && info.isConnected()) {
// Underlying network is still intact, use normal backoff
mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY;
@@ -305,6 +353,13 @@ public class DownloadThread implements Runnable {
}
}
+ // If we're waiting for a network that must be unmetered, our status
+ // is actually queued so we show relevant notifications
+ if (mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK
+ && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) {
+ mInfoDelta.mStatus = STATUS_QUEUED_FOR_WIFI;
+ }
+
} catch (Throwable t) {
mInfoDelta.mStatus = STATUS_UNKNOWN_ERROR;
mInfoDelta.mErrorMsg = t.toString();
@@ -320,20 +375,29 @@ public class DownloadThread implements Runnable {
mInfoDelta.writeToDatabase();
- if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) {
- mInfo.sendIntentIfRequested();
- }
-
TrafficStats.clearThreadStatsTag();
TrafficStats.clearThreadStatsUid();
- netPolicy.unregisterListener(mPolicyListener);
+ mNetworkPolicy.unregisterListener(mPolicyListener);
+ }
- if (wakeLock != null) {
- wakeLock.release();
- wakeLock = null;
+ if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) {
+ mInfo.sendIntentIfRequested();
+ if (mInfo.shouldScanFile(mInfoDelta.mStatus)) {
+ DownloadScanner.requestScanBlocking(mContext, mInfo.mId, mInfoDelta.mFileName,
+ mInfoDelta.mMimeType);
}
+ } else if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY
+ || mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK
+ || mInfoDelta.mStatus == STATUS_QUEUED_FOR_WIFI) {
+ Helpers.scheduleJob(mContext, DownloadInfo.queryDownloadInfo(mContext, mId));
}
+
+ mJobService.jobFinishedInternal(mParams, false);
+ }
+
+ public void requestShutdown() {
+ mShutdownRequested = true;
}
/**
@@ -352,6 +416,13 @@ public class DownloadThread implements Runnable {
}
boolean cleartextTrafficPermitted = mSystemFacade.isCleartextTrafficPermitted(mInfo.mUid);
+ SSLContext appContext;
+ try {
+ appContext = mSystemFacade.getSSLContextForPackage(mContext, mInfo.mPackage);
+ } catch (GeneralSecurityException e) {
+ // This should never happen.
+ throw new StopRequestException(STATUS_UNKNOWN_ERROR, "Unable to create SSLContext.");
+ }
int redirectionCount = 0;
while (redirectionCount++ < Constants.MAX_REDIRECTS) {
// Enforce the cleartext traffic opt-out for the UID. This cannot be enforced earlier
@@ -366,11 +437,18 @@ public class DownloadThread implements Runnable {
// response with body.
HttpURLConnection conn = null;
try {
+ // Check that the caller is allowed to make network connections. If so, make one on
+ // their behalf to open the url.
checkConnectivity();
- conn = (HttpURLConnection) url.openConnection();
+ conn = (HttpURLConnection) mNetwork.openConnection(url);
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(DEFAULT_TIMEOUT);
conn.setReadTimeout(DEFAULT_TIMEOUT);
+ // If this is going over HTTPS configure the trust to be the same as the calling
+ // package.
+ if (conn instanceof HttpsURLConnection) {
+ ((HttpsURLConnection)conn).setSSLSocketFactory(appContext.getSocketFactory());
+ }
addRequestHeaders(conn, resuming);
@@ -530,7 +608,7 @@ public class DownloadThread implements Runnable {
} finally {
if (drmClient != null) {
- drmClient.release();
+ drmClient.close();
}
IoUtils.closeQuietly(in);
@@ -553,7 +631,12 @@ public class DownloadThread implements Runnable {
throws StopRequestException {
final byte buffer[] = new byte[Constants.BUFFER_SIZE];
while (true) {
- checkPausedOrCanceled();
+ if (mPolicyDirty) checkConnectivity();
+
+ if (mShutdownRequested) {
+ throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
+ "Local halt requested; job probably timed out");
+ }
int len = -1;
try {
@@ -624,12 +707,6 @@ public class DownloadThread implements Runnable {
} else if (Downloads.Impl.isStatusSuccess(mInfoDelta.mStatus)) {
// When success, open access if local file
if (mInfoDelta.mFileName != null) {
- try {
- // TODO: remove this once PackageInstaller works with content://
- Os.chmod(mInfoDelta.mFileName, 0644);
- } catch (ErrnoException ignored) {
- }
-
if (mInfo.mDestination != Downloads.Impl.DESTINATION_FILE_URI) {
try {
// Move into final resting place, if needed
@@ -659,38 +736,16 @@ public class DownloadThread implements Runnable {
// checking connectivity will apply current policy
mPolicyDirty = false;
- final NetworkState networkUsable = mInfo.checkCanUseNetwork(mInfoDelta.mTotalBytes);
- 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());
+ final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid,
+ mIgnoreBlocked);
+ if (info == null || !info.isConnected()) {
+ throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is disconnected");
}
- }
-
- /**
- * Check if the download has been paused or canceled, stopping the request
- * appropriately if it has been.
- */
- private void checkPausedOrCanceled() 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 (info.isRoaming() && !mInfo.isRoamingAllowed()) {
+ throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is roaming");
}
-
- // if policy has been changed, trigger connectivity check
- if (mPolicyDirty) {
- checkConnectivity();
+ if (info.isMetered() && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) {
+ throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is metered");
}
}
@@ -775,17 +830,8 @@ public class DownloadThread implements Runnable {
private void parseUnavailableHeaders(HttpURLConnection conn) {
long retryAfter = conn.getHeaderFieldInt("Retry-After", -1);
- if (retryAfter < 0) {
- retryAfter = 0;
- } else {
- if (retryAfter < Constants.MIN_RETRY_AFTER) {
- retryAfter = Constants.MIN_RETRY_AFTER;
- } else if (retryAfter > Constants.MAX_RETRY_AFTER) {
- retryAfter = Constants.MAX_RETRY_AFTER;
- }
- retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
- }
-
+ retryAfter = MathUtils.constrain(retryAfter, Constants.MIN_RETRY_AFTER,
+ Constants.MAX_RETRY_AFTER);
mInfoDelta.mRetryAfter = (int) (retryAfter * SECOND_IN_MILLIS);
}
@@ -850,6 +896,22 @@ public class DownloadThread implements Runnable {
// caller is NPMS, since we only register with them
mPolicyDirty = true;
}
+
+ @Override
+ public void onRestrictBackgroundWhitelistChanged(int uid, boolean whitelisted) {
+ // caller is NPMS, since we only register with them
+ if (uid == mInfo.mUid) {
+ mPolicyDirty = true;
+ }
+ }
+
+ @Override
+ public void onRestrictBackgroundBlacklistChanged(int uid, boolean blacklisted) {
+ // caller is NPMS, since we only register with them
+ if (uid == mInfo.mUid) {
+ mPolicyDirty = true;
+ }
+ }
};
private static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {