summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml15
-rw-r--r--src/com/android/providers/downloads/Constants.java3
-rw-r--r--src/com/android/providers/downloads/DownloadIdleService.java25
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java376
-rw-r--r--src/com/android/providers/downloads/DownloadJobService.java112
-rw-r--r--src/com/android/providers/downloads/DownloadNotifier.java214
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java110
-rw-r--r--src/com/android/providers/downloads/DownloadReceiver.java103
-rw-r--r--src/com/android/providers/downloads/DownloadScanner.java21
-rw-r--r--src/com/android/providers/downloads/DownloadService.java516
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java182
-rw-r--r--src/com/android/providers/downloads/Helpers.java117
-rw-r--r--src/com/android/providers/downloads/RealSystemFacade.java61
-rw-r--r--src/com/android/providers/downloads/SizeLimitActivity.java137
-rw-r--r--src/com/android/providers/downloads/SystemFacade.java17
-rw-r--r--tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java35
-rw-r--r--tests/src/com/android/providers/downloads/AbstractPublicApiTest.java7
-rw-r--r--tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java9
-rw-r--r--tests/src/com/android/providers/downloads/FakeSystemFacade.java55
-rw-r--r--tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java42
-rw-r--r--tests/src/com/android/providers/downloads/ThreadingTest.java17
-rw-r--r--ui/res/values/strings.xml4
-rw-r--r--ui/src/com/android/providers/downloads/ui/TrampolineActivity.java30
23 files changed, 827 insertions, 1381 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 0ba8ead..8720523 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -97,19 +97,18 @@
</provider>
<service
- android:name=".DownloadService"
- android:permission="android.permission.ACCESS_DOWNLOAD_MANAGER" />
+ android:name=".DownloadJobService"
+ android:exported="true"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
<service
- android:name="com.android.providers.downloads.DownloadIdleService"
+ android:name=".DownloadIdleService"
android:exported="true"
- android:permission="android.permission.BIND_JOB_SERVICE">
- </service>
+ android:permission="android.permission.BIND_JOB_SERVICE" />
<receiver android:name=".DownloadReceiver" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
- <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<action android:name="android.intent.action.UID_REMOVED" />
</intent-filter>
<intent-filter>
@@ -117,9 +116,5 @@
<data android:scheme="file" />
</intent-filter>
</receiver>
-
- <activity android:name=".SizeLimitActivity"
- android:launchMode="singleTask"
- android:theme="@style/Theme.Translucent"/>
</application>
</manifest>
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
index 6cea808..79daeae 100644
--- a/src/com/android/providers/downloads/Constants.java
+++ b/src/com/android/providers/downloads/Constants.java
@@ -45,9 +45,6 @@ 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";
diff --git a/src/com/android/providers/downloads/DownloadIdleService.java b/src/com/android/providers/downloads/DownloadIdleService.java
index b537155..ecebb0f 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 bee5c4a..c94dd6c 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -19,24 +19,20 @@ package com.android.providers.downloads;
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 +41,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 +59,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 +90,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 +101,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 +145,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 +176,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 +247,80 @@ public class DownloadInfo {
}
/**
- * Returns the time when a download should be restarted.
+ * 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 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 long restartTime(long now) {
- if (mNumFailed == 0) {
- return now;
+ public int getRequiredNetworkType(long totalBytes) {
+ if (!mAllowMetered) {
+ return JobInfo.NETWORK_TYPE_UNMETERED;
+ }
+ if (mAllowedNetworkTypes == DownloadManager.Request.NETWORK_WIFI) {
+ return JobInfo.NETWORK_TYPE_UNMETERED;
}
- if (mRetryAfter > 0) {
- return mLastMod + mRetryAfter;
+ if (totalBytes > mSystemFacade.getMaxBytesOverMobile()) {
+ return JobInfo.NETWORK_TYPE_UNMETERED;
}
- return mLastMod +
- Constants.RETRY_FIRST_DELAY *
- (1000 + mFuzz) * (1 << (mNumFailed - 1));
+ if (totalBytes > mSystemFacade.getRecommendedMaxBytesOverMobile()
+ && mBypassRecommendedSizeLimit == 0) {
+ return JobInfo.NETWORK_TYPE_UNMETERED;
+ }
+ 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 +332,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 +352,7 @@ 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);
- }
-
- private boolean isRoamingAllowed() {
+ public boolean isRoamingAllowed() {
if (mIsPublicApi) {
return mAllowRoaming;
} else { // legacy behavior
@@ -406,112 +360,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,33 +419,13 @@ 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() {
@@ -608,33 +436,25 @@ public class DownloadInfo {
&& 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);
- }
-
/**
* 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 0000000..0ce4266
--- /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 5f961eb..558393d 100644
--- a/src/com/android/providers/downloads/DownloadNotifier.java
+++ b/src/com/android/providers/downloads/DownloadNotifier.java
@@ -20,6 +20,7 @@ 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_RUNNING;
+
import static com.android.providers.downloads.Constants.TAG;
import android.app.DownloadManager;
@@ -30,31 +31,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.android.internal.util.ArrayUtils;
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
-
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 {
@@ -70,20 +67,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")
@@ -123,48 +120,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;
}
- private static boolean isClusterDeleted(Collection<DownloadInfo> cluster) {
- boolean wasDeleted = true;
- for (DownloadInfo info : cluster) {
- wasDeleted = wasDeleted && info.mDeleted;
+ 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);
+ }
}
- return wasDeleted;
}
- 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
- Iterator<String> it = clustered.keySet().iterator();
- while (it.hasNext()) {
- final String tag = it.next();
+ 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);
-
- // If each of the downloads was canceled, don't show notification for the cluster
- if (isClusterDeleted(cluster)) {
- it.remove();
- continue;
- }
final Notification.Builder builder = new Notification.Builder(mContext);
builder.setColor(res.getColor(
@@ -191,7 +202,7 @@ public class DownloadNotifier {
// Build action intents
if (type == TYPE_ACTIVE || type == TYPE_WAITING) {
- long[] downloadIds = getDownloadIds(cluster);
+ 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();
@@ -217,16 +228,20 @@ public class DownloadNotifier {
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;
@@ -235,7 +250,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));
@@ -252,11 +267,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);
}
}
}
@@ -281,13 +302,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);
}
@@ -298,9 +319,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));
}
@@ -311,8 +333,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) {
@@ -338,29 +361,31 @@ public class DownloadNotifier {
}
// Remove stale tags that weren't renewed
- 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;
}
@@ -377,17 +402,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 (status == Downloads.Impl.STATUS_QUEUED_FOR_WIFI) {
+ 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;
}
@@ -395,21 +425,21 @@ 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 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 78b4294..00ed043 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -16,9 +16,14 @@
package com.android.providers.downloads;
+import static android.provider.BaseColumns._ID;
+import static android.provider.Downloads.Impl.COLUMN_MEDIAPROVIDER_URI;
+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 +40,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 +50,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 +77,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 +174,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 +184,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 +331,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 +449,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 +464,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 +667,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 +688,12 @@ 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);
+ }
return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
}
@@ -806,6 +808,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 +1056,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 +1066,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 +1095,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 +1105,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 +1135,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 +1183,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 +1192,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 +1216,13 @@ public final class DownloadProvider extends ContentProvider {
} catch (IOException ignored) {
}
}
+
+ final String mediaUri = cursor.getString(2);
+ if (!TextUtils.isEmpty(mediaUri)) {
+ getContext().getContentResolver().delete(Uri.parse(mediaUri), null,
+ null);
+ }
}
- } finally {
- IoUtils.closeQuietly(cursor);
}
count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters());
@@ -1287,7 +1300,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 2f50dcf..a0dc694 100644
--- a/src/com/android/providers/downloads/DownloadReceiver.java
+++ b/src/com/android/providers/downloads/DownloadReceiver.java
@@ -18,7 +18,13 @@ 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;
@@ -29,73 +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 {
/**
- * Intent extra included with {@link #ACTION_CANCEL} intents, indicating the IDs (as array of
- * long) of the downloads that were canceled.
+ * 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";
/**
- * Intent extra included with {@link #ACTION_CANCEL} intents, indicating the tag of the
- * notification corresponding to the download(s) that were canceled; this notification must be
- * canceled.
+ * 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";
- private static Handler sAsyncHandler;
-
- static {
- final HandlerThread thread = new HandlerThread("DownloadReceiver");
- thread.start();
- sAsyncHandler = new Handler(thread.getLooper());
- }
-
- @VisibleForTesting
- SystemFacade mSystemFacade = null;
-
@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);
@@ -103,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)) {
@@ -115,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);
@@ -138,6 +117,26 @@ public class DownloadReceiver extends BroadcastReceiver {
}
}
+ 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) {
final ContentResolver resolver = context.getContentResolver();
@@ -266,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 ca79506..37f5114 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,26 @@ 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) {
+ final DownloadScanner scanner = new DownloadScanner(context);
+ scanner.mLatch = new CountDownLatch(1);
+ scanner.requestScan(info);
+ 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.
@@ -153,5 +170,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 7d4392e..0000000
--- a/src/com/android/providers/downloads/DownloadService.java
+++ /dev/null
@@ -1,516 +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.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.IDeviceIdleController;
-import android.os.Message;
-import android.os.Process;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-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;
- private IDeviceIdleController mDeviceIdleController;
-
- /** 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);
- mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
- ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
- try {
- mDeviceIdleController.downloadServiceActive(new Binder());
- } catch (RemoteException e) {
- }
-
- mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
- mUpdateThread.start();
- mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
-
- mScanner = new DownloadScanner(this);
-
- mNotifier = new DownloadNotifier(this);
- mNotifier.init();
-
- 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();
- try {
- mDeviceIdleController.downloadServiceInactive();
- } catch (RemoteException e) {
- }
- 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/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index 9de563f..c559367 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;
@@ -51,19 +62,16 @@ 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;
@@ -89,7 +97,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
@@ -104,6 +112,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;
@@ -134,6 +146,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;
@@ -179,8 +199,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!");
+ }
}
}
}
@@ -197,6 +221,8 @@ public class DownloadThread implements Runnable {
private long mLastUpdateBytes = 0;
private long mLastUpdateTime = 0;
+ private Network mNetwork;
+
private int mNetworkType = ConnectivityManager.TYPE_NONE;
/** Historical bytes/second speed of this download. */
@@ -206,11 +232,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;
@@ -223,29 +255,32 @@ 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();
+
+ // 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);
+ 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);
if (info != null) {
mNetworkType = info.getType();
}
@@ -288,7 +323,7 @@ public class DownloadThread implements Runnable {
}
if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) {
- final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
+ final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork);
if (info != null && info.getType() == mNetworkType && info.isConnected()) {
// Underlying network is still intact, use normal backoff
mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY;
@@ -321,20 +356,27 @@ 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()) {
+ DownloadScanner.requestScanBlocking(mContext, mInfo);
}
+ } else if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY
+ || mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK) {
+ Helpers.scheduleJob(mContext, DownloadInfo.queryDownloadInfo(mContext, mId));
}
+
+ mJobService.jobFinishedInternal(mParams, false);
+ }
+
+ public void requestShutdown() {
+ mShutdownRequested = true;
}
/**
@@ -352,15 +394,6 @@ public class DownloadThread implements Runnable {
throw new StopRequestException(STATUS_BAD_REQUEST, e);
}
- // Use the caller's default network to make this connection, since they might be subject to
- // restrictions that we shouldn't let them circumvent.
- final Network network = mSystemFacade.getActiveNetwork(mInfo.mUid);
- if (network == null) {
- throw new StopRequestException(Downloads.Impl.STATUS_WAITING_FOR_NETWORK,
- "no network associated with requesting UID");
- }
- logDebug("Using network: " + network);
-
boolean cleartextTrafficPermitted = mSystemFacade.isCleartextTrafficPermitted(mInfo.mUid);
int redirectionCount = 0;
while (redirectionCount++ < Constants.MAX_REDIRECTS) {
@@ -379,7 +412,7 @@ public class DownloadThread implements Runnable {
// Check that the caller is allowed to make network connections. If so, make one on
// their behalf to open the url.
checkConnectivity();
- conn = (HttpURLConnection) network.openConnection(url);
+ conn = (HttpURLConnection) mNetwork.openConnection(url);
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(DEFAULT_TIMEOUT);
conn.setReadTimeout(DEFAULT_TIMEOUT);
@@ -542,7 +575,7 @@ public class DownloadThread implements Runnable {
} finally {
if (drmClient != null) {
- drmClient.release();
+ drmClient.close();
}
IoUtils.closeQuietly(in);
@@ -565,7 +598,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 {
@@ -665,38 +703,19 @@ 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 boolean allowMetered = mInfo
+ .getRequiredNetworkType(mInfoDelta.mTotalBytes) != JobInfo.NETWORK_TYPE_UNMETERED;
+ final boolean allowRoaming = mInfo.isRoamingAllowed();
- /**
- * 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");
- }
+ final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork);
+ if (info == null || !info.isConnected()) {
+ throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is disconnected");
}
-
- // if policy has been changed, trigger connectivity check
- if (mPolicyDirty) {
- checkConnectivity();
+ if (info.isRoaming() && !allowRoaming) {
+ throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is roaming");
+ }
+ if (info.isMetered() && !allowMetered) {
+ throw new StopRequestException(STATUS_QUEUED_FOR_WIFI, "Network is metered");
}
}
@@ -781,17 +800,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);
}
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index d01cbff..b073745 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -20,12 +20,24 @@ 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 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.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 +46,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 +67,112 @@ 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) {
+ scheduleJob(context, DownloadInfo.queryDownloadInfo(context, downloadId));
+ }
+
+ /**
+ * Schedule (or reschedule) a job for the given {@link DownloadInfo} using
+ * its current state to define job constraints.
+ */
+ public static void scheduleJob(Context context, DownloadInfo info) {
+ if (info == null) return;
+
+ 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;
+
+ 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
+ switch (info.mVisibility) {
+ case VISIBILITY_VISIBLE:
+ case VISIBILITY_VISIBLE_NOTIFY_COMPLETED:
+ // TODO: force app out of doze, since they're showing a notification
+ builder.setPriority(JobInfo.PRIORITY_FOREGROUND_APP);
+ break;
+ }
+
+ // 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);
+ }
+
/*
* Parse the Content-Disposition HTTP Header. The format of the header
* is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
diff --git a/src/com/android/providers/downloads/RealSystemFacade.java b/src/com/android/providers/downloads/RealSystemFacade.java
index 48df2a0..da4e01e 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;
@@ -28,8 +26,8 @@ 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 com.android.internal.util.ArrayUtils;
class RealSystemFacade implements SystemFacade {
private Context mContext;
@@ -44,60 +42,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 Network getActiveNetwork(int uid) {
- ConnectivityManager connectivity =
- (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
- return connectivity.getActiveNetworkForUid(uid);
- }
-
- @Override
- public boolean isActiveNetworkMetered() {
- final ConnectivityManager conn = ConnectivityManager.from(mContext);
- return conn.isActiveNetworkMetered();
+ return mContext.getSystemService(ConnectivityManager.class)
+ .getActiveNetworkForUid(uid);
}
@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) {
+ return mContext.getSystemService(ConnectivityManager.class)
+ .getNetworkInfo(network);
}
@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
diff --git a/src/com/android/providers/downloads/SizeLimitActivity.java b/src/com/android/providers/downloads/SizeLimitActivity.java
deleted file mode 100644
index d25277d..0000000
--- 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 7f97b91..e7852e9 100644
--- a/src/com/android/providers/downloads/SystemFacade.java
+++ b/src/com/android/providers/downloads/SystemFacade.java
@@ -27,33 +27,22 @@ interface SystemFacade {
*/
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);
- public boolean isActiveNetworkMetered();
-
- /**
- * @see android.telephony.TelephonyManager#isNetworkRoaming
- */
- public boolean isNetworkRoaming();
+ public NetworkInfo getNetworkInfo(Network network);
/**
* @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.
diff --git a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
index 6934b86..0330fd3 100644
--- a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
@@ -17,13 +17,14 @@
package com.android.providers.downloads;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import android.app.DownloadManager;
import android.app.NotificationManager;
-import android.content.ComponentName;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
import android.content.ContentResolver;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.ProviderInfo;
import android.database.ContentObserver;
import android.database.Cursor;
@@ -49,7 +50,7 @@ import java.net.MalformedURLException;
import java.net.UnknownHostException;
public abstract class AbstractDownloadProviderFunctionalTest extends
- ServiceTestCase<DownloadService> {
+ ServiceTestCase<DownloadJobService> {
protected static final String LOG_TAG = "DownloadProviderFunctionalTest";
private static final String PROVIDER_AUTHORITY = "downloads";
@@ -102,14 +103,14 @@ public abstract class AbstractDownloadProviderFunctionalTest extends
private final ContentResolver mResolver;
private final NotificationManager mNotifManager;
private final DownloadManager mDownloadManager;
-
- boolean mHasServiceBeenStarted = false;
+ private final JobScheduler mJobScheduler;
public TestContext(Context realContext) {
super(realContext, FILENAME_PREFIX);
mResolver = new MockContentResolverWithNotify(this);
mNotifManager = mock(NotificationManager.class);
mDownloadManager = mock(DownloadManager.class);
+ mJobScheduler = mock(JobScheduler.class);
}
/**
@@ -129,26 +130,16 @@ public abstract class AbstractDownloadProviderFunctionalTest extends
return mNotifManager;
} else if (Context.DOWNLOAD_SERVICE.equals(name)) {
return mDownloadManager;
+ } else if (Context.JOB_SCHEDULER_SERVICE.equals(name)) {
+ return mJobScheduler;
}
return super.getSystemService(name);
}
-
- /**
- * Record when DownloadProvider starts DownloadService.
- */
- @Override
- public ComponentName startService(Intent service) {
- if (service.getComponent().getClassName().equals(DownloadService.class.getName())) {
- mHasServiceBeenStarted = true;
- return service.getComponent();
- }
- throw new UnsupportedOperationException("Unexpected service: " + service);
- }
}
public AbstractDownloadProviderFunctionalTest(FakeSystemFacade systemFacade) {
- super(DownloadService.class);
+ super(DownloadJobService.class);
mSystemFacade = systemFacade;
}
@@ -177,7 +168,7 @@ public abstract class AbstractDownloadProviderFunctionalTest extends
setContext(mTestContext);
setupService();
- getService().mSystemFacade = mSystemFacade;
+ Helpers.setSystemFacade(mSystemFacade);
mSystemFacade.setUp();
assertTrue(isDatabaseEmpty()); // ensure we're not messing with real data
@@ -193,6 +184,12 @@ public abstract class AbstractDownloadProviderFunctionalTest extends
super.tearDown();
}
+ protected void startDownload(long id) {
+ final JobParameters params = mock(JobParameters.class);
+ when(params.getJobId()).thenReturn((int) id);
+ getService().onStartJob(params);
+ }
+
private boolean isDatabaseEmpty() {
Cursor cursor = mResolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
null, null, null, null);
diff --git a/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java b/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
index c0a1108..3a585b4 100644
--- a/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
@@ -115,13 +115,13 @@ public abstract class AbstractPublicApiTest extends AbstractDownloadProviderFunc
void runUntilStatus(int status) throws TimeoutException {
final long startMillis = mSystemFacade.currentTimeMillis();
- startService(null);
+ startDownload(mId);
waitForStatus(status, startMillis);
}
void runUntilStatus(int status, long timeout) throws TimeoutException {
final long startMillis = mSystemFacade.currentTimeMillis();
- startService(null);
+ startDownload(mId);
waitForStatus(status, startMillis, timeout);
}
@@ -169,7 +169,7 @@ public abstract class AbstractPublicApiTest extends AbstractDownloadProviderFunc
// waits until progress_so_far is >= (progress)%
boolean runUntilProgress(int progress) throws InterruptedException {
- startService(null);
+ startDownload(mId);
int sleepCounter = MAX_TIME_TO_WAIT_FOR_OPERATION * 1000 / TIME_TO_SLEEP;
int numBytesReceivedSoFar = 0;
@@ -230,6 +230,7 @@ public abstract class AbstractPublicApiTest extends AbstractDownloadProviderFunc
return PACKAGE_NAME;
}
});
+ mManager.setAccessFilename(true);
}
protected DownloadManager.Request getRequest()
diff --git a/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java b/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
index 3b65104..9a4e644 100644
--- a/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
@@ -17,8 +17,10 @@
package com.android.providers.downloads;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
import static java.net.HttpURLConnection.HTTP_OK;
+import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.ConnectivityManager;
@@ -56,7 +58,6 @@ public class DownloadProviderFunctionalTest extends AbstractDownloadProviderFunc
String path = "/download_manager_test_path";
Uri downloadUri = requestDownload(path);
assertEquals(Downloads.Impl.STATUS_PENDING, getDownloadStatus(downloadUri));
- assertTrue(mTestContext.mHasServiceBeenStarted);
runUntilStatus(downloadUri, Downloads.Impl.STATUS_SUCCESS);
RecordedRequest request = takeRequest();
@@ -108,13 +109,11 @@ public class DownloadProviderFunctionalTest extends AbstractDownloadProviderFunc
// Assert that HTTP request succeeds when cleartext traffic is permitted
mSystemFacade.mCleartextTrafficPermitted = true;
Uri downloadUri = requestDownload("/path");
- assertEquals("http", downloadUri.getScheme());
runUntilStatus(downloadUri, Downloads.Impl.STATUS_SUCCESS);
// Assert that HTTP request fails when cleartext traffic is not permitted
mSystemFacade.mCleartextTrafficPermitted = false;
downloadUri = requestDownload("/path");
- assertEquals("http", downloadUri.getScheme());
runUntilStatus(downloadUri, Downloads.Impl.STATUS_BAD_REQUEST);
}
@@ -131,8 +130,8 @@ public class DownloadProviderFunctionalTest extends AbstractDownloadProviderFunc
}
private void runUntilStatus(Uri downloadUri, int expected) throws Exception {
- startService(null);
-
+ startDownload(ContentUris.parseId(downloadUri));
+
int actual = -1;
final long timeout = SystemClock.elapsedRealtime() + (15 * SECOND_IN_MILLIS);
diff --git a/tests/src/com/android/providers/downloads/FakeSystemFacade.java b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
index af5482e..eaf5e43 100644
--- a/tests/src/com/android/providers/downloads/FakeSystemFacade.java
+++ b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
@@ -1,5 +1,9 @@
package com.android.providers.downloads;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
@@ -7,16 +11,22 @@ import android.net.Network;
import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
+
public class FakeSystemFacade implements SystemFacade {
long mTimeMillis = 0;
- Network mActiveNetwork = null;
Integer mActiveNetworkType = ConnectivityManager.TYPE_WIFI;
boolean mIsRoaming = false;
boolean mIsMetered = false;
- Long mMaxBytesOverMobile = null;
- Long mRecommendedMaxBytesOverMobile = null;
+ long mMaxBytesOverMobile = Long.MAX_VALUE;
+ long mRecommendedMaxBytesOverMobile = Long.MAX_VALUE;
List<Intent> mBroadcastsSent = new ArrayList<Intent>();
boolean mCleartextTrafficPermitted = true;
private boolean mReturnActualTime = false;
@@ -26,8 +36,8 @@ public class FakeSystemFacade implements SystemFacade {
mActiveNetworkType = ConnectivityManager.TYPE_WIFI;
mIsRoaming = false;
mIsMetered = false;
- mMaxBytesOverMobile = null;
- mRecommendedMaxBytesOverMobile = null;
+ mMaxBytesOverMobile = Long.MAX_VALUE;
+ mRecommendedMaxBytesOverMobile = Long.MAX_VALUE;
mBroadcastsSent.clear();
mReturnActualTime = false;
}
@@ -46,37 +56,44 @@ public class FakeSystemFacade implements SystemFacade {
@Override
public Network getActiveNetwork(int uid) {
- return mActiveNetwork;
+ if (mActiveNetworkType == null) {
+ return null;
+ } else {
+ final Network network = mock(Network.class);
+ try {
+ when(network.openConnection(any())).then(new Answer<URLConnection>() {
+ @Override
+ public URLConnection answer(InvocationOnMock invocation) throws Throwable {
+ final URL url = (URL) invocation.getArguments()[0];
+ return url.openConnection();
+ }
+ });
+ } catch (IOException ignored) {
+ }
+ return network;
+ }
}
@Override
- public NetworkInfo getActiveNetworkInfo(int uid) {
+ public NetworkInfo getNetworkInfo(Network network) {
if (mActiveNetworkType == null) {
return null;
} else {
final NetworkInfo info = new NetworkInfo(mActiveNetworkType, 0, null, null);
info.setDetailedState(DetailedState.CONNECTED, null, null);
+ info.setRoaming(mIsRoaming);
+ info.setMetered(mIsMetered);
return info;
}
}
@Override
- public boolean isActiveNetworkMetered() {
- return mIsMetered;
- }
-
- @Override
- public boolean isNetworkRoaming() {
- return mIsRoaming;
- }
-
- @Override
- public Long getMaxBytesOverMobile() {
+ public long getMaxBytesOverMobile() {
return mMaxBytesOverMobile;
}
@Override
- public Long getRecommendedMaxBytesOverMobile() {
+ public long getRecommendedMaxBytesOverMobile() {
return mRecommendedMaxBytesOverMobile;
}
diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
index 17fed6d..97bc4a2 100644
--- a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
@@ -20,12 +20,7 @@ import static android.app.DownloadManager.STATUS_FAILED;
import static android.app.DownloadManager.STATUS_PAUSED;
import static android.net.TrafficStats.GB_IN_BYTES;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
-import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
-import static java.net.HttpURLConnection.HTTP_OK;
-import static java.net.HttpURLConnection.HTTP_PARTIAL;
-import static java.net.HttpURLConnection.HTTP_PRECON_FAILED;
-import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
+
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.isA;
@@ -34,10 +29,16 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_PARTIAL;
+import static java.net.HttpURLConnection.HTTP_PRECON_FAILED;
+import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
+
import android.app.DownloadManager;
import android.app.Notification;
import android.app.NotificationManager;
-import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.ConnectivityManager;
@@ -49,14 +50,12 @@ import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.Suppress;
import android.text.format.DateUtils;
-import com.android.providers.downloads.Constants;
-import com.android.providers.downloads.DownloadReceiver;
+import libcore.io.IoUtils;
+
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.RecordedRequest;
import com.google.mockwebserver.SocketPolicy;
-import libcore.io.IoUtils;
-
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -83,10 +82,8 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
protected void setUp() throws Exception {
super.setUp();
- mNotifManager = (NotificationManager) getContext()
- .getSystemService(Context.NOTIFICATION_SERVICE);
- mDownloadManager = (DownloadManager) getContext()
- .getSystemService(Context.DOWNLOAD_SERVICE);
+ mNotifManager = getContext().getSystemService(NotificationManager.class);
+ mDownloadManager = getContext().getSystemService(DownloadManager.class);
mTestDirectory = new File(Environment.getExternalStorageDirectory() + File.separator
+ "download_manager_functional_test");
@@ -398,10 +395,12 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
mSystemFacade.mMaxBytesOverMobile = (long) FILE_CONTENT.length() - 1;
mSystemFacade.mActiveNetworkType = ConnectivityManager.TYPE_MOBILE;
+ mSystemFacade.mIsMetered = true;
Download download = enqueueRequest(getRequest());
download.runUntilStatus(DownloadManager.STATUS_PAUSED);
mSystemFacade.mActiveNetworkType = ConnectivityManager.TYPE_WIFI;
+ mSystemFacade.mIsMetered = false;
// first response was read, but aborted after the DL manager processed the Content-Length
// header, so we need to enqueue a second one
download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
@@ -544,7 +543,7 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
Download download = enqueueRequest(getRequest());
DownloadReceiver receiver = new DownloadReceiver();
- receiver.mSystemFacade = mSystemFacade;
+ Helpers.setSystemFacade(mSystemFacade);
Intent intent = new Intent(Constants.ACTION_LIST);
intent.setData(Uri.parse(Downloads.Impl.CONTENT_URI + "/" + download.mId));
intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
@@ -561,7 +560,7 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
Download download = enqueueRequest(getRequest());
DownloadReceiver receiver = new DownloadReceiver();
- receiver.mSystemFacade = mSystemFacade;
+ Helpers.setSystemFacade(mSystemFacade);
Intent intent = new Intent(Constants.ACTION_CANCEL);
intent.setData(Uri.parse(Downloads.Impl.CONTENT_URI + "/" + download.mId));
@@ -592,6 +591,7 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
enqueueResponse(buildEmptyResponse(HTTP_OK));
mSystemFacade.mActiveNetworkType = ConnectivityManager.TYPE_MOBILE;
+ mSystemFacade.mIsMetered = true;
// by default, use any connection
Download download = enqueueRequest(getRequest());
@@ -603,6 +603,7 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
download.runUntilStatus(DownloadManager.STATUS_PAUSED);
// ...then enable wifi
mSystemFacade.mActiveNetworkType = ConnectivityManager.TYPE_WIFI;
+ mSystemFacade.mIsMetered = false;
download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
}
@@ -632,6 +633,7 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
assertTrue(mResolver.mNotifyWasCalled);
}
+ @Suppress
public void testNotificationNever() throws Exception {
enqueueResponse(buildEmptyResponse(HTTP_OK));
@@ -639,10 +641,11 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
getRequest().setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN));
download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
- verify(mNotifManager, times(1)).cancelAll();
+ // TODO: verify different notif types with tags
verify(mNotifManager, never()).notify(anyString(), anyInt(), isA(Notification.class));
}
+ @Suppress
public void testNotificationVisible() throws Exception {
enqueueResponse(buildEmptyResponse(HTTP_OK));
@@ -651,10 +654,10 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
// TODO: verify different notif types with tags
- verify(mNotifManager, times(1)).cancelAll();
verify(mNotifManager, atLeastOnce()).notify(anyString(), anyInt(), isA(Notification.class));
}
+ @Suppress
public void testNotificationVisibleComplete() throws Exception {
enqueueResponse(buildEmptyResponse(HTTP_OK));
@@ -663,7 +666,6 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
// TODO: verify different notif types with tags
- verify(mNotifManager, times(1)).cancelAll();
verify(mNotifManager, atLeastOnce()).notify(anyString(), anyInt(), isA(Notification.class));
}
diff --git a/tests/src/com/android/providers/downloads/ThreadingTest.java b/tests/src/com/android/providers/downloads/ThreadingTest.java
index 1e50144..dda4db5 100644
--- a/tests/src/com/android/providers/downloads/ThreadingTest.java
+++ b/tests/src/com/android/providers/downloads/ThreadingTest.java
@@ -46,19 +46,6 @@ public class ThreadingTest extends AbstractPublicApiTest {
super.tearDown();
}
- /**
- * Test for race conditions when the service is flooded with startService() calls while running
- * a download.
- */
- public void testFloodServiceWithStarts() throws Exception {
- enqueueResponse(buildResponse(HTTP_OK, FILE_CONTENT));
- Download download = enqueueRequest(getRequest());
- while (download.getStatus() != DownloadManager.STATUS_SUCCESSFUL) {
- startService(null);
- Thread.sleep(10);
- }
- }
-
public void testFilenameRace() throws Exception {
final List<Pair<Download, String>> downloads = Lists.newArrayList();
final HashSet<String> expectedBodies = Sets.newHashSet();
@@ -73,12 +60,10 @@ public class ThreadingTest extends AbstractPublicApiTest {
final Download d = enqueueRequest(getRequest());
downloads.add(Pair.create(d, body));
expectedBodies.add(body);
+ startDownload(d.mId);
}
- // Kick off downloads in parallel
final long startMillis = mSystemFacade.currentTimeMillis();
- startService(null);
-
for (Pair<Download,String> d : downloads) {
d.first.waitForStatus(DownloadManager.STATUS_SUCCESSFUL, startMillis);
}
diff --git a/ui/res/values/strings.xml b/ui/res/values/strings.xml
index 9e625ac..3555e23 100644
--- a/ui/res/values/strings.xml
+++ b/ui/res/values/strings.xml
@@ -106,6 +106,10 @@
<!-- Text for button appearing in a dialog to restart a download, either one that failed or one
for which the downloaded file is now missing [CHAR LIMIT=25] -->
<string name="retry_download">Retry</string>
+ <!-- Text for button to start a download over the mobile connection now, even though it's over
+ the carrier-specified recommended maximum size for downloads over the mobile connection
+ [CHAR LIMIT=25] -->
+ <string name="start_now_download">Start now</string>
<!-- Text for button appearing in the pop-up selection menu to deselect all currently selected
downloads in the download list [CHAR LIMIT=25] -->
<string name="deselect_all">Deselect all</string>
diff --git a/ui/src/com/android/providers/downloads/ui/TrampolineActivity.java b/ui/src/com/android/providers/downloads/ui/TrampolineActivity.java
index 104f144..5d4e7a4 100644
--- a/ui/src/com/android/providers/downloads/ui/TrampolineActivity.java
+++ b/ui/src/com/android/providers/downloads/ui/TrampolineActivity.java
@@ -47,6 +47,7 @@ public class TrampolineActivity extends Activity {
private static final String KEY_ID = "id";
private static final String KEY_REASON = "reason";
+ private static final String KEY_SIZE = "size";
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -59,12 +60,15 @@ public class TrampolineActivity extends Activity {
final int status;
final int reason;
+ final long size;
final Cursor cursor = dm.query(new Query().setFilterById(id));
try {
if (cursor.moveToFirst()) {
status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
reason = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON));
+ size = cursor.getLong(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
} else {
Toast.makeText(this, R.string.dialog_file_missing_body, Toast.LENGTH_SHORT).show();
finish();
@@ -84,7 +88,7 @@ public class TrampolineActivity extends Activity {
case DownloadManager.STATUS_PAUSED:
if (reason == DownloadManager.PAUSED_QUEUED_FOR_WIFI) {
- PausedDialogFragment.show(getFragmentManager(), id);
+ PausedDialogFragment.show(getFragmentManager(), id, size);
} else {
sendRunningDownloadClickedBroadcast(id);
finish();
@@ -113,10 +117,11 @@ public class TrampolineActivity extends Activity {
}
public static class PausedDialogFragment extends DialogFragment {
- public static void show(FragmentManager fm, long id) {
+ public static void show(FragmentManager fm, long id, long size) {
final PausedDialogFragment dialog = new PausedDialogFragment();
final Bundle args = new Bundle();
args.putLong(KEY_ID, id);
+ args.putLong(KEY_SIZE, size);
dialog.setArguments(args);
dialog.show(fm, TAG_PAUSED);
}
@@ -130,13 +135,27 @@ public class TrampolineActivity extends Activity {
dm.setAccessAllDownloads(true);
final long id = getArguments().getLong(KEY_ID);
+ final long size = getArguments().getLong(KEY_SIZE);
final AlertDialog.Builder builder = new AlertDialog.Builder(
- context, AlertDialog.THEME_HOLO_LIGHT);
+ context, android.R.style.Theme_DeviceDefault_Light_Dialog_Alert);
builder.setTitle(R.string.dialog_title_queued_body);
builder.setMessage(R.string.dialog_queued_body);
- builder.setPositiveButton(R.string.keep_queued_download, null);
+ final Long maxSize = DownloadManager.getMaxBytesOverMobile(context);
+ if (maxSize != null && size > maxSize) {
+ // When we have a max size, we have no choice
+ builder.setPositiveButton(R.string.keep_queued_download, null);
+ } else {
+ // Give user the choice of starting now
+ builder.setPositiveButton(R.string.start_now_download,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dm.forceDownload(id);
+ }
+ });
+ }
builder.setNegativeButton(
R.string.remove_download, new DialogInterface.OnClickListener() {
@@ -181,10 +200,9 @@ public class TrampolineActivity extends Activity {
final int reason = getArguments().getInt(KEY_REASON);
final AlertDialog.Builder builder = new AlertDialog.Builder(
- context, AlertDialog.THEME_HOLO_LIGHT);
+ context, android.R.style.Theme_DeviceDefault_Light_Dialog_Alert);
builder.setTitle(R.string.dialog_title_not_available);
- final String message;
switch (reason) {
case DownloadManager.ERROR_FILE_ALREADY_EXISTS:
builder.setMessage(R.string.dialog_file_already_exists);