summaryrefslogtreecommitdiffstats
path: root/src/com/android
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android')
-rw-r--r--src/com/android/providers/downloads/Constants.java6
-rw-r--r--src/com/android/providers/downloads/DownloadIdleService.java25
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java396
-rw-r--r--src/com/android/providers/downloads/DownloadJobService.java112
-rw-r--r--src/com/android/providers/downloads/DownloadNotifier.java237
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java133
-rw-r--r--src/com/android/providers/downloads/DownloadReceiver.java117
-rw-r--r--src/com/android/providers/downloads/DownloadScanner.java30
-rw-r--r--src/com/android/providers/downloads/DownloadService.java501
-rw-r--r--src/com/android/providers/downloads/DownloadStorageProvider.java64
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java228
-rw-r--r--src/com/android/providers/downloads/Helpers.java123
-rw-r--r--src/com/android/providers/downloads/OpenHelper.java17
-rw-r--r--src/com/android/providers/downloads/RealSystemFacade.java79
-rw-r--r--src/com/android/providers/downloads/SizeLimitActivity.java137
-rw-r--r--src/com/android/providers/downloads/SystemFacade.java28
16 files changed, 978 insertions, 1255 deletions
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
index 7b8fcd24..79daeaed 100644
--- a/src/com/android/providers/downloads/Constants.java
+++ b/src/com/android/providers/downloads/Constants.java
@@ -45,15 +45,15 @@ public class Constants {
/** The column that is used for the initiating app's UID */
public static final String UID = "uid";
- /** The intent that gets sent when the service must wake up for a retry */
- public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP";
-
/** the intent that gets sent when clicking a successful download */
public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN";
/** the intent that gets sent when clicking an incomplete/failed download */
public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST";
+ /** the intent that gets sent when canceling a download */
+ public static final String ACTION_CANCEL = "android.intent.action.DOWNLOAD_CANCEL";
+
/** the intent that gets sent when deleting the notification of a completed download */
public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE";
diff --git a/src/com/android/providers/downloads/DownloadIdleService.java b/src/com/android/providers/downloads/DownloadIdleService.java
index b5371552..ecebb0f8 100644
--- a/src/com/android/providers/downloads/DownloadIdleService.java
+++ b/src/com/android/providers/downloads/DownloadIdleService.java
@@ -20,10 +20,14 @@ import static com.android.providers.downloads.Constants.TAG;
import static com.android.providers.downloads.StorageUtils.listFilesRecursive;
import android.app.DownloadManager;
+import android.app.job.JobInfo;
import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
import android.app.job.JobService;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
+import android.content.Context;
import android.database.Cursor;
import android.os.Environment;
import android.provider.Downloads;
@@ -33,11 +37,12 @@ import android.text.format.DateUtils;
import android.util.Slog;
import com.android.providers.downloads.StorageUtils.ConcreteFile;
-import com.google.android.collect.Lists;
-import com.google.android.collect.Sets;
import libcore.io.IoUtils;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Sets;
+
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
@@ -48,6 +53,7 @@ import java.util.HashSet;
* deleted directly on disk.
*/
public class DownloadIdleService extends JobService {
+ private static final int IDLE_JOB_ID = -100;
private class IdleRunnable implements Runnable {
private JobParameters mParams;
@@ -66,7 +72,7 @@ public class DownloadIdleService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
- new Thread(new IdleRunnable(params)).start();
+ Helpers.getAsyncHandler().post(new IdleRunnable(params));
return true;
}
@@ -77,6 +83,19 @@ public class DownloadIdleService extends JobService {
return false;
}
+ public static void scheduleIdlePass(Context context) {
+ final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
+ if (scheduler.getPendingJob(IDLE_JOB_ID) == null) {
+ final JobInfo job = new JobInfo.Builder(IDLE_JOB_ID,
+ new ComponentName(context, DownloadIdleService.class))
+ .setPeriodic(12 * DateUtils.HOUR_IN_MILLIS)
+ .setRequiresCharging(true)
+ .setRequiresDeviceIdle(true)
+ .build();
+ scheduler.schedule(job);
+ }
+ }
+
private interface StaleQuery {
final String[] PROJECTION = new String[] {
Downloads.Impl._ID,
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index bee5c4a9..c571de4d 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -16,27 +16,26 @@
package com.android.providers.downloads;
+import static android.provider.Downloads.Impl.VISIBILITY_VISIBLE;
+import static android.provider.Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
+
import static com.android.providers.downloads.Constants.TAG;
import android.app.DownloadManager;
+import android.app.job.JobInfo;
import android.content.ContentResolver;
import android.content.ContentUris;
-import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.NetworkInfo.DetailedState;
import android.net.Uri;
import android.os.Environment;
import android.provider.Downloads;
-import android.provider.Downloads.Impl;
import android.text.TextUtils;
+import android.text.format.DateUtils;
import android.util.Log;
import android.util.Pair;
-import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.IndentingPrintWriter;
import java.io.CharArrayWriter;
@@ -45,9 +44,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
/**
* Details about a specific download. Fields should only be mutated by updating
@@ -66,14 +62,6 @@ public class DownloadInfo {
mCursor = cursor;
}
- public DownloadInfo newDownloadInfo(
- Context context, SystemFacade systemFacade, DownloadNotifier notifier) {
- final DownloadInfo info = new DownloadInfo(context, systemFacade, notifier);
- updateFromDatabase(info);
- readRequestHeaders(info);
- return info;
- }
-
public void updateFromDatabase(DownloadInfo info) {
info.mId = getLong(Downloads.Impl._ID);
info.mUri = getString(Downloads.Impl.COLUMN_URI);
@@ -105,6 +93,7 @@ public class DownloadInfo {
info.mAllowedNetworkTypes = getInt(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
info.mAllowRoaming = getInt(Downloads.Impl.COLUMN_ALLOW_ROAMING) != 0;
info.mAllowMetered = getInt(Downloads.Impl.COLUMN_ALLOW_METERED) != 0;
+ info.mFlags = getInt(Downloads.Impl.COLUMN_FLAGS);
info.mTitle = getString(Downloads.Impl.COLUMN_TITLE);
info.mDescription = getString(Downloads.Impl.COLUMN_DESCRIPTION);
info.mBypassRecommendedSizeLimit =
@@ -115,7 +104,7 @@ public class DownloadInfo {
}
}
- private void readRequestHeaders(DownloadInfo info) {
+ public void readRequestHeaders(DownloadInfo info) {
info.mRequestHeaders.clear();
Uri headerUri = Uri.withAppendedPath(
info.getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT);
@@ -159,56 +148,6 @@ public class DownloadInfo {
}
}
- /**
- * Constants used to indicate network state for a specific download, after
- * applying any requested constraints.
- */
- public enum NetworkState {
- /**
- * The network is usable for the given download.
- */
- OK,
-
- /**
- * There is no network connectivity.
- */
- NO_CONNECTION,
-
- /**
- * The download exceeds the maximum size for this network.
- */
- UNUSABLE_DUE_TO_SIZE,
-
- /**
- * The download exceeds the recommended maximum size for this network,
- * the user must confirm for this download to proceed without WiFi.
- */
- RECOMMENDED_UNUSABLE_DUE_TO_SIZE,
-
- /**
- * The current connection is roaming, and the download can't proceed
- * over a roaming connection.
- */
- CANNOT_USE_ROAMING,
-
- /**
- * The app requesting the download specific that it can't use the
- * current network connection.
- */
- TYPE_DISALLOWED_BY_REQUESTOR,
-
- /**
- * Current network is blocked for requesting application.
- */
- BLOCKED;
- }
-
- /**
- * 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";
-
public long mId;
public String mUri;
@Deprecated
@@ -240,33 +179,35 @@ public class DownloadInfo {
public int mAllowedNetworkTypes;
public boolean mAllowRoaming;
public boolean mAllowMetered;
+ public int mFlags;
public String mTitle;
public String mDescription;
public int mBypassRecommendedSizeLimit;
- public int mFuzz;
-
private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
- /**
- * Result of last {@link DownloadThread} started by
- * {@link #startDownloadIfReady(ExecutorService)}.
- */
- @GuardedBy("this")
- private Future<?> mSubmittedTask;
-
- @GuardedBy("this")
- private DownloadThread mTask;
-
private final Context mContext;
private final SystemFacade mSystemFacade;
- private final DownloadNotifier mNotifier;
- private DownloadInfo(Context context, SystemFacade systemFacade, DownloadNotifier notifier) {
+ public DownloadInfo(Context context) {
mContext = context;
- mSystemFacade = systemFacade;
- mNotifier = notifier;
- mFuzz = Helpers.sRandom.nextInt(1001);
+ mSystemFacade = Helpers.getSystemFacade(context);
+ }
+
+ public static DownloadInfo queryDownloadInfo(Context context, long downloadId) {
+ final ContentResolver resolver = context.getContentResolver();
+ try (Cursor cursor = resolver.query(
+ ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, downloadId),
+ null, null, null, null)) {
+ final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
+ final DownloadInfo info = new DownloadInfo(context);
+ if (cursor.moveToFirst()) {
+ reader.updateFromDatabase(info);
+ reader.readRequestHeaders(info);
+ return info;
+ }
+ }
+ return null;
}
public Collection<Pair<String, String>> getHeaders() {
@@ -309,43 +250,93 @@ public class DownloadInfo {
}
/**
- * Returns the time when a download should be restarted.
+ * Return if this download is visible to the user while running.
+ */
+ public boolean isVisible() {
+ switch (mVisibility) {
+ case VISIBILITY_VISIBLE:
+ case VISIBILITY_VISIBLE_NOTIFY_COMPLETED:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Add random fuzz to the given delay so it's anywhere between 1-1.5x the
+ * requested delay.
+ */
+ private long fuzzDelay(long delay) {
+ return delay + Helpers.sRandom.nextInt((int) (delay / 2));
+ }
+
+ /**
+ * Return minimum latency in milliseconds required before this download is
+ * allowed to start again.
+ *
+ * @see android.app.job.JobInfo.Builder#setMinimumLatency(long)
*/
- public long restartTime(long now) {
- if (mNumFailed == 0) {
- return now;
+ public long getMinimumLatency() {
+ if (mStatus == Downloads.Impl.STATUS_WAITING_TO_RETRY) {
+ final long now = mSystemFacade.currentTimeMillis();
+ final long startAfter;
+ if (mNumFailed == 0) {
+ startAfter = now;
+ } else if (mRetryAfter > 0) {
+ startAfter = mLastMod + fuzzDelay(mRetryAfter);
+ } else {
+ final long delay = (Constants.RETRY_FIRST_DELAY * DateUtils.SECOND_IN_MILLIS
+ * (1 << (mNumFailed - 1)));
+ startAfter = mLastMod + fuzzDelay(delay);
+ }
+ return Math.max(0, startAfter - now);
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Return the network type constraint required by this download.
+ *
+ * @see android.app.job.JobInfo.Builder#setRequiredNetworkType(int)
+ */
+ public int getRequiredNetworkType(long totalBytes) {
+ if (!mAllowMetered) {
+ return JobInfo.NETWORK_TYPE_UNMETERED;
+ }
+ if (mAllowedNetworkTypes == DownloadManager.Request.NETWORK_WIFI) {
+ return JobInfo.NETWORK_TYPE_UNMETERED;
+ }
+ if (totalBytes > mSystemFacade.getMaxBytesOverMobile()) {
+ return JobInfo.NETWORK_TYPE_UNMETERED;
}
- if (mRetryAfter > 0) {
- return mLastMod + mRetryAfter;
+ if (totalBytes > mSystemFacade.getRecommendedMaxBytesOverMobile()
+ && mBypassRecommendedSizeLimit == 0) {
+ return JobInfo.NETWORK_TYPE_UNMETERED;
}
- return mLastMod +
- Constants.RETRY_FIRST_DELAY *
- (1000 + mFuzz) * (1 << (mNumFailed - 1));
+ if (!mAllowRoaming) {
+ return JobInfo.NETWORK_TYPE_NOT_ROAMING;
+ }
+ return JobInfo.NETWORK_TYPE_ANY;
}
/**
- * Returns whether this download should be enqueued.
+ * Returns whether this download is ready to be scheduled.
*/
- private boolean isReadyToDownload() {
+ public boolean isReadyToSchedule() {
if (mControl == Downloads.Impl.CONTROL_PAUSED) {
// the download is paused, so it's not going to start
return false;
}
switch (mStatus) {
- case 0: // status hasn't been initialized yet, this is a new download
- case Downloads.Impl.STATUS_PENDING: // download is explicit marked as ready to start
- case Downloads.Impl.STATUS_RUNNING: // download interrupted (process killed etc) while
- // running, without a chance to update the database
- return true;
-
+ case 0:
+ case Downloads.Impl.STATUS_PENDING:
+ case Downloads.Impl.STATUS_RUNNING:
case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
+ case Downloads.Impl.STATUS_WAITING_TO_RETRY:
case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
- return checkCanUseNetwork(mTotalBytes) == NetworkState.OK;
+ return true;
- case Downloads.Impl.STATUS_WAITING_TO_RETRY:
- // download was waiting for a delayed restart
- final long now = mSystemFacade.currentTimeMillis();
- return restartTime(now) <= now;
case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR:
// is the media mounted?
final Uri uri = Uri.parse(mUri);
@@ -357,11 +348,10 @@ public class DownloadInfo {
Log.w(TAG, "Expected file URI on external storage: " + mUri);
return false;
}
- case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR:
- // avoids repetition of retrying download
+
+ default:
return false;
}
- return false;
}
/**
@@ -378,27 +368,11 @@ public class DownloadInfo {
return false;
}
- /**
- * Returns whether this download is allowed to use the network.
- */
- public NetworkState checkCanUseNetwork(long totalBytes) {
- final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mUid);
- if (info == null || !info.isConnected()) {
- return NetworkState.NO_CONNECTION;
- }
- if (DetailedState.BLOCKED.equals(info.getDetailedState())) {
- return NetworkState.BLOCKED;
- }
- if (mSystemFacade.isNetworkRoaming() && !isRoamingAllowed()) {
- return NetworkState.CANNOT_USE_ROAMING;
- }
- if (mSystemFacade.isActiveNetworkMetered() && !mAllowMetered) {
- return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR;
- }
- return checkIsNetworkTypeAllowed(info.getType(), totalBytes);
+ public boolean isMeteredAllowed(long totalBytes) {
+ return getRequiredNetworkType(totalBytes) != JobInfo.NETWORK_TYPE_UNMETERED;
}
- private boolean isRoamingAllowed() {
+ public boolean isRoamingAllowed() {
if (mIsPublicApi) {
return mAllowRoaming;
} else { // legacy behavior
@@ -406,112 +380,6 @@ public class DownloadInfo {
}
}
- /**
- * Check if this download can proceed over the given network type.
- * @param networkType a constant from ConnectivityManager.TYPE_*.
- * @return one of the NETWORK_* constants
- */
- private NetworkState checkIsNetworkTypeAllowed(int networkType, long totalBytes) {
- if (mIsPublicApi) {
- final int flag = translateNetworkTypeToApiFlag(networkType);
- final boolean allowAllNetworkTypes = mAllowedNetworkTypes == ~0;
- if (!allowAllNetworkTypes && (flag & mAllowedNetworkTypes) == 0) {
- return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR;
- }
- }
- return checkSizeAllowedForNetwork(networkType, totalBytes);
- }
-
- /**
- * Translate a ConnectivityManager.TYPE_* constant to the corresponding
- * DownloadManager.Request.NETWORK_* bit flag.
- */
- private int translateNetworkTypeToApiFlag(int networkType) {
- switch (networkType) {
- case ConnectivityManager.TYPE_MOBILE:
- return DownloadManager.Request.NETWORK_MOBILE;
-
- case ConnectivityManager.TYPE_WIFI:
- return DownloadManager.Request.NETWORK_WIFI;
-
- case ConnectivityManager.TYPE_BLUETOOTH:
- return DownloadManager.Request.NETWORK_BLUETOOTH;
-
- default:
- return 0;
- }
- }
-
- /**
- * Check if the download's size prohibits it from running over the current network.
- * @return one of the NETWORK_* constants
- */
- private NetworkState checkSizeAllowedForNetwork(int networkType, long totalBytes) {
- if (totalBytes <= 0) {
- // we don't know the size yet
- return NetworkState.OK;
- }
-
- if (ConnectivityManager.isNetworkTypeMobile(networkType)) {
- Long maxBytesOverMobile = mSystemFacade.getMaxBytesOverMobile();
- if (maxBytesOverMobile != null && totalBytes > maxBytesOverMobile) {
- return NetworkState.UNUSABLE_DUE_TO_SIZE;
- }
- if (mBypassRecommendedSizeLimit == 0) {
- Long recommendedMaxBytesOverMobile = mSystemFacade
- .getRecommendedMaxBytesOverMobile();
- if (recommendedMaxBytesOverMobile != null
- && totalBytes > recommendedMaxBytesOverMobile) {
- return NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE;
- }
- }
- }
-
- return NetworkState.OK;
- }
-
- /**
- * If download is ready to start, and isn't already pending or executing,
- * create a {@link DownloadThread} and enqueue it into given
- * {@link Executor}.
- *
- * @return If actively downloading.
- */
- public boolean startDownloadIfReady(ExecutorService executor) {
- synchronized (this) {
- final boolean isReady = isReadyToDownload();
- final boolean isActive = mSubmittedTask != null && !mSubmittedTask.isDone();
- if (isReady && !isActive) {
- if (mStatus != Impl.STATUS_RUNNING) {
- mStatus = Impl.STATUS_RUNNING;
- ContentValues values = new ContentValues();
- values.put(Impl.COLUMN_STATUS, mStatus);
- mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
- }
-
- mTask = new DownloadThread(mContext, mSystemFacade, mNotifier, this);
- mSubmittedTask = executor.submit(mTask);
- }
- return isReady;
- }
- }
-
- /**
- * If download is ready to be scanned, enqueue it into the given
- * {@link DownloadScanner}.
- *
- * @return If actively scanning.
- */
- public boolean startScanIfReady(DownloadScanner scanner) {
- synchronized (this) {
- final boolean isReady = shouldScanFile();
- if (isReady) {
- scanner.requestScan(this);
- }
- return isReady;
- }
- }
-
public boolean isOnCache() {
return (mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION
|| mDestination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION
@@ -571,70 +439,42 @@ public class DownloadInfo {
pw.printPair("mAllowedNetworkTypes", mAllowedNetworkTypes);
pw.printPair("mAllowRoaming", mAllowRoaming);
pw.printPair("mAllowMetered", mAllowMetered);
+ pw.printPair("mFlags", mFlags);
pw.println();
pw.decreaseIndent();
}
/**
- * Return time when this download will be ready for its next action, in
- * milliseconds after given time.
- *
- * @return If {@code 0}, download is ready to proceed immediately. If
- * {@link Long#MAX_VALUE}, then download has no future actions.
- */
- public long nextActionMillis(long now) {
- if (Downloads.Impl.isStatusCompleted(mStatus)) {
- return Long.MAX_VALUE;
- }
- if (mStatus != Downloads.Impl.STATUS_WAITING_TO_RETRY) {
- return 0;
- }
- long when = restartTime(now);
- if (when <= now) {
- return 0;
- }
- return when - now;
- }
-
- /**
* Returns whether a file should be scanned
*/
- public boolean shouldScanFile() {
+ public boolean shouldScanFile(int status) {
return (mMediaScanned == 0)
&& (mDestination == Downloads.Impl.DESTINATION_EXTERNAL ||
mDestination == Downloads.Impl.DESTINATION_FILE_URI ||
mDestination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
- && Downloads.Impl.isStatusSuccess(mStatus);
- }
-
- void notifyPauseDueToSize(boolean isWifiRequired) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(getAllDownloadsUri());
- intent.setClassName(SizeLimitActivity.class.getPackage().getName(),
- SizeLimitActivity.class.getName());
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.putExtra(EXTRA_IS_WIFI_REQUIRED, isWifiRequired);
- mContext.startActivity(intent);
+ && Downloads.Impl.isStatusSuccess(status);
}
/**
* Query and return status of requested download.
*/
- public static int queryDownloadStatus(ContentResolver resolver, long id) {
- final Cursor cursor = resolver.query(
- ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id),
- new String[] { Downloads.Impl.COLUMN_STATUS }, null, null, null);
- try {
+ public int queryDownloadStatus() {
+ return queryDownloadInt(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
+ }
+
+ public int queryDownloadControl() {
+ return queryDownloadInt(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN);
+ }
+
+ public int queryDownloadInt(String columnName, int defaultValue) {
+ try (Cursor cursor = mContext.getContentResolver().query(getAllDownloadsUri(),
+ new String[] { columnName }, null, null, null)) {
if (cursor.moveToFirst()) {
return cursor.getInt(0);
} else {
- // TODO: increase strictness of value returned for unknown
- // downloads; this is safe default for now.
- return Downloads.Impl.STATUS_PENDING;
+ return defaultValue;
}
- } finally {
- cursor.close();
}
}
}
diff --git a/src/com/android/providers/downloads/DownloadJobService.java b/src/com/android/providers/downloads/DownloadJobService.java
new file mode 100644
index 00000000..0ce4266a
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadJobService.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 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.ALL_DOWNLOADS_CONTENT_URI;
+
+import static com.android.providers.downloads.Constants.TAG;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.database.ContentObserver;
+import android.util.Log;
+import android.util.SparseArray;
+
+/**
+ * Service that hosts download jobs. Each active download job is handled as a
+ * unique {@link DownloadThread} instance.
+ * <p>
+ * The majority of downloads should have ETag values to enable resuming, so if a
+ * given download isn't able to finish in the normal job timeout (10 minutes),
+ * we just reschedule the job and resume again in the future.
+ */
+public class DownloadJobService extends JobService {
+ // @GuardedBy("mActiveThreads")
+ private SparseArray<DownloadThread> mActiveThreads = new SparseArray<>();
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // While someone is bound to us, watch for database changes that should
+ // trigger notification updates.
+ getContentResolver().registerContentObserver(ALL_DOWNLOADS_CONTENT_URI, true, mObserver);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ getContentResolver().unregisterContentObserver(mObserver);
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ final int id = params.getJobId();
+
+ // Spin up thread to handle this download
+ final DownloadInfo info = DownloadInfo.queryDownloadInfo(this, id);
+ if (info == null) {
+ Log.w(TAG, "Odd, no details found for download " + id);
+ return false;
+ }
+
+ final DownloadThread thread;
+ synchronized (mActiveThreads) {
+ thread = new DownloadThread(this, params, info);
+ mActiveThreads.put(id, thread);
+ }
+ thread.start();
+
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ final int id = params.getJobId();
+
+ final DownloadThread thread;
+ synchronized (mActiveThreads) {
+ thread = mActiveThreads.removeReturnOld(id);
+ }
+ if (thread != null) {
+ // If the thread is still running, ask it to gracefully shutdown,
+ // and reschedule ourselves to resume in the future.
+ thread.requestShutdown();
+
+ Helpers.scheduleJob(this, DownloadInfo.queryDownloadInfo(this, id));
+ }
+ return false;
+ }
+
+ public void jobFinishedInternal(JobParameters params, boolean needsReschedule) {
+ synchronized (mActiveThreads) {
+ mActiveThreads.remove(params.getJobId());
+ }
+
+ // Update notifications one last time while job is protecting us
+ mObserver.onChange(false);
+
+ jobFinished(params, needsReschedule);
+ }
+
+ private ContentObserver mObserver = new ContentObserver(Helpers.getAsyncHandler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ Helpers.getDownloadNotifier(DownloadJobService.this).update();
+ }
+ };
+}
diff --git a/src/com/android/providers/downloads/DownloadNotifier.java b/src/com/android/providers/downloads/DownloadNotifier.java
index 60c249f9..d5808690 100644
--- a/src/com/android/providers/downloads/DownloadNotifier.java
+++ b/src/com/android/providers/downloads/DownloadNotifier.java
@@ -19,7 +19,9 @@ package com.android.providers.downloads;
import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE;
import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION;
+import static android.provider.Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
import static android.provider.Downloads.Impl.STATUS_RUNNING;
+
import static com.android.providers.downloads.Constants.TAG;
import android.app.DownloadManager;
@@ -30,28 +32,27 @@ import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
+import android.database.Cursor;
import android.net.Uri;
import android.os.SystemClock;
import android.provider.Downloads;
+import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.IntArray;
import android.util.Log;
import android.util.LongSparseLongArray;
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
+import com.android.internal.util.ArrayUtils;
import java.text.NumberFormat;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
import javax.annotation.concurrent.GuardedBy;
/**
- * Update {@link NotificationManager} to reflect current {@link DownloadInfo}
- * states. Collapses similar downloads into a single notification, and builds
+ * Update {@link NotificationManager} to reflect current download states.
+ * Collapses similar downloads into a single notification, and builds
* {@link PendingIntent} that launch towards {@link DownloadReceiver}.
*/
public class DownloadNotifier {
@@ -67,20 +68,20 @@ public class DownloadNotifier {
* Currently active notifications, mapped from clustering tag to timestamp
* when first shown.
*
- * @see #buildNotificationTag(DownloadInfo)
+ * @see #buildNotificationTag(Cursor)
*/
@GuardedBy("mActiveNotifs")
- private final HashMap<String, Long> mActiveNotifs = Maps.newHashMap();
+ private final ArrayMap<String, Long> mActiveNotifs = new ArrayMap<>();
/**
- * Current speed of active downloads, mapped from {@link DownloadInfo#mId}
- * to speed in bytes per second.
+ * Current speed of active downloads, mapped from download ID to speed in
+ * bytes per second.
*/
@GuardedBy("mDownloadSpeed")
private final LongSparseLongArray mDownloadSpeed = new LongSparseLongArray();
/**
- * Last time speed was reproted, mapped from {@link DownloadInfo#mId} to
+ * Last time speed was reproted, mapped from download ID to
* {@link SystemClock#elapsedRealtime()}.
*/
@GuardedBy("mDownloadSpeed")
@@ -92,8 +93,16 @@ public class DownloadNotifier {
Context.NOTIFICATION_SERVICE);
}
- public void cancelAll() {
- mNotifManager.cancelAll();
+ public void init() {
+ synchronized (mActiveNotifs) {
+ mActiveNotifs.clear();
+ final StatusBarNotification[] notifs = mNotifManager.getActiveNotifications();
+ if (!ArrayUtils.isEmpty(notifs)) {
+ for (StatusBarNotification notif : notifs) {
+ mActiveNotifs.put(notif.getTag(), notif.getPostTime());
+ }
+ }
+ }
}
/**
@@ -112,32 +121,62 @@ public class DownloadNotifier {
}
}
- /**
- * Update {@link NotificationManager} to reflect the given set of
- * {@link DownloadInfo}, adding, collapsing, and removing as needed.
- */
- public void updateWith(Collection<DownloadInfo> downloads) {
- synchronized (mActiveNotifs) {
- updateWithLocked(downloads);
+ private interface UpdateQuery {
+ final String[] PROJECTION = new String[] {
+ Downloads.Impl._ID,
+ Downloads.Impl.COLUMN_STATUS,
+ Downloads.Impl.COLUMN_VISIBILITY,
+ Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
+ Downloads.Impl.COLUMN_CURRENT_BYTES,
+ Downloads.Impl.COLUMN_TOTAL_BYTES,
+ Downloads.Impl.COLUMN_DESTINATION,
+ Downloads.Impl.COLUMN_TITLE,
+ Downloads.Impl.COLUMN_DESCRIPTION,
+ };
+
+ final int _ID = 0;
+ final int STATUS = 1;
+ final int VISIBILITY = 2;
+ final int NOTIFICATION_PACKAGE = 3;
+ final int CURRENT_BYTES = 4;
+ final int TOTAL_BYTES = 5;
+ final int DESTINATION = 6;
+ final int TITLE = 7;
+ final int DESCRIPTION = 8;
+ }
+
+ public void update() {
+ try (Cursor cursor = mContext.getContentResolver().query(
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, UpdateQuery.PROJECTION,
+ Downloads.Impl.COLUMN_DELETED + " == '0'", null, null)) {
+ synchronized (mActiveNotifs) {
+ updateWithLocked(cursor);
+ }
}
}
- private void updateWithLocked(Collection<DownloadInfo> downloads) {
+ private void updateWithLocked(Cursor cursor) {
final Resources res = mContext.getResources();
// Cluster downloads together
- final Multimap<String, DownloadInfo> clustered = ArrayListMultimap.create();
- for (DownloadInfo info : downloads) {
- final String tag = buildNotificationTag(info);
+ final ArrayMap<String, IntArray> clustered = new ArrayMap<>();
+ while (cursor.moveToNext()) {
+ final String tag = buildNotificationTag(cursor);
if (tag != null) {
- clustered.put(tag, info);
+ IntArray cluster = clustered.get(tag);
+ if (cluster == null) {
+ cluster = new IntArray();
+ clustered.put(tag, cluster);
+ }
+ cluster.add(cursor.getPosition());
}
}
// Build notification for each cluster
- for (String tag : clustered.keySet()) {
+ for (int i = 0; i < clustered.size(); i++) {
+ final String tag = clustered.keyAt(i);
+ final IntArray cluster = clustered.valueAt(i);
final int type = getNotificationTagType(tag);
- final Collection<DownloadInfo> cluster = clustered.get(tag);
final Notification.Builder builder = new Notification.Builder(mContext);
builder.setColor(res.getColor(
@@ -164,27 +203,46 @@ public class DownloadNotifier {
// Build action intents
if (type == TYPE_ACTIVE || type == TYPE_WAITING) {
+ final long[] downloadIds = getDownloadIds(cursor, cluster);
+
// build a synthetic uri for intent identification purposes
final Uri uri = new Uri.Builder().scheme("active-dl").appendPath(tag).build();
final Intent intent = new Intent(Constants.ACTION_LIST,
uri, mContext, DownloadReceiver.class);
intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
- getDownloadIds(cluster));
+ downloadIds);
builder.setContentIntent(PendingIntent.getBroadcast(mContext,
0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
builder.setOngoing(true);
+ // Add a Cancel action
+ final Uri cancelUri = new Uri.Builder().scheme("cancel-dl").appendPath(tag).build();
+ final Intent cancelIntent = new Intent(Constants.ACTION_CANCEL,
+ cancelUri, mContext, DownloadReceiver.class);
+ cancelIntent.putExtra(DownloadReceiver.EXTRA_CANCELED_DOWNLOAD_IDS, downloadIds);
+ cancelIntent.putExtra(DownloadReceiver.EXTRA_CANCELED_DOWNLOAD_NOTIFICATION_TAG, tag);
+
+ builder.addAction(
+ android.R.drawable.ic_menu_close_clear_cancel,
+ res.getString(R.string.button_cancel_download),
+ PendingIntent.getBroadcast(mContext,
+ 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT));
+
} else if (type == TYPE_COMPLETE) {
- final DownloadInfo info = cluster.iterator().next();
+ cursor.moveToPosition(cluster.get(0));
+ final long id = cursor.getLong(UpdateQuery._ID);
+ final int status = cursor.getInt(UpdateQuery.STATUS);
+ final int destination = cursor.getInt(UpdateQuery.DESTINATION);
+
final Uri uri = ContentUris.withAppendedId(
- Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, info.mId);
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
builder.setAutoCancel(true);
final String action;
- if (Downloads.Impl.isStatusError(info.mStatus)) {
+ if (Downloads.Impl.isStatusError(status)) {
action = Constants.ACTION_LIST;
} else {
- if (info.mDestination != Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
+ if (destination != Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
action = Constants.ACTION_OPEN;
} else {
action = Constants.ACTION_LIST;
@@ -193,7 +251,7 @@ public class DownloadNotifier {
final Intent intent = new Intent(action, uri, mContext, DownloadReceiver.class);
intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
- getDownloadIds(cluster));
+ getDownloadIds(cursor, cluster));
builder.setContentIntent(PendingIntent.getBroadcast(mContext,
0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
@@ -210,11 +268,17 @@ public class DownloadNotifier {
long total = 0;
long speed = 0;
synchronized (mDownloadSpeed) {
- for (DownloadInfo info : cluster) {
- if (info.mTotalBytes != -1) {
- current += info.mCurrentBytes;
- total += info.mTotalBytes;
- speed += mDownloadSpeed.get(info.mId);
+ for (int j = 0; j < cluster.size(); j++) {
+ cursor.moveToPosition(cluster.get(j));
+
+ final long id = cursor.getLong(UpdateQuery._ID);
+ final long currentBytes = cursor.getLong(UpdateQuery.CURRENT_BYTES);
+ final long totalBytes = cursor.getLong(UpdateQuery.TOTAL_BYTES);
+
+ if (totalBytes != -1) {
+ current += currentBytes;
+ total += totalBytes;
+ speed += mDownloadSpeed.get(id);
}
}
}
@@ -239,13 +303,13 @@ public class DownloadNotifier {
// Build titles and description
final Notification notif;
if (cluster.size() == 1) {
- final DownloadInfo info = cluster.iterator().next();
-
- builder.setContentTitle(getDownloadTitle(res, info));
+ cursor.moveToPosition(cluster.get(0));
+ builder.setContentTitle(getDownloadTitle(res, cursor));
if (type == TYPE_ACTIVE) {
- if (!TextUtils.isEmpty(info.mDescription)) {
- builder.setContentText(info.mDescription);
+ final String description = cursor.getString(UpdateQuery.DESCRIPTION);
+ if (!TextUtils.isEmpty(description)) {
+ builder.setContentText(description);
} else {
builder.setContentText(remainingText);
}
@@ -256,9 +320,10 @@ public class DownloadNotifier {
res.getString(R.string.notification_need_wifi_for_size));
} else if (type == TYPE_COMPLETE) {
- if (Downloads.Impl.isStatusError(info.mStatus)) {
+ final int status = cursor.getInt(UpdateQuery.STATUS);
+ if (Downloads.Impl.isStatusError(status)) {
builder.setContentText(res.getText(R.string.notification_download_failed));
- } else if (Downloads.Impl.isStatusSuccess(info.mStatus)) {
+ } else if (Downloads.Impl.isStatusSuccess(status)) {
builder.setContentText(
res.getText(R.string.notification_download_complete));
}
@@ -269,8 +334,9 @@ public class DownloadNotifier {
} else {
final Notification.InboxStyle inboxStyle = new Notification.InboxStyle(builder);
- for (DownloadInfo info : cluster) {
- inboxStyle.addLine(getDownloadTitle(res, info));
+ for (int j = 0; j < cluster.size(); j++) {
+ cursor.moveToPosition(cluster.get(j));
+ inboxStyle.addLine(getDownloadTitle(res, cursor));
}
if (type == TYPE_ACTIVE) {
@@ -296,29 +362,31 @@ public class DownloadNotifier {
}
// Remove stale tags that weren't renewed
- final Iterator<String> it = mActiveNotifs.keySet().iterator();
- while (it.hasNext()) {
- final String tag = it.next();
- if (!clustered.containsKey(tag)) {
+ for (int i = 0; i < mActiveNotifs.size();) {
+ final String tag = mActiveNotifs.keyAt(i);
+ if (clustered.containsKey(tag)) {
+ i++;
+ } else {
mNotifManager.cancel(tag, 0);
- it.remove();
+ mActiveNotifs.removeAt(i);
}
}
}
- private static CharSequence getDownloadTitle(Resources res, DownloadInfo info) {
- if (!TextUtils.isEmpty(info.mTitle)) {
- return info.mTitle;
+ private static CharSequence getDownloadTitle(Resources res, Cursor cursor) {
+ final String title = cursor.getString(UpdateQuery.TITLE);
+ if (!TextUtils.isEmpty(title)) {
+ return title;
} else {
return res.getString(R.string.download_unknown_title);
}
}
- private long[] getDownloadIds(Collection<DownloadInfo> infos) {
- final long[] ids = new long[infos.size()];
- int i = 0;
- for (DownloadInfo info : infos) {
- ids[i++] = info.mId;
+ private long[] getDownloadIds(Cursor cursor, IntArray cluster) {
+ final long[] ids = new long[cluster.size()];
+ for (int i = 0; i < cluster.size(); i++) {
+ cursor.moveToPosition(cluster.get(i));
+ ids[i] = cursor.getLong(UpdateQuery._ID);
}
return ids;
}
@@ -335,17 +403,22 @@ public class DownloadNotifier {
}
/**
- * Build tag used for collapsing several {@link DownloadInfo} into a single
+ * Build tag used for collapsing several downloads into a single
* {@link Notification}.
*/
- private static String buildNotificationTag(DownloadInfo info) {
- if (info.mStatus == Downloads.Impl.STATUS_QUEUED_FOR_WIFI) {
- return TYPE_WAITING + ":" + info.mPackage;
- } else if (isActiveAndVisible(info)) {
- return TYPE_ACTIVE + ":" + info.mPackage;
- } else if (isCompleteAndVisible(info)) {
+ private static String buildNotificationTag(Cursor cursor) {
+ final long id = cursor.getLong(UpdateQuery._ID);
+ final int status = cursor.getInt(UpdateQuery.STATUS);
+ final int visibility = cursor.getInt(UpdateQuery.VISIBILITY);
+ final String notifPackage = cursor.getString(UpdateQuery.NOTIFICATION_PACKAGE);
+
+ if (isQueuedAndVisible(status, visibility)) {
+ return TYPE_WAITING + ":" + notifPackage;
+ } else if (isActiveAndVisible(status, visibility)) {
+ return TYPE_ACTIVE + ":" + notifPackage;
+ } else if (isCompleteAndVisible(status, visibility)) {
// Complete downloads always have unique notifs
- return TYPE_COMPLETE + ":" + info.mId;
+ return TYPE_COMPLETE + ":" + id;
} else {
return null;
}
@@ -353,21 +426,27 @@ public class DownloadNotifier {
/**
* Return the cluster type of the given tag, as created by
- * {@link #buildNotificationTag(DownloadInfo)}.
+ * {@link #buildNotificationTag(Cursor)}.
*/
private static int getNotificationTagType(String tag) {
return Integer.parseInt(tag.substring(0, tag.indexOf(':')));
}
- private static boolean isActiveAndVisible(DownloadInfo download) {
- return download.mStatus == STATUS_RUNNING &&
- (download.mVisibility == VISIBILITY_VISIBLE
- || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+ private static boolean isQueuedAndVisible(int status, int visibility) {
+ return status == STATUS_QUEUED_FOR_WIFI &&
+ (visibility == VISIBILITY_VISIBLE
+ || visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+ }
+
+ private static boolean isActiveAndVisible(int status, int visibility) {
+ return status == STATUS_RUNNING &&
+ (visibility == VISIBILITY_VISIBLE
+ || visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
}
- private static boolean isCompleteAndVisible(DownloadInfo download) {
- return Downloads.Impl.isStatusCompleted(download.mStatus) &&
- (download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED
- || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
+ private static boolean isCompleteAndVisible(int status, int visibility) {
+ return Downloads.Impl.isStatusCompleted(status) &&
+ (visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED
+ || visibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
}
}
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index e79aff5f..d30018f7 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -16,9 +16,18 @@
package com.android.providers.downloads;
+import static android.provider.BaseColumns._ID;
+import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
+import static android.provider.Downloads.Impl.COLUMN_MEDIAPROVIDER_URI;
+import static android.provider.Downloads.Impl.COLUMN_MEDIA_SCANNED;
+import static android.provider.Downloads.Impl.COLUMN_MIME_TYPE;
+import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD;
+import static android.provider.Downloads.Impl._DATA;
+
import android.app.AppOpsManager;
import android.app.DownloadManager;
import android.app.DownloadManager.Request;
+import android.app.job.JobScheduler;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -35,8 +44,6 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.OnCloseListener;
import android.os.Process;
@@ -47,9 +54,10 @@ import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
+import com.android.internal.util.IndentingPrintWriter;
+
import libcore.io.IoUtils;
-import com.android.internal.util.IndentingPrintWriter;
import com.google.android.collect.Maps;
import com.google.common.annotations.VisibleForTesting;
@@ -73,7 +81,7 @@ public final class DownloadProvider extends ContentProvider {
/** Database filename */
private static final String DB_NAME = "downloads.db";
/** Current database version */
- private static final int DB_VERSION = 109;
+ private static final int DB_VERSION = 110;
/** Name of table in the database */
private static final String DB_TABLE = "downloads";
@@ -170,7 +178,8 @@ public final class DownloadProvider extends ContentProvider {
private static final List<String> downloadManagerColumnsList =
Arrays.asList(DownloadManager.UNDERLYING_COLUMNS);
- private Handler mHandler;
+ @VisibleForTesting
+ SystemFacade mSystemFacade;
/** The database that lies underneath this content provider */
private SQLiteOpenHelper mOpenHelper = null;
@@ -179,9 +188,6 @@ public final class DownloadProvider extends ContentProvider {
private int mSystemUid = -1;
private int mDefContainerUid = -1;
- @VisibleForTesting
- SystemFacade mSystemFacade;
-
/**
* This class encapsulates a SQL where clause and its parameters. It makes it possible for
* shared methods (like {@link DownloadProvider#getWhereClause(Uri, String, String[], int)})
@@ -329,6 +335,11 @@ public final class DownloadProvider extends ContentProvider {
"BOOLEAN NOT NULL DEFAULT 0");
break;
+ case 110:
+ addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_FLAGS,
+ "INTEGER NOT NULL DEFAULT 0");
+ break;
+
default:
throw new IllegalStateException("Don't know how to upgrade to " + version);
}
@@ -442,11 +453,6 @@ public final class DownloadProvider extends ContentProvider {
mSystemFacade = new RealSystemFacade(getContext());
}
- HandlerThread handlerThread =
- new HandlerThread("DownloadProvider handler", Process.THREAD_PRIORITY_BACKGROUND);
- handlerThread.start();
- mHandler = new Handler(handlerThread.getLooper());
-
mOpenHelper = new DatabaseHelper(getContext());
// Initialize the system uid
mSystemUid = Process.SYSTEM_UID;
@@ -462,10 +468,6 @@ public final class DownloadProvider extends ContentProvider {
if (appInfo != null) {
mDefContainerUid = appInfo.uid;
}
- // start the DownloadService class. don't wait for the 1st download to be issued.
- // saves us by getting some initialization code in DownloadService out of the way.
- Context context = getContext();
- context.startService(new Intent(context, DownloadService.class));
return true;
}
@@ -669,6 +671,7 @@ public final class DownloadProvider extends ContentProvider {
copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues);
+ copyInteger(Downloads.Impl.COLUMN_FLAGS, values, filteredValues);
}
if (Constants.LOGVV) {
@@ -689,9 +692,18 @@ public final class DownloadProvider extends ContentProvider {
insertRequestHeaders(db, rowID, values);
notifyContentChanged(uri, match);
- // Always start service to handle notifications and/or scanning
- final Context context = getContext();
- context.startService(new Intent(context, DownloadService.class));
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Helpers.scheduleJob(getContext(), rowID);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ if (values.getAsInteger(COLUMN_DESTINATION) == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
+ && values.getAsInteger(COLUMN_MEDIA_SCANNED) == 0) {
+ DownloadScanner.requestScanBlocking(getContext(), rowID, values.getAsString(_DATA),
+ values.getAsString(COLUMN_MIME_TYPE));
+ }
return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
}
@@ -806,6 +818,7 @@ public final class DownloadProvider extends ContentProvider {
values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING);
values.remove(Downloads.Impl.COLUMN_ALLOW_METERED);
+ values.remove(Downloads.Impl.COLUMN_FLAGS);
values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED);
values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE);
@@ -1053,14 +1066,7 @@ public final class DownloadProvider extends ContentProvider {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count;
- boolean startService = false;
-
- if (values.containsKey(Downloads.Impl.COLUMN_DELETED)) {
- if (values.getAsInteger(Downloads.Impl.COLUMN_DELETED) == 1) {
- // some rows are to be 'deleted'. need to start DownloadService.
- startService = true;
- }
- }
+ boolean updateSchedule = false;
ContentValues filteredValues;
if (Binder.getCallingPid() != Process.myPid()) {
@@ -1070,7 +1076,7 @@ public final class DownloadProvider extends ContentProvider {
Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL);
if (i != null) {
filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i);
- startService = true;
+ updateSchedule = true;
}
copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
@@ -1099,7 +1105,7 @@ public final class DownloadProvider extends ContentProvider {
boolean isUserBypassingSizeLimit =
values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
if (isRestart || isUserBypassingSizeLimit) {
- startService = true;
+ updateSchedule = true;
}
}
@@ -1109,12 +1115,27 @@ public final class DownloadProvider extends ContentProvider {
case MY_DOWNLOADS_ID:
case ALL_DOWNLOADS:
case ALL_DOWNLOADS_ID:
- SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
- if (filteredValues.size() > 0) {
- count = db.update(DB_TABLE, filteredValues, selection.getSelection(),
- selection.getParameters());
- } else {
+ if (filteredValues.size() == 0) {
count = 0;
+ break;
+ }
+
+ final SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
+ count = db.update(DB_TABLE, filteredValues, selection.getSelection(),
+ selection.getParameters());
+ if (updateSchedule) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ try (Cursor cursor = db.query(DB_TABLE, new String[] { _ID },
+ selection.getSelection(), selection.getParameters(),
+ null, null, null)) {
+ while (cursor.moveToNext()) {
+ Helpers.scheduleJob(getContext(), cursor.getInt(0));
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
break;
@@ -1124,10 +1145,6 @@ public final class DownloadProvider extends ContentProvider {
}
notifyContentChanged(uri, match);
- if (startService) {
- Context context = getContext();
- context.startService(new Intent(context, DownloadService.class));
- }
return count;
}
@@ -1176,7 +1193,8 @@ public final class DownloadProvider extends ContentProvider {
Helpers.validateSelection(where, sAppReadableColumnsSet);
}
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ final JobScheduler scheduler = getContext().getSystemService(JobScheduler.class);
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count;
int match = sURIMatcher.match(uri);
switch (match) {
@@ -1184,15 +1202,16 @@ public final class DownloadProvider extends ContentProvider {
case MY_DOWNLOADS_ID:
case ALL_DOWNLOADS:
case ALL_DOWNLOADS_ID:
- SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
+ final SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
deleteRequestHeaders(db, selection.getSelection(), selection.getParameters());
- final Cursor cursor = db.query(DB_TABLE, new String[] {
- Downloads.Impl._ID, Downloads.Impl._DATA
- }, selection.getSelection(), selection.getParameters(), null, null, null);
- try {
+ try (Cursor cursor = db.query(DB_TABLE, new String[] {
+ _ID, _DATA, COLUMN_MEDIAPROVIDER_URI
+ }, selection.getSelection(), selection.getParameters(), null, null, null)) {
while (cursor.moveToNext()) {
final long id = cursor.getLong(0);
+ scheduler.cancel((int) id);
+
DownloadStorageProvider.onDownloadProviderDelete(getContext(), id);
final String path = cursor.getString(1);
@@ -1207,9 +1226,18 @@ public final class DownloadProvider extends ContentProvider {
} catch (IOException ignored) {
}
}
+
+ final String mediaUri = cursor.getString(2);
+ if (!TextUtils.isEmpty(mediaUri)) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ getContext().getContentResolver().delete(Uri.parse(mediaUri), null,
+ null);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
}
- } finally {
- IoUtils.closeQuietly(cursor);
}
count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters());
@@ -1270,7 +1298,13 @@ public final class DownloadProvider extends ContentProvider {
throw new FileNotFoundException("No filename found.");
}
- final File file = new File(path);
+ final File file;
+ try {
+ file = new File(path).getCanonicalFile();
+ } catch (IOException e) {
+ throw new FileNotFoundException(e.getMessage());
+ }
+
if (!Helpers.isFilenameValid(getContext(), file)) {
throw new FileNotFoundException("Invalid file: " + file);
}
@@ -1281,7 +1315,8 @@ public final class DownloadProvider extends ContentProvider {
} else {
try {
// When finished writing, update size and timestamp
- return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() {
+ return ParcelFileDescriptor.open(file, pfdMode, Helpers.getAsyncHandler(),
+ new OnCloseListener() {
@Override
public void onClose(IOException e) {
final ContentValues values = new ContentValues();
diff --git a/src/com/android/providers/downloads/DownloadReceiver.java b/src/com/android/providers/downloads/DownloadReceiver.java
index 28e2a673..a0dc6947 100644
--- a/src/com/android/providers/downloads/DownloadReceiver.java
+++ b/src/com/android/providers/downloads/DownloadReceiver.java
@@ -18,9 +18,16 @@ package com.android.providers.downloads;
import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION;
+
import static com.android.providers.downloads.Constants.TAG;
+import static com.android.providers.downloads.Helpers.getAsyncHandler;
+import static com.android.providers.downloads.Helpers.getDownloadNotifier;
+import static com.android.providers.downloads.Helpers.getInt;
+import static com.android.providers.downloads.Helpers.getString;
+import static com.android.providers.downloads.Helpers.getSystemFacade;
import android.app.DownloadManager;
+import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -28,58 +35,49 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
import android.net.Uri;
-import android.os.Handler;
-import android.os.HandlerThread;
import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
import android.widget.Toast;
-import com.google.common.annotations.VisibleForTesting;
-
/**
* Receives system broadcasts (boot, network connectivity)
*/
public class DownloadReceiver extends BroadcastReceiver {
- private static Handler sAsyncHandler;
-
- static {
- final HandlerThread thread = new HandlerThread("DownloadReceiver");
- thread.start();
- sAsyncHandler = new Handler(thread.getLooper());
- }
+ /**
+ * Intent extra included with {@link Constants#ACTION_CANCEL} intents,
+ * indicating the IDs (as array of long) of the downloads that were
+ * canceled.
+ */
+ public static final String EXTRA_CANCELED_DOWNLOAD_IDS =
+ "com.android.providers.downloads.extra.CANCELED_DOWNLOAD_IDS";
- @VisibleForTesting
- SystemFacade mSystemFacade = null;
+ /**
+ * Intent extra included with {@link Constants#ACTION_CANCEL} intents,
+ * indicating the tag of the notification corresponding to the download(s)
+ * that were canceled; this notification must be canceled.
+ */
+ public static final String EXTRA_CANCELED_DOWNLOAD_NOTIFICATION_TAG =
+ "com.android.providers.downloads.extra.CANCELED_DOWNLOAD_NOTIFICATION_TAG";
@Override
public void onReceive(final Context context, final Intent intent) {
- if (mSystemFacade == null) {
- mSystemFacade = new RealSystemFacade(context);
- }
-
final String action = intent.getAction();
- if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
- startService(context);
-
- } else if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
- startService(context);
-
- } else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
- final ConnectivityManager connManager = (ConnectivityManager) context
- .getSystemService(Context.CONNECTIVITY_SERVICE);
- final NetworkInfo info = connManager.getActiveNetworkInfo();
- if (info != null && info.isConnected()) {
- startService(context);
- }
-
+ if (Intent.ACTION_BOOT_COMPLETED.equals(action)
+ || Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
+ final PendingResult result = goAsync();
+ getAsyncHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ handleBootCompleted(context);
+ result.finish();
+ }
+ });
} else if (Intent.ACTION_UID_REMOVED.equals(action)) {
final PendingResult result = goAsync();
- sAsyncHandler.post(new Runnable() {
+ getAsyncHandler().post(new Runnable() {
@Override
public void run() {
handleUidRemoved(context, intent);
@@ -87,9 +85,6 @@ public class DownloadReceiver extends BroadcastReceiver {
}
});
- } else if (Constants.ACTION_RETRY.equals(action)) {
- startService(context);
-
} else if (Constants.ACTION_OPEN.equals(action)
|| Constants.ACTION_LIST.equals(action)
|| Constants.ACTION_HIDE.equals(action)) {
@@ -99,7 +94,7 @@ public class DownloadReceiver extends BroadcastReceiver {
// TODO: remove this once test is refactored
handleNotificationBroadcast(context, intent);
} else {
- sAsyncHandler.post(new Runnable() {
+ getAsyncHandler().post(new Runnable() {
@Override
public void run() {
handleNotificationBroadcast(context, intent);
@@ -107,7 +102,39 @@ public class DownloadReceiver extends BroadcastReceiver {
}
});
}
+ } else if (Constants.ACTION_CANCEL.equals(action)) {
+ long[] downloadIds = intent.getLongArrayExtra(
+ DownloadReceiver.EXTRA_CANCELED_DOWNLOAD_IDS);
+ DownloadManager manager = (DownloadManager) context.getSystemService(
+ Context.DOWNLOAD_SERVICE);
+ manager.remove(downloadIds);
+
+ String notifTag = intent.getStringExtra(
+ DownloadReceiver.EXTRA_CANCELED_DOWNLOAD_NOTIFICATION_TAG);
+ NotificationManager notifManager = (NotificationManager) context.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ notifManager.cancel(notifTag, 0);
+ }
+ }
+
+ private void handleBootCompleted(Context context) {
+ // Show any relevant notifications for completed downloads
+ getDownloadNotifier(context).update();
+
+ // Schedule all downloads that are ready
+ final ContentResolver resolver = context.getContentResolver();
+ try (Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, null, null,
+ null, null)) {
+ final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
+ final DownloadInfo info = new DownloadInfo(context);
+ while (cursor.moveToNext()) {
+ reader.updateFromDatabase(info);
+ Helpers.scheduleJob(context, info);
+ }
}
+
+ // Schedule idle pass to clean up orphaned files
+ DownloadIdleService.scheduleIdlePass(context);
}
private void handleUidRemoved(Context context, Intent intent) {
@@ -238,18 +265,6 @@ public class DownloadReceiver extends BroadcastReceiver {
}
}
- mSystemFacade.sendBroadcast(appIntent);
- }
-
- private static String getString(Cursor cursor, String col) {
- return cursor.getString(cursor.getColumnIndexOrThrow(col));
- }
-
- private static int getInt(Cursor cursor, String col) {
- return cursor.getInt(cursor.getColumnIndexOrThrow(col));
- }
-
- private void startService(Context context) {
- context.startService(new Intent(context, DownloadService.class));
+ getSystemFacade(context).sendBroadcast(appIntent);
}
}
diff --git a/src/com/android/providers/downloads/DownloadScanner.java b/src/com/android/providers/downloads/DownloadScanner.java
index ca795062..4a5ba87e 100644
--- a/src/com/android/providers/downloads/DownloadScanner.java
+++ b/src/com/android/providers/downloads/DownloadScanner.java
@@ -35,6 +35,8 @@ import com.android.internal.annotations.GuardedBy;
import com.google.common.collect.Maps;
import java.util.HashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
/**
* Manages asynchronous scanning of completed downloads.
@@ -66,11 +68,30 @@ public class DownloadScanner implements MediaScannerConnectionClient {
@GuardedBy("mConnection")
private HashMap<String, ScanRequest> mPending = Maps.newHashMap();
+ private CountDownLatch mLatch;
+
public DownloadScanner(Context context) {
mContext = context;
mConnection = new MediaScannerConnection(context, this);
}
+ public static void requestScanBlocking(Context context, DownloadInfo info) {
+ requestScanBlocking(context, info.mId, info.mFileName, info.mMimeType);
+ }
+
+ public static void requestScanBlocking(Context context, long id, String path, String mimeType) {
+ final DownloadScanner scanner = new DownloadScanner(context);
+ scanner.mLatch = new CountDownLatch(1);
+ scanner.requestScan(new ScanRequest(id, path, mimeType));
+ try {
+ scanner.mLatch.await(SCAN_TIMEOUT, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ scanner.shutdown();
+ }
+ }
+
/**
* Check if requested scans are still pending. Scans may timeout after an
* internal duration.
@@ -98,10 +119,9 @@ public class DownloadScanner implements MediaScannerConnectionClient {
*
* @see #hasPendingScans()
*/
- public void requestScan(DownloadInfo info) {
- if (LOGV) Log.v(TAG, "requestScan() for " + info.mFileName);
+ public void requestScan(ScanRequest req) {
+ if (LOGV) Log.v(TAG, "requestScan() for " + req.path);
synchronized (mConnection) {
- final ScanRequest req = new ScanRequest(info.mId, info.mFileName, info.mMimeType);
mPending.put(req.path, req);
if (mConnection.isConnected()) {
@@ -153,5 +173,9 @@ public class DownloadScanner implements MediaScannerConnectionClient {
// so clean up now-orphaned media entry.
resolver.delete(uri, null, null);
}
+
+ if (mLatch != null) {
+ mLatch.countDown();
+ }
}
}
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
deleted file mode 100644
index b0b73297..00000000
--- a/src/com/android/providers/downloads/DownloadService.java
+++ /dev/null
@@ -1,501 +0,0 @@
-/*
- * 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.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static com.android.providers.downloads.Constants.TAG;
-
-import android.app.AlarmManager;
-import android.app.DownloadManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Process;
-import android.provider.Downloads;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.IndentingPrintWriter;
-import com.google.android.collect.Maps;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Performs background downloads as requested by applications that use
- * {@link DownloadManager}. Multiple start commands can be issued at this
- * service, and it will continue running until no downloads are being actively
- * processed. It may schedule alarms to resume downloads in future.
- * <p>
- * Any database updates important enough to initiate tasks should always be
- * delivered through {@link Context#startService(Intent)}.
- */
-public class DownloadService extends Service {
- // TODO: migrate WakeLock from individual DownloadThreads out into
- // DownloadReceiver to protect our entire workflow.
-
- private static final boolean DEBUG_LIFECYCLE = false;
-
- @VisibleForTesting
- SystemFacade mSystemFacade;
-
- private AlarmManager mAlarmManager;
-
- /** Observer to get notified when the content observer's data changes */
- private DownloadManagerContentObserver mObserver;
-
- /** Class to handle Notification Manager updates */
- private DownloadNotifier mNotifier;
-
- /** Scheduling of the periodic cleanup job */
- private JobInfo mCleanupJob;
-
- private static final int CLEANUP_JOB_ID = 1;
- private static final long CLEANUP_JOB_PERIOD = 1000 * 60 * 60 * 24; // one day
- private static ComponentName sCleanupServiceName = new ComponentName(
- DownloadIdleService.class.getPackage().getName(),
- DownloadIdleService.class.getName());
-
- /**
- * The Service's view of the list of downloads, mapping download IDs to the corresponding info
- * object. This is kept independently from the content provider, and the Service only initiates
- * downloads based on this data, so that it can deal with situation where the data in the
- * content provider changes or disappears.
- */
- @GuardedBy("mDownloads")
- private final Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
-
- private final ExecutorService mExecutor = buildDownloadExecutor();
-
- private static ExecutorService buildDownloadExecutor() {
- final int maxConcurrent = Resources.getSystem().getInteger(
- com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed);
-
- // Create a bounded thread pool for executing downloads; it creates
- // threads as needed (up to maximum) and reclaims them when finished.
- final ThreadPoolExecutor executor = new ThreadPoolExecutor(
- maxConcurrent, maxConcurrent, 10, TimeUnit.SECONDS,
- new LinkedBlockingQueue<Runnable>()) {
- @Override
- protected void afterExecute(Runnable r, Throwable t) {
- super.afterExecute(r, t);
-
- if (t == null && r instanceof Future<?>) {
- try {
- ((Future<?>) r).get();
- } catch (CancellationException ce) {
- t = ce;
- } catch (ExecutionException ee) {
- t = ee.getCause();
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- }
- }
-
- if (t != null) {
- Log.w(TAG, "Uncaught exception", t);
- }
- }
- };
- executor.allowCoreThreadTimeOut(true);
- return executor;
- }
-
- private DownloadScanner mScanner;
-
- private HandlerThread mUpdateThread;
- private Handler mUpdateHandler;
-
- private volatile int mLastStartId;
-
- /**
- * Receives notifications when the data in the content provider changes
- */
- private class DownloadManagerContentObserver extends ContentObserver {
- public DownloadManagerContentObserver() {
- super(new Handler());
- }
-
- @Override
- public void onChange(final boolean selfChange) {
- enqueueUpdate();
- }
- }
-
- /**
- * Returns an IBinder instance when someone wants to connect to this
- * service. Binding to this service is not allowed.
- *
- * @throws UnsupportedOperationException
- */
- @Override
- public IBinder onBind(Intent i) {
- throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
- }
-
- /**
- * Initializes the service when it is first created
- */
- @Override
- public void onCreate() {
- super.onCreate();
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Service onCreate");
- }
-
- if (mSystemFacade == null) {
- mSystemFacade = new RealSystemFacade(this);
- }
-
- mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
-
- mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
- mUpdateThread.start();
- mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
-
- mScanner = new DownloadScanner(this);
-
- mNotifier = new DownloadNotifier(this);
- mNotifier.cancelAll();
-
- mObserver = new DownloadManagerContentObserver();
- getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- true, mObserver);
-
- JobScheduler js = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
- if (needToScheduleCleanup(js)) {
- final JobInfo job = new JobInfo.Builder(CLEANUP_JOB_ID, sCleanupServiceName)
- .setPeriodic(CLEANUP_JOB_PERIOD)
- .setRequiresCharging(true)
- .setRequiresDeviceIdle(true)
- .build();
- js.schedule(job);
- }
- }
-
- private boolean needToScheduleCleanup(JobScheduler js) {
- List<JobInfo> myJobs = js.getAllPendingJobs();
- if (myJobs != null) {
- final int N = myJobs.size();
- for (int i = 0; i < N; i++) {
- if (myJobs.get(i).getId() == CLEANUP_JOB_ID) {
- // It's already been (persistently) scheduled; no need to do it again
- return false;
- }
- }
- }
- return true;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- int returnValue = super.onStartCommand(intent, flags, startId);
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Service onStart");
- }
- mLastStartId = startId;
- enqueueUpdate();
- return returnValue;
- }
-
- @Override
- public void onDestroy() {
- getContentResolver().unregisterContentObserver(mObserver);
- mScanner.shutdown();
- mUpdateThread.quit();
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Service onDestroy");
- }
- super.onDestroy();
- }
-
- /**
- * Enqueue an {@link #updateLocked()} pass to occur in future.
- */
- public void enqueueUpdate() {
- if (mUpdateHandler != null) {
- mUpdateHandler.removeMessages(MSG_UPDATE);
- mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget();
- }
- }
-
- /**
- * Enqueue an {@link #updateLocked()} pass to occur after delay, usually to
- * catch any finished operations that didn't trigger an update pass.
- */
- private void enqueueFinalUpdate() {
- mUpdateHandler.removeMessages(MSG_FINAL_UPDATE);
- mUpdateHandler.sendMessageDelayed(
- mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1),
- 5 * MINUTE_IN_MILLIS);
- }
-
- private static final int MSG_UPDATE = 1;
- private static final int MSG_FINAL_UPDATE = 2;
-
- private Handler.Callback mUpdateCallback = new Handler.Callback() {
- @Override
- public boolean handleMessage(Message msg) {
- Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-
- final int startId = msg.arg1;
- if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId);
-
- // Since database is current source of truth, our "active" status
- // depends on database state. We always get one final update pass
- // once the real actions have finished and persisted their state.
-
- // TODO: switch to asking real tasks to derive active state
- // TODO: handle media scanner timeouts
-
- final boolean isActive;
- synchronized (mDownloads) {
- isActive = updateLocked();
- }
-
- if (msg.what == MSG_FINAL_UPDATE) {
- // Dump thread stacks belonging to pool
- for (Map.Entry<Thread, StackTraceElement[]> entry :
- Thread.getAllStackTraces().entrySet()) {
- if (entry.getKey().getName().startsWith("pool")) {
- Log.d(TAG, entry.getKey() + ": " + Arrays.toString(entry.getValue()));
- }
- }
-
- // Dump speed and update details
- mNotifier.dumpSpeeds();
-
- Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive
- + "; someone didn't update correctly.");
- }
-
- if (isActive) {
- // Still doing useful work, keep service alive. These active
- // tasks will trigger another update pass when they're finished.
-
- // Enqueue delayed update pass to catch finished operations that
- // didn't trigger an update pass; these are bugs.
- enqueueFinalUpdate();
-
- } else {
- // No active tasks, and any pending update messages can be
- // ignored, since any updates important enough to initiate tasks
- // will always be delivered with a new startId.
-
- if (stopSelfResult(startId)) {
- if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped");
- getContentResolver().unregisterContentObserver(mObserver);
- mScanner.shutdown();
- mUpdateThread.quit();
- }
- }
-
- return true;
- }
- };
-
- /**
- * Update {@link #mDownloads} to match {@link DownloadProvider} state.
- * Depending on current download state it may enqueue {@link DownloadThread}
- * instances, request {@link DownloadScanner} scans, update user-visible
- * notifications, and/or schedule future actions with {@link AlarmManager}.
- * <p>
- * Should only be called from {@link #mUpdateThread} as after being
- * requested through {@link #enqueueUpdate()}.
- *
- * @return If there are active tasks being processed, as of the database
- * snapshot taken in this update.
- */
- private boolean updateLocked() {
- final long now = mSystemFacade.currentTimeMillis();
-
- boolean isActive = false;
- long nextActionMillis = Long.MAX_VALUE;
-
- final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet());
-
- final ContentResolver resolver = getContentResolver();
- final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- null, null, null, null);
- try {
- final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
- final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
- while (cursor.moveToNext()) {
- final long id = cursor.getLong(idColumn);
- staleIds.remove(id);
-
- DownloadInfo info = mDownloads.get(id);
- if (info != null) {
- updateDownload(reader, info, now);
- } else {
- info = insertDownloadLocked(reader, now);
- }
-
- if (info.mDeleted) {
- // Delete download if requested, but only after cleaning up
- if (!TextUtils.isEmpty(info.mMediaProviderUri)) {
- resolver.delete(Uri.parse(info.mMediaProviderUri), null, null);
- }
-
- deleteFileIfExists(info.mFileName);
- resolver.delete(info.getAllDownloadsUri(), null, null);
-
- } else {
- // Kick off download task if ready
- final boolean activeDownload = info.startDownloadIfReady(mExecutor);
-
- // Kick off media scan if completed
- final boolean activeScan = info.startScanIfReady(mScanner);
-
- if (DEBUG_LIFECYCLE && (activeDownload || activeScan)) {
- Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload
- + ", activeScan=" + activeScan);
- }
-
- isActive |= activeDownload;
- isActive |= activeScan;
- }
-
- // Keep track of nearest next action
- nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis);
- }
- } finally {
- cursor.close();
- }
-
- // Clean up stale downloads that disappeared
- for (Long id : staleIds) {
- deleteDownloadLocked(id);
- }
-
- // Update notifications visible to user
- mNotifier.updateWith(mDownloads.values());
-
- // Set alarm when next action is in future. It's okay if the service
- // continues to run in meantime, since it will kick off an update pass.
- if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) {
- if (Constants.LOGV) {
- Log.v(TAG, "scheduling start in " + nextActionMillis + "ms");
- }
-
- final Intent intent = new Intent(Constants.ACTION_RETRY);
- intent.setClass(this, DownloadReceiver.class);
- mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis,
- PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT));
- }
-
- return isActive;
- }
-
- /**
- * Keeps a local copy of the info about a download, and initiates the
- * download if appropriate.
- */
- private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) {
- final DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade, mNotifier);
- mDownloads.put(info.mId, info);
-
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "processing inserted download " + info.mId);
- }
-
- return info;
- }
-
- /**
- * Updates the local copy of the info about a download.
- */
- private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
- reader.updateFromDatabase(info);
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "processing updated download " + info.mId +
- ", status: " + info.mStatus);
- }
- }
-
- /**
- * Removes the local copy of the info about a download.
- */
- private void deleteDownloadLocked(long id) {
- DownloadInfo info = mDownloads.get(id);
- if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
- info.mStatus = Downloads.Impl.STATUS_CANCELED;
- }
- if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
- if (Constants.LOGVV) {
- Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
- }
- deleteFileIfExists(info.mFileName);
- }
- mDownloads.remove(info.mId);
- }
-
- private void deleteFileIfExists(String path) {
- if (!TextUtils.isEmpty(path)) {
- if (Constants.LOGVV) {
- Log.d(TAG, "deleteFileIfExists() deleting " + path);
- }
- final File file = new File(path);
- if (file.exists() && !file.delete()) {
- Log.w(TAG, "file: '" + path + "' couldn't be deleted");
- }
- }
- }
-
- @Override
- protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
- final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
- synchronized (mDownloads) {
- final List<Long> ids = Lists.newArrayList(mDownloads.keySet());
- Collections.sort(ids);
- for (Long id : ids) {
- final DownloadInfo info = mDownloads.get(id);
- info.dump(pw);
- }
- }
- }
-}
diff --git a/src/com/android/providers/downloads/DownloadStorageProvider.java b/src/com/android/providers/downloads/DownloadStorageProvider.java
index 1b5dc844..e0bb7cd1 100644
--- a/src/com/android/providers/downloads/DownloadStorageProvider.java
+++ b/src/com/android/providers/downloads/DownloadStorageProvider.java
@@ -35,6 +35,7 @@ import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
+import android.support.provider.DocumentArchiveHelper;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
@@ -65,11 +66,14 @@ public class DownloadStorageProvider extends DocumentsProvider {
};
private DownloadManager mDm;
+ private DocumentArchiveHelper mArchiveHelper;
@Override
public boolean onCreate() {
mDm = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
mDm.setAccessAllDownloads(true);
+ mDm.setAccessFilename(true);
+ mArchiveHelper = new DocumentArchiveHelper(this, ':');
return true;
}
@@ -151,7 +155,30 @@ public class DownloadStorageProvider extends DocumentsProvider {
}
@Override
+ public String renameDocument(String documentId, String displayName)
+ throws FileNotFoundException {
+ displayName = FileUtils.buildValidFatFilename(displayName);
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final long id = Long.parseLong(documentId);
+
+ if (!mDm.rename(getContext(), id, displayName)) {
+ throw new IllegalStateException(
+ "Failed to rename to " + displayName + " in downloadsManager");
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ return null;
+ }
+
+ @Override
public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException {
+ if (mArchiveHelper.isArchivedDocument(docId)) {
+ return mArchiveHelper.queryDocument(docId, projection);
+ }
+
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
if (DOC_ID_ROOT.equals(docId)) {
@@ -164,6 +191,8 @@ public class DownloadStorageProvider extends DocumentsProvider {
cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
copyNotificationUri(result, cursor);
if (cursor.moveToFirst()) {
+ // We don't know if this queryDocument() call is from Downloads (manage)
+ // or Files. Safely assume it's Files.
includeDownloadFromCursor(result, cursor);
}
} finally {
@@ -177,6 +206,11 @@ public class DownloadStorageProvider extends DocumentsProvider {
@Override
public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder)
throws FileNotFoundException {
+ if (mArchiveHelper.isArchivedDocument(docId) ||
+ mArchiveHelper.isSupportedArchiveType(getDocumentType(docId))) {
+ return mArchiveHelper.queryChildDocuments(docId, projection, sortOrder);
+ }
+
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
// Delegate to real provider
@@ -200,6 +234,10 @@ public class DownloadStorageProvider extends DocumentsProvider {
public Cursor queryChildDocumentsForManage(
String parentDocumentId, String[] projection, String sortOrder)
throws FileNotFoundException {
+ if (mArchiveHelper.isArchivedDocument(parentDocumentId)) {
+ return mArchiveHelper.queryDocument(parentDocumentId, projection);
+ }
+
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
// Delegate to real provider
@@ -256,6 +294,10 @@ public class DownloadStorageProvider extends DocumentsProvider {
@Override
public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
throws FileNotFoundException {
+ if (mArchiveHelper.isArchivedDocument(docId)) {
+ return mArchiveHelper.openDocument(docId, mode, signal);
+ }
+
// Delegate to real provider
final long token = Binder.clearCallingIdentity();
try {
@@ -303,10 +345,12 @@ public class DownloadStorageProvider extends DocumentsProvider {
size = null;
}
+ int extraFlags = Document.FLAG_PARTIAL;
final int status = cursor.getInt(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
switch (status) {
case DownloadManager.STATUS_SUCCESSFUL:
+ extraFlags = Document.FLAG_SUPPORTS_RENAME; // only successful is non-partial
break;
case DownloadManager.STATUS_PAUSED:
summary = getContext().getString(R.string.download_queued);
@@ -331,11 +375,15 @@ public class DownloadStorageProvider extends DocumentsProvider {
break;
}
- int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE;
- if (mimeType != null && mimeType.startsWith("image/")) {
+ int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE | extraFlags;
+ if (mimeType.startsWith("image/")) {
flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
}
+ if (mArchiveHelper.isSupportedArchiveType(mimeType)) {
+ flags |= Document.FLAG_ARCHIVE;
+ }
+
final long lastModified = cursor.getLong(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP));
@@ -345,8 +393,18 @@ public class DownloadStorageProvider extends DocumentsProvider {
row.add(Document.COLUMN_SUMMARY, summary);
row.add(Document.COLUMN_SIZE, size);
row.add(Document.COLUMN_MIME_TYPE, mimeType);
- row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
row.add(Document.COLUMN_FLAGS, flags);
+ // Incomplete downloads get a null timestamp. This prevents thrashy UI when a bunch of
+ // active downloads get sorted by mod time.
+ if (status != DownloadManager.STATUS_RUNNING) {
+ row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
+ }
+
+ final String localFilePath = cursor.getString(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
+ if (localFilePath != null) {
+ row.add(DocumentArchiveHelper.COLUMN_LOCAL_FILE_PATH, localFilePath);
+ }
}
/**
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) {
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index d1cc5450..e9549052 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -20,12 +20,22 @@ import static android.os.Environment.buildExternalStorageAppCacheDirs;
import static android.os.Environment.buildExternalStorageAppFilesDirs;
import static android.os.Environment.buildExternalStorageAppMediaDirs;
import static android.os.Environment.buildExternalStorageAppObbDirs;
+import static android.provider.Downloads.Impl.FLAG_REQUIRES_CHARGING;
+import static android.provider.Downloads.Impl.FLAG_REQUIRES_DEVICE_IDLE;
+
import static com.android.providers.downloads.Constants.TAG;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
import android.content.Context;
+import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.os.FileUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.storage.StorageManager;
@@ -34,6 +44,8 @@ import android.provider.Downloads;
import android.util.Log;
import android.webkit.MimeTypeMap;
+import com.google.common.annotations.VisibleForTesting;
+
import java.io.File;
import java.io.IOException;
import java.util.Random;
@@ -53,9 +65,116 @@ public class Helpers {
private static final Object sUniqueLock = new Object();
+ private static HandlerThread sAsyncHandlerThread;
+ private static Handler sAsyncHandler;
+
+ private static SystemFacade sSystemFacade;
+ private static DownloadNotifier sNotifier;
+
private Helpers() {
}
+ public synchronized static Handler getAsyncHandler() {
+ if (sAsyncHandlerThread == null) {
+ sAsyncHandlerThread = new HandlerThread("sAsyncHandlerThread",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ sAsyncHandlerThread.start();
+ sAsyncHandler = new Handler(sAsyncHandlerThread.getLooper());
+ }
+ return sAsyncHandler;
+ }
+
+ @VisibleForTesting
+ public synchronized static void setSystemFacade(SystemFacade systemFacade) {
+ sSystemFacade = systemFacade;
+ }
+
+ public synchronized static SystemFacade getSystemFacade(Context context) {
+ if (sSystemFacade == null) {
+ sSystemFacade = new RealSystemFacade(context);
+ }
+ return sSystemFacade;
+ }
+
+ public synchronized static DownloadNotifier getDownloadNotifier(Context context) {
+ if (sNotifier == null) {
+ sNotifier = new DownloadNotifier(context);
+ }
+ return sNotifier;
+ }
+
+ public static String getString(Cursor cursor, String col) {
+ return cursor.getString(cursor.getColumnIndexOrThrow(col));
+ }
+
+ public static int getInt(Cursor cursor, String col) {
+ return cursor.getInt(cursor.getColumnIndexOrThrow(col));
+ }
+
+ public static void scheduleJob(Context context, long downloadId) {
+ final boolean scheduled = scheduleJob(context,
+ DownloadInfo.queryDownloadInfo(context, downloadId));
+ if (!scheduled) {
+ // If we didn't schedule a future job, kick off a notification
+ // update pass immediately
+ getDownloadNotifier(context).update();
+ }
+ }
+
+ /**
+ * Schedule (or reschedule) a job for the given {@link DownloadInfo} using
+ * its current state to define job constraints.
+ */
+ public static boolean scheduleJob(Context context, DownloadInfo info) {
+ if (info == null) return false;
+
+ final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
+
+ // Tear down any existing job for this download
+ final int jobId = (int) info.mId;
+ scheduler.cancel(jobId);
+
+ // Skip scheduling if download is paused or finished
+ if (!info.isReadyToSchedule()) return false;
+
+ final JobInfo.Builder builder = new JobInfo.Builder(jobId,
+ new ComponentName(context, DownloadJobService.class));
+
+ // When this download will show a notification, run with a higher
+ // priority, since it's effectively a foreground service
+ if (info.isVisible()) {
+ builder.setPriority(JobInfo.PRIORITY_FOREGROUND_APP);
+ builder.setFlags(JobInfo.FLAG_WILL_BE_FOREGROUND);
+ }
+
+ // We might have a backoff constraint due to errors
+ final long latency = info.getMinimumLatency();
+ if (latency > 0) {
+ builder.setMinimumLatency(latency);
+ }
+
+ // We always require a network, but the type of network might be further
+ // restricted based on download request or user override
+ builder.setRequiredNetworkType(info.getRequiredNetworkType(info.mTotalBytes));
+
+ if ((info.mFlags & FLAG_REQUIRES_CHARGING) != 0) {
+ builder.setRequiresCharging(true);
+ }
+ if ((info.mFlags & FLAG_REQUIRES_DEVICE_IDLE) != 0) {
+ builder.setRequiresDeviceIdle(true);
+ }
+
+ // If package name was filtered during insert (probably due to being
+ // invalid), blame based on the requesting UID instead
+ String packageName = info.mPackage;
+ if (packageName == null) {
+ packageName = context.getPackageManager().getPackagesForUid(info.mUid)[0];
+ }
+
+ scheduler.scheduleAsPackage(builder.build(), packageName, UserHandle.myUserId(), TAG);
+ return true;
+ }
+
/*
* Parse the Content-Disposition HTTP Header. The format of the header
* is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
@@ -357,8 +476,6 @@ public class Helpers {
static boolean isFilenameValidInExternalPackage(Context context, File file,
String packageName) {
try {
- file = file.getCanonicalFile();
-
if (containsCanonical(buildExternalStorageAppFilesDirs(packageName), file) ||
containsCanonical(buildExternalStorageAppObbDirs(packageName), file) ||
containsCanonical(buildExternalStorageAppCacheDirs(packageName), file) ||
@@ -380,8 +497,6 @@ public class Helpers {
*/
static boolean isFilenameValid(Context context, File file, boolean allowInternal) {
try {
- file = file.getCanonicalFile();
-
if (allowInternal) {
if (containsCanonical(context.getFilesDir(), file)
|| containsCanonical(context.getCacheDir(), file)
diff --git a/src/com/android/providers/downloads/OpenHelper.java b/src/com/android/providers/downloads/OpenHelper.java
index 4eb319c4..27ab86b9 100644
--- a/src/com/android/providers/downloads/OpenHelper.java
+++ b/src/com/android/providers/downloads/OpenHelper.java
@@ -30,6 +30,8 @@ import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
+import android.os.StrictMode;
+import android.provider.DocumentsContract;
import android.provider.Downloads.Impl.RequestHeaders;
import android.util.Log;
@@ -49,11 +51,14 @@ public class OpenHelper {
intent.addFlags(intentFlags);
try {
+ StrictMode.disableDeathOnFileUriExposure();
context.startActivity(intent);
return true;
} catch (ActivityNotFoundException e) {
Log.w(TAG, "Failed to start " + intent + ": " + e);
return false;
+ } finally {
+ StrictMode.enableDeathOnFileUriExposure();
}
}
@@ -65,6 +70,7 @@ public class OpenHelper {
final DownloadManager downManager = (DownloadManager) context.getSystemService(
Context.DOWNLOAD_SERVICE);
downManager.setAccessAllDownloads(true);
+ downManager.setAccessFilename(true);
final Cursor cursor = downManager.query(new DownloadManager.Query().setFilterById(id));
try {
@@ -77,6 +83,9 @@ public class OpenHelper {
String mimeType = getCursorString(cursor, COLUMN_MEDIA_TYPE);
mimeType = DownloadDrmHelper.getOriginalMimeType(context, file, mimeType);
+ final Uri documentUri = DocumentsContract.buildDocumentUri(
+ Constants.STORAGE_AUTHORITY, String.valueOf(id));
+
final Intent intent = new Intent(Intent.ACTION_VIEW);
if ("application/vnd.android.package-archive".equals(mimeType)) {
@@ -88,14 +97,10 @@ public class OpenHelper {
intent.putExtra(Intent.EXTRA_ORIGINATING_URI, remoteUri);
intent.putExtra(Intent.EXTRA_REFERRER, getRefererUri(context, id));
intent.putExtra(Intent.EXTRA_ORIGINATING_UID, getOriginatingUid(context, id));
- } else if ("file".equals(localUri.getScheme())) {
+ } else {
+ intent.setDataAndType(documentUri, mimeType);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- intent.setDataAndType(
- ContentUris.withAppendedId(ALL_DOWNLOADS_CONTENT_URI, id), mimeType);
- } else {
- intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- intent.setDataAndType(localUri, mimeType);
}
return intent;
diff --git a/src/com/android/providers/downloads/RealSystemFacade.java b/src/com/android/providers/downloads/RealSystemFacade.java
index b3f170fb..df1d245f 100644
--- a/src/com/android/providers/downloads/RealSystemFacade.java
+++ b/src/com/android/providers/downloads/RealSystemFacade.java
@@ -16,8 +16,6 @@
package com.android.providers.downloads;
-import com.android.internal.util.ArrayUtils;
-
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
@@ -26,9 +24,17 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
+import android.net.Network;
import android.net.NetworkInfo;
-import android.telephony.TelephonyManager;
-import android.util.Log;
+import android.security.NetworkSecurityPolicy;
+import android.security.net.config.ApplicationConfig;
+
+import java.security.GeneralSecurityException;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+
+import com.android.internal.util.ArrayUtils;
class RealSystemFacade implements SystemFacade {
private Context mContext;
@@ -43,53 +49,27 @@ class RealSystemFacade implements SystemFacade {
}
@Override
- public NetworkInfo getActiveNetworkInfo(int uid) {
- ConnectivityManager connectivity =
- (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
- if (connectivity == null) {
- Log.w(Constants.TAG, "couldn't get connectivity manager");
- return null;
- }
-
- final NetworkInfo activeInfo = connectivity.getActiveNetworkInfoForUid(uid);
- if (activeInfo == null && Constants.LOGVV) {
- Log.v(Constants.TAG, "network is not available");
- }
- return activeInfo;
- }
-
- @Override
- public boolean isActiveNetworkMetered() {
- final ConnectivityManager conn = ConnectivityManager.from(mContext);
- return conn.isActiveNetworkMetered();
+ public Network getActiveNetwork(int uid, boolean ignoreBlocked) {
+ return mContext.getSystemService(ConnectivityManager.class)
+ .getActiveNetworkForUid(uid, ignoreBlocked);
}
@Override
- public boolean isNetworkRoaming() {
- ConnectivityManager connectivity =
- (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
- if (connectivity == null) {
- Log.w(Constants.TAG, "couldn't get connectivity manager");
- return false;
- }
-
- NetworkInfo info = connectivity.getActiveNetworkInfo();
- boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
- boolean isRoaming = isMobile && TelephonyManager.getDefault().isNetworkRoaming();
- if (Constants.LOGVV && isRoaming) {
- Log.v(Constants.TAG, "network is roaming");
- }
- return isRoaming;
+ public NetworkInfo getNetworkInfo(Network network, int uid, boolean ignoreBlocked) {
+ return mContext.getSystemService(ConnectivityManager.class)
+ .getNetworkInfoForUid(network, uid, ignoreBlocked);
}
@Override
- public Long getMaxBytesOverMobile() {
- return DownloadManager.getMaxBytesOverMobile(mContext);
+ public long getMaxBytesOverMobile() {
+ final Long value = DownloadManager.getMaxBytesOverMobile(mContext);
+ return (value == null) ? Long.MAX_VALUE : value;
}
@Override
- public Long getRecommendedMaxBytesOverMobile() {
- return DownloadManager.getRecommendedMaxBytesOverMobile(mContext);
+ public long getRecommendedMaxBytesOverMobile() {
+ final Long value = DownloadManager.getRecommendedMaxBytesOverMobile(mContext);
+ return (value == null) ? Long.MAX_VALUE : value;
}
@Override
@@ -121,6 +101,21 @@ class RealSystemFacade implements SystemFacade {
return false;
}
+ @Override
+ public SSLContext getSSLContextForPackage(Context context, String packageName)
+ throws GeneralSecurityException {
+ ApplicationConfig appConfig;
+ try {
+ appConfig = NetworkSecurityPolicy.getApplicationConfigForPackage(context, packageName);
+ } catch (NameNotFoundException e) {
+ // Unknown package -- fallback to the default SSLContext
+ return SSLContext.getDefault();
+ }
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(null, new TrustManager[] {appConfig.getTrustManager()}, null);
+ return ctx;
+ }
+
/**
* Returns whether cleartext network traffic (HTTP) is permitted for the provided package.
*/
diff --git a/src/com/android/providers/downloads/SizeLimitActivity.java b/src/com/android/providers/downloads/SizeLimitActivity.java
deleted file mode 100644
index d25277d9..00000000
--- a/src/com/android/providers/downloads/SizeLimitActivity.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2010 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 android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.ContentValues;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.Downloads;
-import android.text.format.Formatter;
-import android.util.Log;
-
-import java.util.LinkedList;
-import java.util.Queue;
-
-/**
- * Activity to show dialogs to the user when a download exceeds a limit on download sizes for
- * mobile networks. This activity gets started by the background download service when a download's
- * size is discovered to be exceeded one of these thresholds.
- */
-public class SizeLimitActivity extends Activity
- implements DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
- private Dialog mDialog;
- private Queue<Intent> mDownloadsToShow = new LinkedList<Intent>();
- private Uri mCurrentUri;
- private Intent mCurrentIntent;
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- setIntent(intent);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- Intent intent = getIntent();
- if (intent != null) {
- mDownloadsToShow.add(intent);
- setIntent(null);
- showNextDialog();
- }
- if (mDialog != null && !mDialog.isShowing()) {
- mDialog.show();
- }
- }
-
- private void showNextDialog() {
- if (mDialog != null) {
- return;
- }
-
- if (mDownloadsToShow.isEmpty()) {
- finish();
- return;
- }
-
- mCurrentIntent = mDownloadsToShow.poll();
- mCurrentUri = mCurrentIntent.getData();
- Cursor cursor = getContentResolver().query(mCurrentUri, null, null, null, null);
- try {
- if (!cursor.moveToFirst()) {
- Log.e(Constants.TAG, "Empty cursor for URI " + mCurrentUri);
- dialogClosed();
- return;
- }
- showDialog(cursor);
- } finally {
- cursor.close();
- }
- }
-
- private void showDialog(Cursor cursor) {
- int size = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES));
- String sizeString = Formatter.formatFileSize(this, size);
- String queueText = getString(R.string.button_queue_for_wifi);
- boolean isWifiRequired =
- mCurrentIntent.getExtras().getBoolean(DownloadInfo.EXTRA_IS_WIFI_REQUIRED);
-
- AlertDialog.Builder builder = new AlertDialog.Builder(this, AlertDialog.THEME_HOLO_DARK);
- if (isWifiRequired) {
- builder.setTitle(R.string.wifi_required_title)
- .setMessage(getString(R.string.wifi_required_body, sizeString, queueText))
- .setPositiveButton(R.string.button_queue_for_wifi, this)
- .setNegativeButton(R.string.button_cancel_download, this);
- } else {
- builder.setTitle(R.string.wifi_recommended_title)
- .setMessage(getString(R.string.wifi_recommended_body, sizeString, queueText))
- .setPositiveButton(R.string.button_start_now, this)
- .setNegativeButton(R.string.button_queue_for_wifi, this);
- }
- mDialog = builder.setOnCancelListener(this).show();
- }
-
- @Override
- public void onCancel(DialogInterface dialog) {
- dialogClosed();
- }
-
- private void dialogClosed() {
- mDialog = null;
- mCurrentUri = null;
- showNextDialog();
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- boolean isRequired =
- mCurrentIntent.getExtras().getBoolean(DownloadInfo.EXTRA_IS_WIFI_REQUIRED);
- if (isRequired && which == AlertDialog.BUTTON_NEGATIVE) {
- getContentResolver().delete(mCurrentUri, null, null);
- } else if (!isRequired && which == AlertDialog.BUTTON_POSITIVE) {
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT, true);
- getContentResolver().update(mCurrentUri, values , null, null);
- }
- dialogClosed();
- }
-}
diff --git a/src/com/android/providers/downloads/SystemFacade.java b/src/com/android/providers/downloads/SystemFacade.java
index 83fc7a63..c34317cb 100644
--- a/src/com/android/providers/downloads/SystemFacade.java
+++ b/src/com/android/providers/downloads/SystemFacade.java
@@ -16,41 +16,37 @@
package com.android.providers.downloads;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Network;
import android.net.NetworkInfo;
+import java.security.GeneralSecurityException;
+import javax.net.ssl.SSLContext;
+
interface SystemFacade {
/**
* @see System#currentTimeMillis()
*/
public long currentTimeMillis();
- /**
- * @return Currently active network, or null if there's no active
- * connection.
- */
- public NetworkInfo getActiveNetworkInfo(int uid);
+ public Network getActiveNetwork(int uid, boolean ignoreBlocked);
- public boolean isActiveNetworkMetered();
-
- /**
- * @see android.telephony.TelephonyManager#isNetworkRoaming
- */
- public boolean isNetworkRoaming();
+ public NetworkInfo getNetworkInfo(Network network, int uid, boolean ignoreBlocked);
/**
* @return maximum size, in bytes, of downloads that may go over a mobile connection; or null if
* there's no limit
*/
- public Long getMaxBytesOverMobile();
+ public long getMaxBytesOverMobile();
/**
* @return recommended maximum size, in bytes, of downloads that may go over a mobile
* connection; or null if there's no recommended limit. The user will have the option to bypass
* this limit.
*/
- public Long getRecommendedMaxBytesOverMobile();
+ public long getRecommendedMaxBytesOverMobile();
/**
* Send a broadcast intent.
@@ -66,4 +62,10 @@ interface SystemFacade {
* Returns true if cleartext network traffic is permitted for the specified UID.
*/
public boolean isCleartextTrafficPermitted(int uid);
+
+ /**
+ * Return a {@link SSLContext} configured using the specified package's configuration.
+ */
+ public SSLContext getSSLContextForPackage(Context context, String pckg)
+ throws GeneralSecurityException;
}