Move DownloadManager to use JobScheduler.
Jeff Sharkey [Thu, 21 Apr 2016 05:23:09 +0000 (23:23 -0600)]
JobScheduler is in a much better position to coordinate tasks across
the platform to optimize battery and RAM usage.  This change removes
a bunch of manual scheduling logic by representing each download as
a separate job with relevant scheduling constraints.  Requested
network types, retry backoff timing, and newly added charging and
idle constraints are plumbed through as job parameters.

When a job times out, we halt the download and schedule it to resume
later.  The majority of downloads should have ETag values to enable
resuming like this.

Remove local wakelocks, since the platform now acquires and blames
our jobs on the requesting app.

When an active download is pushing updates to the database, check for
both paused and cancelled state to quickly halt an ongoing download.

Shift DownloadNotifier to update directly based on a Cursor, since we
no longer have the overhead of fully-parsed DownloadInfo objects.

Unify a handful of worker threads into a single shared thread.

Remove legacy "large download" activity that was thrown in the face
of the user; the UX best-practice is to go through notification, and
update that dialog to let the user override and continue if under
the hard limit.

Bug: 28098882, 26571724
Change-Id: I33ebe59b3c2ea9c89ec526f70b1950c734abc4a7

23 files changed:
AndroidManifest.xml
src/com/android/providers/downloads/Constants.java
src/com/android/providers/downloads/DownloadIdleService.java
src/com/android/providers/downloads/DownloadInfo.java
src/com/android/providers/downloads/DownloadJobService.java [new file with mode: 0644]
src/com/android/providers/downloads/DownloadNotifier.java
src/com/android/providers/downloads/DownloadProvider.java
src/com/android/providers/downloads/DownloadReceiver.java
src/com/android/providers/downloads/DownloadScanner.java
src/com/android/providers/downloads/DownloadService.java [deleted file]
src/com/android/providers/downloads/DownloadThread.java
src/com/android/providers/downloads/Helpers.java
src/com/android/providers/downloads/RealSystemFacade.java
src/com/android/providers/downloads/SizeLimitActivity.java [deleted file]
src/com/android/providers/downloads/SystemFacade.java
tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
tests/src/com/android/providers/downloads/FakeSystemFacade.java
tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
tests/src/com/android/providers/downloads/ThreadingTest.java
ui/res/values/strings.xml
ui/src/com/android/providers/downloads/ui/TrampolineActivity.java

index 0ba8ead..8720523 100644 (file)
         </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>
                 <data android:scheme="file" />
             </intent-filter>
         </receiver>
-
-        <activity android:name=".SizeLimitActivity"
-                  android:launchMode="singleTask"
-                  android:theme="@style/Theme.Translucent"/>
     </application>
 </manifest>
index 6cea808..79daeae 100644 (file)
@@ -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";
 
index b537155..ecebb0f 100644 (file)
@@ -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,
index bee5c4a..c94dd6c 100644 (file)
@@ -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 (file)
index 0000000..0ce4266
--- /dev/null
@@ -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();
+        }
+    };
+}
index 5f961eb..558393d 100644 (file)
@@ -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);
     }
 }
index 78b4294..00ed043 100644 (file)
 
 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();
index 2f50dcf..a0dc694 100644 (file)
@@ -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);
     }
 }
index ca79506..37f5114 100644 (file)
@@ -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 (file)
index 7d4392e..0000000
+++ /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);
-            }
-        }
-    }
-}
index 9de563f..c559367 100644 (file)
 
 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);
     }
 
index d01cbff..b073745 100644 (file)
@@ -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
index 48df2a0..da4e01e 100644 (file)
@@ -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 (file)
index d25277d..0000000
+++ /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();
-    }
-}
index 7f97b91..e7852e9 100644 (file)
@@ -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.
index 6934b86..0330fd3 100644 (file)
 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);
index c0a1108..3a585b4 100644 (file)
@@ -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()
index 3b65104..9a4e644 100644 (file)
 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);
index af5482e..eaf5e43 100644 (file)
@@ -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;
     }
 
index 17fed6d..97bc4a2 100644 (file)
@@ -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));
     }
 
index 1e50144..dda4db5 100644 (file)
@@ -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);
         }
index 9e625ac..3555e23 100644 (file)
     <!-- 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>
index 104f144..5d4e7a4 100644 (file)
@@ -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);