diff options
Diffstat (limited to 'src/com')
14 files changed, 711 insertions, 1284 deletions
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java index 6cea8086..79daeaed 100644 --- a/src/com/android/providers/downloads/Constants.java +++ b/src/com/android/providers/downloads/Constants.java @@ -45,9 +45,6 @@ public class Constants { /** The column that is used for the initiating app's UID */ public static final String UID = "uid"; - /** The intent that gets sent when the service must wake up for a retry */ - public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP"; - /** the intent that gets sent when clicking a successful download */ public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN"; diff --git a/src/com/android/providers/downloads/DownloadIdleService.java b/src/com/android/providers/downloads/DownloadIdleService.java index b5371552..ecebb0f8 100644 --- a/src/com/android/providers/downloads/DownloadIdleService.java +++ b/src/com/android/providers/downloads/DownloadIdleService.java @@ -20,10 +20,14 @@ import static com.android.providers.downloads.Constants.TAG; import static com.android.providers.downloads.StorageUtils.listFilesRecursive; import android.app.DownloadManager; +import android.app.job.JobInfo; import android.app.job.JobParameters; +import android.app.job.JobScheduler; import android.app.job.JobService; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentUris; +import android.content.Context; import android.database.Cursor; import android.os.Environment; import android.provider.Downloads; @@ -33,11 +37,12 @@ import android.text.format.DateUtils; import android.util.Slog; import com.android.providers.downloads.StorageUtils.ConcreteFile; -import com.google.android.collect.Lists; -import com.google.android.collect.Sets; import libcore.io.IoUtils; +import com.google.android.collect.Lists; +import com.google.android.collect.Sets; + import java.io.File; import java.util.ArrayList; import java.util.HashSet; @@ -48,6 +53,7 @@ import java.util.HashSet; * deleted directly on disk. */ public class DownloadIdleService extends JobService { + private static final int IDLE_JOB_ID = -100; private class IdleRunnable implements Runnable { private JobParameters mParams; @@ -66,7 +72,7 @@ public class DownloadIdleService extends JobService { @Override public boolean onStartJob(JobParameters params) { - new Thread(new IdleRunnable(params)).start(); + Helpers.getAsyncHandler().post(new IdleRunnable(params)); return true; } @@ -77,6 +83,19 @@ public class DownloadIdleService extends JobService { return false; } + public static void scheduleIdlePass(Context context) { + final JobScheduler scheduler = context.getSystemService(JobScheduler.class); + if (scheduler.getPendingJob(IDLE_JOB_ID) == null) { + final JobInfo job = new JobInfo.Builder(IDLE_JOB_ID, + new ComponentName(context, DownloadIdleService.class)) + .setPeriodic(12 * DateUtils.HOUR_IN_MILLIS) + .setRequiresCharging(true) + .setRequiresDeviceIdle(true) + .build(); + scheduler.schedule(job); + } + } + private interface StaleQuery { final String[] PROJECTION = new String[] { Downloads.Impl._ID, diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java index bee5c4a9..c94dd6c2 100644 --- a/src/com/android/providers/downloads/DownloadInfo.java +++ b/src/com/android/providers/downloads/DownloadInfo.java @@ -19,24 +19,20 @@ package com.android.providers.downloads; import static com.android.providers.downloads.Constants.TAG; import android.app.DownloadManager; +import android.app.job.JobInfo; import android.content.ContentResolver; import android.content.ContentUris; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.NetworkInfo.DetailedState; import android.net.Uri; import android.os.Environment; import android.provider.Downloads; -import android.provider.Downloads.Impl; import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.Log; import android.util.Pair; -import com.android.internal.annotations.GuardedBy; import com.android.internal.util.IndentingPrintWriter; import java.io.CharArrayWriter; @@ -45,9 +41,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; /** * Details about a specific download. Fields should only be mutated by updating @@ -66,14 +59,6 @@ public class DownloadInfo { mCursor = cursor; } - public DownloadInfo newDownloadInfo( - Context context, SystemFacade systemFacade, DownloadNotifier notifier) { - final DownloadInfo info = new DownloadInfo(context, systemFacade, notifier); - updateFromDatabase(info); - readRequestHeaders(info); - return info; - } - public void updateFromDatabase(DownloadInfo info) { info.mId = getLong(Downloads.Impl._ID); info.mUri = getString(Downloads.Impl.COLUMN_URI); @@ -105,6 +90,7 @@ public class DownloadInfo { info.mAllowedNetworkTypes = getInt(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES); info.mAllowRoaming = getInt(Downloads.Impl.COLUMN_ALLOW_ROAMING) != 0; info.mAllowMetered = getInt(Downloads.Impl.COLUMN_ALLOW_METERED) != 0; + info.mFlags = getInt(Downloads.Impl.COLUMN_FLAGS); info.mTitle = getString(Downloads.Impl.COLUMN_TITLE); info.mDescription = getString(Downloads.Impl.COLUMN_DESCRIPTION); info.mBypassRecommendedSizeLimit = @@ -115,7 +101,7 @@ public class DownloadInfo { } } - private void readRequestHeaders(DownloadInfo info) { + public void readRequestHeaders(DownloadInfo info) { info.mRequestHeaders.clear(); Uri headerUri = Uri.withAppendedPath( info.getAllDownloadsUri(), Downloads.Impl.RequestHeaders.URI_SEGMENT); @@ -159,56 +145,6 @@ public class DownloadInfo { } } - /** - * Constants used to indicate network state for a specific download, after - * applying any requested constraints. - */ - public enum NetworkState { - /** - * The network is usable for the given download. - */ - OK, - - /** - * There is no network connectivity. - */ - NO_CONNECTION, - - /** - * The download exceeds the maximum size for this network. - */ - UNUSABLE_DUE_TO_SIZE, - - /** - * The download exceeds the recommended maximum size for this network, - * the user must confirm for this download to proceed without WiFi. - */ - RECOMMENDED_UNUSABLE_DUE_TO_SIZE, - - /** - * The current connection is roaming, and the download can't proceed - * over a roaming connection. - */ - CANNOT_USE_ROAMING, - - /** - * The app requesting the download specific that it can't use the - * current network connection. - */ - TYPE_DISALLOWED_BY_REQUESTOR, - - /** - * Current network is blocked for requesting application. - */ - BLOCKED; - } - - /** - * For intents used to notify the user that a download exceeds a size threshold, if this extra - * is true, WiFi is required for this download size; otherwise, it is only recommended. - */ - public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired"; - public long mId; public String mUri; @Deprecated @@ -240,33 +176,35 @@ public class DownloadInfo { public int mAllowedNetworkTypes; public boolean mAllowRoaming; public boolean mAllowMetered; + public int mFlags; public String mTitle; public String mDescription; public int mBypassRecommendedSizeLimit; - public int mFuzz; - private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>(); - /** - * Result of last {@link DownloadThread} started by - * {@link #startDownloadIfReady(ExecutorService)}. - */ - @GuardedBy("this") - private Future<?> mSubmittedTask; - - @GuardedBy("this") - private DownloadThread mTask; - private final Context mContext; private final SystemFacade mSystemFacade; - private final DownloadNotifier mNotifier; - private DownloadInfo(Context context, SystemFacade systemFacade, DownloadNotifier notifier) { + public DownloadInfo(Context context) { mContext = context; - mSystemFacade = systemFacade; - mNotifier = notifier; - mFuzz = Helpers.sRandom.nextInt(1001); + mSystemFacade = Helpers.getSystemFacade(context); + } + + public static DownloadInfo queryDownloadInfo(Context context, long downloadId) { + final ContentResolver resolver = context.getContentResolver(); + try (Cursor cursor = resolver.query( + ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, downloadId), + null, null, null, null)) { + final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); + final DownloadInfo info = new DownloadInfo(context); + if (cursor.moveToFirst()) { + reader.updateFromDatabase(info); + reader.readRequestHeaders(info); + return info; + } + } + return null; } public Collection<Pair<String, String>> getHeaders() { @@ -309,43 +247,80 @@ public class DownloadInfo { } /** - * Returns the time when a download should be restarted. + * Add random fuzz to the given delay so it's anywhere between 1-1.5x the + * requested delay. + */ + private long fuzzDelay(long delay) { + return delay + Helpers.sRandom.nextInt((int) (delay / 2)); + } + + /** + * Return minimum latency in milliseconds required before this download is + * allowed to start again. + * + * @see android.app.job.JobInfo.Builder#setMinimumLatency(long) + */ + public long getMinimumLatency() { + if (mStatus == Downloads.Impl.STATUS_WAITING_TO_RETRY) { + final long now = mSystemFacade.currentTimeMillis(); + final long startAfter; + if (mNumFailed == 0) { + startAfter = now; + } else if (mRetryAfter > 0) { + startAfter = mLastMod + fuzzDelay(mRetryAfter); + } else { + final long delay = (Constants.RETRY_FIRST_DELAY * DateUtils.SECOND_IN_MILLIS + * (1 << (mNumFailed - 1))); + startAfter = mLastMod + fuzzDelay(delay); + } + return Math.max(0, startAfter - now); + } else { + return 0; + } + } + + /** + * Return the network type constraint required by this download. + * + * @see android.app.job.JobInfo.Builder#setRequiredNetworkType(int) */ - public long restartTime(long now) { - if (mNumFailed == 0) { - return now; + public int getRequiredNetworkType(long totalBytes) { + if (!mAllowMetered) { + return JobInfo.NETWORK_TYPE_UNMETERED; + } + if (mAllowedNetworkTypes == DownloadManager.Request.NETWORK_WIFI) { + return JobInfo.NETWORK_TYPE_UNMETERED; } - if (mRetryAfter > 0) { - return mLastMod + mRetryAfter; + if (totalBytes > mSystemFacade.getMaxBytesOverMobile()) { + return JobInfo.NETWORK_TYPE_UNMETERED; } - return mLastMod + - Constants.RETRY_FIRST_DELAY * - (1000 + mFuzz) * (1 << (mNumFailed - 1)); + if (totalBytes > mSystemFacade.getRecommendedMaxBytesOverMobile() + && mBypassRecommendedSizeLimit == 0) { + return JobInfo.NETWORK_TYPE_UNMETERED; + } + if (!mAllowRoaming) { + return JobInfo.NETWORK_TYPE_NOT_ROAMING; + } + return JobInfo.NETWORK_TYPE_ANY; } /** - * Returns whether this download should be enqueued. + * Returns whether this download is ready to be scheduled. */ - private boolean isReadyToDownload() { + public boolean isReadyToSchedule() { if (mControl == Downloads.Impl.CONTROL_PAUSED) { // the download is paused, so it's not going to start return false; } switch (mStatus) { - case 0: // status hasn't been initialized yet, this is a new download - case Downloads.Impl.STATUS_PENDING: // download is explicit marked as ready to start - case Downloads.Impl.STATUS_RUNNING: // download interrupted (process killed etc) while - // running, without a chance to update the database - return true; - + case 0: + case Downloads.Impl.STATUS_PENDING: + case Downloads.Impl.STATUS_RUNNING: case Downloads.Impl.STATUS_WAITING_FOR_NETWORK: + case Downloads.Impl.STATUS_WAITING_TO_RETRY: case Downloads.Impl.STATUS_QUEUED_FOR_WIFI: - return checkCanUseNetwork(mTotalBytes) == NetworkState.OK; + return true; - case Downloads.Impl.STATUS_WAITING_TO_RETRY: - // download was waiting for a delayed restart - final long now = mSystemFacade.currentTimeMillis(); - return restartTime(now) <= now; case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR: // is the media mounted? final Uri uri = Uri.parse(mUri); @@ -357,11 +332,10 @@ public class DownloadInfo { Log.w(TAG, "Expected file URI on external storage: " + mUri); return false; } - case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR: - // avoids repetition of retrying download + + default: return false; } - return false; } /** @@ -378,27 +352,7 @@ public class DownloadInfo { return false; } - /** - * Returns whether this download is allowed to use the network. - */ - public NetworkState checkCanUseNetwork(long totalBytes) { - final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mUid); - if (info == null || !info.isConnected()) { - return NetworkState.NO_CONNECTION; - } - if (DetailedState.BLOCKED.equals(info.getDetailedState())) { - return NetworkState.BLOCKED; - } - if (mSystemFacade.isNetworkRoaming() && !isRoamingAllowed()) { - return NetworkState.CANNOT_USE_ROAMING; - } - if (mSystemFacade.isActiveNetworkMetered() && !mAllowMetered) { - return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR; - } - return checkIsNetworkTypeAllowed(info.getType(), totalBytes); - } - - private boolean isRoamingAllowed() { + public boolean isRoamingAllowed() { if (mIsPublicApi) { return mAllowRoaming; } else { // legacy behavior @@ -406,112 +360,6 @@ public class DownloadInfo { } } - /** - * Check if this download can proceed over the given network type. - * @param networkType a constant from ConnectivityManager.TYPE_*. - * @return one of the NETWORK_* constants - */ - private NetworkState checkIsNetworkTypeAllowed(int networkType, long totalBytes) { - if (mIsPublicApi) { - final int flag = translateNetworkTypeToApiFlag(networkType); - final boolean allowAllNetworkTypes = mAllowedNetworkTypes == ~0; - if (!allowAllNetworkTypes && (flag & mAllowedNetworkTypes) == 0) { - return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR; - } - } - return checkSizeAllowedForNetwork(networkType, totalBytes); - } - - /** - * Translate a ConnectivityManager.TYPE_* constant to the corresponding - * DownloadManager.Request.NETWORK_* bit flag. - */ - private int translateNetworkTypeToApiFlag(int networkType) { - switch (networkType) { - case ConnectivityManager.TYPE_MOBILE: - return DownloadManager.Request.NETWORK_MOBILE; - - case ConnectivityManager.TYPE_WIFI: - return DownloadManager.Request.NETWORK_WIFI; - - case ConnectivityManager.TYPE_BLUETOOTH: - return DownloadManager.Request.NETWORK_BLUETOOTH; - - default: - return 0; - } - } - - /** - * Check if the download's size prohibits it from running over the current network. - * @return one of the NETWORK_* constants - */ - private NetworkState checkSizeAllowedForNetwork(int networkType, long totalBytes) { - if (totalBytes <= 0) { - // we don't know the size yet - return NetworkState.OK; - } - - if (ConnectivityManager.isNetworkTypeMobile(networkType)) { - Long maxBytesOverMobile = mSystemFacade.getMaxBytesOverMobile(); - if (maxBytesOverMobile != null && totalBytes > maxBytesOverMobile) { - return NetworkState.UNUSABLE_DUE_TO_SIZE; - } - if (mBypassRecommendedSizeLimit == 0) { - Long recommendedMaxBytesOverMobile = mSystemFacade - .getRecommendedMaxBytesOverMobile(); - if (recommendedMaxBytesOverMobile != null - && totalBytes > recommendedMaxBytesOverMobile) { - return NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE; - } - } - } - - return NetworkState.OK; - } - - /** - * If download is ready to start, and isn't already pending or executing, - * create a {@link DownloadThread} and enqueue it into given - * {@link Executor}. - * - * @return If actively downloading. - */ - public boolean startDownloadIfReady(ExecutorService executor) { - synchronized (this) { - final boolean isReady = isReadyToDownload(); - final boolean isActive = mSubmittedTask != null && !mSubmittedTask.isDone(); - if (isReady && !isActive) { - if (mStatus != Impl.STATUS_RUNNING) { - mStatus = Impl.STATUS_RUNNING; - ContentValues values = new ContentValues(); - values.put(Impl.COLUMN_STATUS, mStatus); - mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null); - } - - mTask = new DownloadThread(mContext, mSystemFacade, mNotifier, this); - mSubmittedTask = executor.submit(mTask); - } - return isReady; - } - } - - /** - * If download is ready to be scanned, enqueue it into the given - * {@link DownloadScanner}. - * - * @return If actively scanning. - */ - public boolean startScanIfReady(DownloadScanner scanner) { - synchronized (this) { - final boolean isReady = shouldScanFile(); - if (isReady) { - scanner.requestScan(this); - } - return isReady; - } - } - public boolean isOnCache() { return (mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION || mDestination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION @@ -571,33 +419,13 @@ public class DownloadInfo { pw.printPair("mAllowedNetworkTypes", mAllowedNetworkTypes); pw.printPair("mAllowRoaming", mAllowRoaming); pw.printPair("mAllowMetered", mAllowMetered); + pw.printPair("mFlags", mFlags); pw.println(); pw.decreaseIndent(); } /** - * Return time when this download will be ready for its next action, in - * milliseconds after given time. - * - * @return If {@code 0}, download is ready to proceed immediately. If - * {@link Long#MAX_VALUE}, then download has no future actions. - */ - public long nextActionMillis(long now) { - if (Downloads.Impl.isStatusCompleted(mStatus)) { - return Long.MAX_VALUE; - } - if (mStatus != Downloads.Impl.STATUS_WAITING_TO_RETRY) { - return 0; - } - long when = restartTime(now); - if (when <= now) { - return 0; - } - return when - now; - } - - /** * Returns whether a file should be scanned */ public boolean shouldScanFile() { @@ -608,33 +436,25 @@ public class DownloadInfo { && Downloads.Impl.isStatusSuccess(mStatus); } - void notifyPauseDueToSize(boolean isWifiRequired) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(getAllDownloadsUri()); - intent.setClassName(SizeLimitActivity.class.getPackage().getName(), - SizeLimitActivity.class.getName()); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(EXTRA_IS_WIFI_REQUIRED, isWifiRequired); - mContext.startActivity(intent); - } - /** * Query and return status of requested download. */ - public static int queryDownloadStatus(ContentResolver resolver, long id) { - final Cursor cursor = resolver.query( - ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id), - new String[] { Downloads.Impl.COLUMN_STATUS }, null, null, null); - try { + public int queryDownloadStatus() { + return queryDownloadInt(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING); + } + + public int queryDownloadControl() { + return queryDownloadInt(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN); + } + + public int queryDownloadInt(String columnName, int defaultValue) { + try (Cursor cursor = mContext.getContentResolver().query(getAllDownloadsUri(), + new String[] { columnName }, null, null, null)) { if (cursor.moveToFirst()) { return cursor.getInt(0); } else { - // TODO: increase strictness of value returned for unknown - // downloads; this is safe default for now. - return Downloads.Impl.STATUS_PENDING; + return defaultValue; } - } finally { - cursor.close(); } } } diff --git a/src/com/android/providers/downloads/DownloadJobService.java b/src/com/android/providers/downloads/DownloadJobService.java new file mode 100644 index 00000000..0ce4266a --- /dev/null +++ b/src/com/android/providers/downloads/DownloadJobService.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.providers.downloads; + +import static android.provider.Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI; + +import static com.android.providers.downloads.Constants.TAG; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.database.ContentObserver; +import android.util.Log; +import android.util.SparseArray; + +/** + * Service that hosts download jobs. Each active download job is handled as a + * unique {@link DownloadThread} instance. + * <p> + * The majority of downloads should have ETag values to enable resuming, so if a + * given download isn't able to finish in the normal job timeout (10 minutes), + * we just reschedule the job and resume again in the future. + */ +public class DownloadJobService extends JobService { + // @GuardedBy("mActiveThreads") + private SparseArray<DownloadThread> mActiveThreads = new SparseArray<>(); + + @Override + public void onCreate() { + super.onCreate(); + + // While someone is bound to us, watch for database changes that should + // trigger notification updates. + getContentResolver().registerContentObserver(ALL_DOWNLOADS_CONTENT_URI, true, mObserver); + } + + @Override + public void onDestroy() { + super.onDestroy(); + getContentResolver().unregisterContentObserver(mObserver); + } + + @Override + public boolean onStartJob(JobParameters params) { + final int id = params.getJobId(); + + // Spin up thread to handle this download + final DownloadInfo info = DownloadInfo.queryDownloadInfo(this, id); + if (info == null) { + Log.w(TAG, "Odd, no details found for download " + id); + return false; + } + + final DownloadThread thread; + synchronized (mActiveThreads) { + thread = new DownloadThread(this, params, info); + mActiveThreads.put(id, thread); + } + thread.start(); + + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + final int id = params.getJobId(); + + final DownloadThread thread; + synchronized (mActiveThreads) { + thread = mActiveThreads.removeReturnOld(id); + } + if (thread != null) { + // If the thread is still running, ask it to gracefully shutdown, + // and reschedule ourselves to resume in the future. + thread.requestShutdown(); + + Helpers.scheduleJob(this, DownloadInfo.queryDownloadInfo(this, id)); + } + return false; + } + + public void jobFinishedInternal(JobParameters params, boolean needsReschedule) { + synchronized (mActiveThreads) { + mActiveThreads.remove(params.getJobId()); + } + + // Update notifications one last time while job is protecting us + mObserver.onChange(false); + + jobFinished(params, needsReschedule); + } + + private ContentObserver mObserver = new ContentObserver(Helpers.getAsyncHandler()) { + @Override + public void onChange(boolean selfChange) { + Helpers.getDownloadNotifier(DownloadJobService.this).update(); + } + }; +} diff --git a/src/com/android/providers/downloads/DownloadNotifier.java b/src/com/android/providers/downloads/DownloadNotifier.java index 86caa33d..558393d6 100644 --- a/src/com/android/providers/downloads/DownloadNotifier.java +++ b/src/com/android/providers/downloads/DownloadNotifier.java @@ -20,6 +20,7 @@ import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE; import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED; import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION; import static android.provider.Downloads.Impl.STATUS_RUNNING; + import static com.android.providers.downloads.Constants.TAG; import android.app.DownloadManager; @@ -30,31 +31,27 @@ import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.database.Cursor; import android.net.Uri; import android.os.SystemClock; import android.provider.Downloads; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.text.format.DateUtils; +import android.util.ArrayMap; +import android.util.IntArray; import android.util.Log; import android.util.LongSparseLongArray; import com.android.internal.util.ArrayUtils; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; - import java.text.NumberFormat; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; import javax.annotation.concurrent.GuardedBy; /** - * Update {@link NotificationManager} to reflect current {@link DownloadInfo} - * states. Collapses similar downloads into a single notification, and builds + * Update {@link NotificationManager} to reflect current download states. + * Collapses similar downloads into a single notification, and builds * {@link PendingIntent} that launch towards {@link DownloadReceiver}. */ public class DownloadNotifier { @@ -70,20 +67,20 @@ public class DownloadNotifier { * Currently active notifications, mapped from clustering tag to timestamp * when first shown. * - * @see #buildNotificationTag(DownloadInfo) + * @see #buildNotificationTag(Cursor) */ @GuardedBy("mActiveNotifs") - private final HashMap<String, Long> mActiveNotifs = Maps.newHashMap(); + private final ArrayMap<String, Long> mActiveNotifs = new ArrayMap<>(); /** - * Current speed of active downloads, mapped from {@link DownloadInfo#mId} - * to speed in bytes per second. + * Current speed of active downloads, mapped from download ID to speed in + * bytes per second. */ @GuardedBy("mDownloadSpeed") private final LongSparseLongArray mDownloadSpeed = new LongSparseLongArray(); /** - * Last time speed was reproted, mapped from {@link DownloadInfo#mId} to + * Last time speed was reproted, mapped from download ID to * {@link SystemClock#elapsedRealtime()}. */ @GuardedBy("mDownloadSpeed") @@ -123,49 +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; } - @GuardedBy("mActiveNotifs") - 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( @@ -192,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(); @@ -218,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; @@ -236,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)); @@ -253,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); } } } @@ -282,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); } @@ -299,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)); } @@ -312,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) { @@ -339,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; } @@ -378,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; } @@ -396,21 +425,21 @@ public class DownloadNotifier { /** * Return the cluster type of the given tag, as created by - * {@link #buildNotificationTag(DownloadInfo)}. + * {@link #buildNotificationTag(Cursor)}. */ private static int getNotificationTagType(String tag) { return Integer.parseInt(tag.substring(0, tag.indexOf(':'))); } - private static boolean isActiveAndVisible(DownloadInfo download) { - return download.mStatus == STATUS_RUNNING && - (download.mVisibility == VISIBILITY_VISIBLE - || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + private static boolean isActiveAndVisible(int status, int visibility) { + return status == STATUS_RUNNING && + (visibility == VISIBILITY_VISIBLE + || visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED); } - private static boolean isCompleteAndVisible(DownloadInfo download) { - return Downloads.Impl.isStatusCompleted(download.mStatus) && - (download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED - || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION); + private static boolean isCompleteAndVisible(int status, int visibility) { + return Downloads.Impl.isStatusCompleted(status) && + (visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED + || visibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION); } } diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java index 78b42949..00ed043d 100644 --- a/src/com/android/providers/downloads/DownloadProvider.java +++ b/src/com/android/providers/downloads/DownloadProvider.java @@ -16,9 +16,14 @@ package com.android.providers.downloads; +import static android.provider.BaseColumns._ID; +import static android.provider.Downloads.Impl.COLUMN_MEDIAPROVIDER_URI; +import static android.provider.Downloads.Impl._DATA; + import android.app.AppOpsManager; import android.app.DownloadManager; import android.app.DownloadManager.Request; +import android.app.job.JobScheduler; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; @@ -35,8 +40,6 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.os.Binder; -import android.os.Handler; -import android.os.HandlerThread; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.OnCloseListener; import android.os.Process; @@ -47,9 +50,10 @@ import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; +import com.android.internal.util.IndentingPrintWriter; + import libcore.io.IoUtils; -import com.android.internal.util.IndentingPrintWriter; import com.google.android.collect.Maps; import com.google.common.annotations.VisibleForTesting; @@ -73,7 +77,7 @@ public final class DownloadProvider extends ContentProvider { /** Database filename */ private static final String DB_NAME = "downloads.db"; /** Current database version */ - private static final int DB_VERSION = 109; + private static final int DB_VERSION = 110; /** Name of table in the database */ private static final String DB_TABLE = "downloads"; @@ -170,7 +174,8 @@ public final class DownloadProvider extends ContentProvider { private static final List<String> downloadManagerColumnsList = Arrays.asList(DownloadManager.UNDERLYING_COLUMNS); - private Handler mHandler; + @VisibleForTesting + SystemFacade mSystemFacade; /** The database that lies underneath this content provider */ private SQLiteOpenHelper mOpenHelper = null; @@ -179,9 +184,6 @@ public final class DownloadProvider extends ContentProvider { private int mSystemUid = -1; private int mDefContainerUid = -1; - @VisibleForTesting - SystemFacade mSystemFacade; - /** * This class encapsulates a SQL where clause and its parameters. It makes it possible for * shared methods (like {@link DownloadProvider#getWhereClause(Uri, String, String[], int)}) @@ -329,6 +331,11 @@ public final class DownloadProvider extends ContentProvider { "BOOLEAN NOT NULL DEFAULT 0"); break; + case 110: + addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_FLAGS, + "INTEGER NOT NULL DEFAULT 0"); + break; + default: throw new IllegalStateException("Don't know how to upgrade to " + version); } @@ -442,11 +449,6 @@ public final class DownloadProvider extends ContentProvider { mSystemFacade = new RealSystemFacade(getContext()); } - HandlerThread handlerThread = - new HandlerThread("DownloadProvider handler", Process.THREAD_PRIORITY_BACKGROUND); - handlerThread.start(); - mHandler = new Handler(handlerThread.getLooper()); - mOpenHelper = new DatabaseHelper(getContext()); // Initialize the system uid mSystemUid = Process.SYSTEM_UID; @@ -462,10 +464,6 @@ public final class DownloadProvider extends ContentProvider { if (appInfo != null) { mDefContainerUid = appInfo.uid; } - // start the DownloadService class. don't wait for the 1st download to be issued. - // saves us by getting some initialization code in DownloadService out of the way. - Context context = getContext(); - context.startService(new Intent(context, DownloadService.class)); return true; } @@ -669,6 +667,7 @@ public final class DownloadProvider extends ContentProvider { copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues); copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues); copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues); + copyInteger(Downloads.Impl.COLUMN_FLAGS, values, filteredValues); } if (Constants.LOGVV) { @@ -689,9 +688,12 @@ public final class DownloadProvider extends ContentProvider { insertRequestHeaders(db, rowID, values); notifyContentChanged(uri, match); - // Always start service to handle notifications and/or scanning - final Context context = getContext(); - context.startService(new Intent(context, DownloadService.class)); + final long token = Binder.clearCallingIdentity(); + try { + Helpers.scheduleJob(getContext(), rowID); + } finally { + Binder.restoreCallingIdentity(token); + } return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID); } @@ -806,6 +808,7 @@ public final class DownloadProvider extends ContentProvider { values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES); values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING); values.remove(Downloads.Impl.COLUMN_ALLOW_METERED); + values.remove(Downloads.Impl.COLUMN_FLAGS); values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI); values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED); values.remove(Downloads.Impl.COLUMN_ALLOW_WRITE); @@ -1053,14 +1056,7 @@ public final class DownloadProvider extends ContentProvider { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; - boolean startService = false; - - if (values.containsKey(Downloads.Impl.COLUMN_DELETED)) { - if (values.getAsInteger(Downloads.Impl.COLUMN_DELETED) == 1) { - // some rows are to be 'deleted'. need to start DownloadService. - startService = true; - } - } + boolean updateSchedule = false; ContentValues filteredValues; if (Binder.getCallingPid() != Process.myPid()) { @@ -1070,7 +1066,7 @@ public final class DownloadProvider extends ContentProvider { Integer i = values.getAsInteger(Downloads.Impl.COLUMN_CONTROL); if (i != null) { filteredValues.put(Downloads.Impl.COLUMN_CONTROL, i); - startService = true; + updateSchedule = true; } copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues); @@ -1099,7 +1095,7 @@ public final class DownloadProvider extends ContentProvider { boolean isUserBypassingSizeLimit = values.containsKey(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT); if (isRestart || isUserBypassingSizeLimit) { - startService = true; + updateSchedule = true; } } @@ -1109,12 +1105,27 @@ public final class DownloadProvider extends ContentProvider { case MY_DOWNLOADS_ID: case ALL_DOWNLOADS: case ALL_DOWNLOADS_ID: - SqlSelection selection = getWhereClause(uri, where, whereArgs, match); - if (filteredValues.size() > 0) { - count = db.update(DB_TABLE, filteredValues, selection.getSelection(), - selection.getParameters()); - } else { + if (filteredValues.size() == 0) { count = 0; + break; + } + + final SqlSelection selection = getWhereClause(uri, where, whereArgs, match); + count = db.update(DB_TABLE, filteredValues, selection.getSelection(), + selection.getParameters()); + if (updateSchedule) { + final long token = Binder.clearCallingIdentity(); + try { + try (Cursor cursor = db.query(DB_TABLE, new String[] { _ID }, + selection.getSelection(), selection.getParameters(), + null, null, null)) { + while (cursor.moveToNext()) { + Helpers.scheduleJob(getContext(), cursor.getInt(0)); + } + } + } finally { + Binder.restoreCallingIdentity(token); + } } break; @@ -1124,10 +1135,6 @@ public final class DownloadProvider extends ContentProvider { } notifyContentChanged(uri, match); - if (startService) { - Context context = getContext(); - context.startService(new Intent(context, DownloadService.class)); - } return count; } @@ -1176,7 +1183,8 @@ public final class DownloadProvider extends ContentProvider { Helpers.validateSelection(where, sAppReadableColumnsSet); } - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final JobScheduler scheduler = getContext().getSystemService(JobScheduler.class); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; int match = sURIMatcher.match(uri); switch (match) { @@ -1184,15 +1192,16 @@ public final class DownloadProvider extends ContentProvider { case MY_DOWNLOADS_ID: case ALL_DOWNLOADS: case ALL_DOWNLOADS_ID: - SqlSelection selection = getWhereClause(uri, where, whereArgs, match); + final SqlSelection selection = getWhereClause(uri, where, whereArgs, match); deleteRequestHeaders(db, selection.getSelection(), selection.getParameters()); - final Cursor cursor = db.query(DB_TABLE, new String[] { - Downloads.Impl._ID, Downloads.Impl._DATA - }, selection.getSelection(), selection.getParameters(), null, null, null); - try { + try (Cursor cursor = db.query(DB_TABLE, new String[] { + _ID, _DATA, COLUMN_MEDIAPROVIDER_URI + }, selection.getSelection(), selection.getParameters(), null, null, null)) { while (cursor.moveToNext()) { final long id = cursor.getLong(0); + scheduler.cancel((int) id); + DownloadStorageProvider.onDownloadProviderDelete(getContext(), id); final String path = cursor.getString(1); @@ -1207,9 +1216,13 @@ public final class DownloadProvider extends ContentProvider { } catch (IOException ignored) { } } + + final String mediaUri = cursor.getString(2); + if (!TextUtils.isEmpty(mediaUri)) { + getContext().getContentResolver().delete(Uri.parse(mediaUri), null, + null); + } } - } finally { - IoUtils.closeQuietly(cursor); } count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters()); @@ -1287,7 +1300,8 @@ public final class DownloadProvider extends ContentProvider { } else { try { // When finished writing, update size and timestamp - return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() { + return ParcelFileDescriptor.open(file, pfdMode, Helpers.getAsyncHandler(), + new OnCloseListener() { @Override public void onClose(IOException e) { final ContentValues values = new ContentValues(); diff --git a/src/com/android/providers/downloads/DownloadReceiver.java b/src/com/android/providers/downloads/DownloadReceiver.java index 2f50dcf6..a0dc6947 100644 --- a/src/com/android/providers/downloads/DownloadReceiver.java +++ b/src/com/android/providers/downloads/DownloadReceiver.java @@ -18,7 +18,13 @@ package com.android.providers.downloads; import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED; import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION; + import static com.android.providers.downloads.Constants.TAG; +import static com.android.providers.downloads.Helpers.getAsyncHandler; +import static com.android.providers.downloads.Helpers.getDownloadNotifier; +import static com.android.providers.downloads.Helpers.getInt; +import static com.android.providers.downloads.Helpers.getString; +import static com.android.providers.downloads.Helpers.getSystemFacade; import android.app.DownloadManager; import android.app.NotificationManager; @@ -29,73 +35,49 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.net.Uri; -import android.os.Handler; -import android.os.HandlerThread; import android.provider.Downloads; import android.text.TextUtils; import android.util.Log; import android.util.Slog; import android.widget.Toast; -import com.google.common.annotations.VisibleForTesting; - /** * Receives system broadcasts (boot, network connectivity) */ public class DownloadReceiver extends BroadcastReceiver { /** - * Intent extra included with {@link #ACTION_CANCEL} intents, indicating the IDs (as array of - * long) of the downloads that were canceled. + * Intent extra included with {@link Constants#ACTION_CANCEL} intents, + * indicating the IDs (as array of long) of the downloads that were + * canceled. */ public static final String EXTRA_CANCELED_DOWNLOAD_IDS = "com.android.providers.downloads.extra.CANCELED_DOWNLOAD_IDS"; /** - * Intent extra included with {@link #ACTION_CANCEL} intents, indicating the tag of the - * notification corresponding to the download(s) that were canceled; this notification must be - * canceled. + * Intent extra included with {@link Constants#ACTION_CANCEL} intents, + * indicating the tag of the notification corresponding to the download(s) + * that were canceled; this notification must be canceled. */ public static final String EXTRA_CANCELED_DOWNLOAD_NOTIFICATION_TAG = "com.android.providers.downloads.extra.CANCELED_DOWNLOAD_NOTIFICATION_TAG"; - private static Handler sAsyncHandler; - - static { - final HandlerThread thread = new HandlerThread("DownloadReceiver"); - thread.start(); - sAsyncHandler = new Handler(thread.getLooper()); - } - - @VisibleForTesting - SystemFacade mSystemFacade = null; - @Override public void onReceive(final Context context, final Intent intent) { - if (mSystemFacade == null) { - mSystemFacade = new RealSystemFacade(context); - } - final String action = intent.getAction(); - if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { - startService(context); - - } else if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) { - startService(context); - - } else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) { - final ConnectivityManager connManager = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); - final NetworkInfo info = connManager.getActiveNetworkInfo(); - if (info != null && info.isConnected()) { - startService(context); - } - + if (Intent.ACTION_BOOT_COMPLETED.equals(action) + || Intent.ACTION_MEDIA_MOUNTED.equals(action)) { + final PendingResult result = goAsync(); + getAsyncHandler().post(new Runnable() { + @Override + public void run() { + handleBootCompleted(context); + result.finish(); + } + }); } else if (Intent.ACTION_UID_REMOVED.equals(action)) { final PendingResult result = goAsync(); - sAsyncHandler.post(new Runnable() { + getAsyncHandler().post(new Runnable() { @Override public void run() { handleUidRemoved(context, intent); @@ -103,9 +85,6 @@ public class DownloadReceiver extends BroadcastReceiver { } }); - } else if (Constants.ACTION_RETRY.equals(action)) { - startService(context); - } else if (Constants.ACTION_OPEN.equals(action) || Constants.ACTION_LIST.equals(action) || Constants.ACTION_HIDE.equals(action)) { @@ -115,7 +94,7 @@ public class DownloadReceiver extends BroadcastReceiver { // TODO: remove this once test is refactored handleNotificationBroadcast(context, intent); } else { - sAsyncHandler.post(new Runnable() { + getAsyncHandler().post(new Runnable() { @Override public void run() { handleNotificationBroadcast(context, intent); @@ -138,6 +117,26 @@ public class DownloadReceiver extends BroadcastReceiver { } } + private void handleBootCompleted(Context context) { + // Show any relevant notifications for completed downloads + getDownloadNotifier(context).update(); + + // Schedule all downloads that are ready + final ContentResolver resolver = context.getContentResolver(); + try (Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, null, null, + null, null)) { + final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); + final DownloadInfo info = new DownloadInfo(context); + while (cursor.moveToNext()) { + reader.updateFromDatabase(info); + Helpers.scheduleJob(context, info); + } + } + + // Schedule idle pass to clean up orphaned files + DownloadIdleService.scheduleIdlePass(context); + } + private void handleUidRemoved(Context context, Intent intent) { final ContentResolver resolver = context.getContentResolver(); @@ -266,18 +265,6 @@ public class DownloadReceiver extends BroadcastReceiver { } } - mSystemFacade.sendBroadcast(appIntent); - } - - private static String getString(Cursor cursor, String col) { - return cursor.getString(cursor.getColumnIndexOrThrow(col)); - } - - private static int getInt(Cursor cursor, String col) { - return cursor.getInt(cursor.getColumnIndexOrThrow(col)); - } - - private void startService(Context context) { - context.startService(new Intent(context, DownloadService.class)); + getSystemFacade(context).sendBroadcast(appIntent); } } diff --git a/src/com/android/providers/downloads/DownloadScanner.java b/src/com/android/providers/downloads/DownloadScanner.java index ca795062..37f51143 100644 --- a/src/com/android/providers/downloads/DownloadScanner.java +++ b/src/com/android/providers/downloads/DownloadScanner.java @@ -35,6 +35,8 @@ import com.android.internal.annotations.GuardedBy; import com.google.common.collect.Maps; import java.util.HashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * Manages asynchronous scanning of completed downloads. @@ -66,11 +68,26 @@ public class DownloadScanner implements MediaScannerConnectionClient { @GuardedBy("mConnection") private HashMap<String, ScanRequest> mPending = Maps.newHashMap(); + private CountDownLatch mLatch; + public DownloadScanner(Context context) { mContext = context; mConnection = new MediaScannerConnection(context, this); } + public static void requestScanBlocking(Context context, DownloadInfo info) { + final DownloadScanner scanner = new DownloadScanner(context); + scanner.mLatch = new CountDownLatch(1); + scanner.requestScan(info); + try { + scanner.mLatch.await(SCAN_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + scanner.shutdown(); + } + } + /** * Check if requested scans are still pending. Scans may timeout after an * internal duration. @@ -153,5 +170,9 @@ public class DownloadScanner implements MediaScannerConnectionClient { // so clean up now-orphaned media entry. resolver.delete(uri, null, null); } + + if (mLatch != null) { + mLatch.countDown(); + } } } diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java deleted file mode 100644 index 7d4392e8..00000000 --- a/src/com/android/providers/downloads/DownloadService.java +++ /dev/null @@ -1,516 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.providers.downloads; - -import static android.text.format.DateUtils.MINUTE_IN_MILLIS; -import static com.android.providers.downloads.Constants.TAG; - -import android.app.AlarmManager; -import android.app.DownloadManager; -import android.app.PendingIntent; -import android.app.Service; -import android.app.job.JobInfo; -import android.app.job.JobScheduler; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.database.ContentObserver; -import android.database.Cursor; -import android.net.Uri; -import android.os.Binder; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.IDeviceIdleController; -import android.os.Message; -import android.os.Process; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.provider.Downloads; -import android.text.TextUtils; -import android.util.Log; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.util.IndentingPrintWriter; -import com.google.android.collect.Maps; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * Performs background downloads as requested by applications that use - * {@link DownloadManager}. Multiple start commands can be issued at this - * service, and it will continue running until no downloads are being actively - * processed. It may schedule alarms to resume downloads in future. - * <p> - * Any database updates important enough to initiate tasks should always be - * delivered through {@link Context#startService(Intent)}. - */ -public class DownloadService extends Service { - // TODO: migrate WakeLock from individual DownloadThreads out into - // DownloadReceiver to protect our entire workflow. - - private static final boolean DEBUG_LIFECYCLE = false; - - @VisibleForTesting - SystemFacade mSystemFacade; - - private AlarmManager mAlarmManager; - private IDeviceIdleController mDeviceIdleController; - - /** Observer to get notified when the content observer's data changes */ - private DownloadManagerContentObserver mObserver; - - /** Class to handle Notification Manager updates */ - private DownloadNotifier mNotifier; - - /** Scheduling of the periodic cleanup job */ - private JobInfo mCleanupJob; - - private static final int CLEANUP_JOB_ID = 1; - private static final long CLEANUP_JOB_PERIOD = 1000 * 60 * 60 * 24; // one day - private static ComponentName sCleanupServiceName = new ComponentName( - DownloadIdleService.class.getPackage().getName(), - DownloadIdleService.class.getName()); - - /** - * The Service's view of the list of downloads, mapping download IDs to the corresponding info - * object. This is kept independently from the content provider, and the Service only initiates - * downloads based on this data, so that it can deal with situation where the data in the - * content provider changes or disappears. - */ - @GuardedBy("mDownloads") - private final Map<Long, DownloadInfo> mDownloads = Maps.newHashMap(); - - private final ExecutorService mExecutor = buildDownloadExecutor(); - - private static ExecutorService buildDownloadExecutor() { - final int maxConcurrent = Resources.getSystem().getInteger( - com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed); - - // Create a bounded thread pool for executing downloads; it creates - // threads as needed (up to maximum) and reclaims them when finished. - final ThreadPoolExecutor executor = new ThreadPoolExecutor( - maxConcurrent, maxConcurrent, 10, TimeUnit.SECONDS, - new LinkedBlockingQueue<Runnable>()) { - @Override - protected void afterExecute(Runnable r, Throwable t) { - super.afterExecute(r, t); - - if (t == null && r instanceof Future<?>) { - try { - ((Future<?>) r).get(); - } catch (CancellationException ce) { - t = ce; - } catch (ExecutionException ee) { - t = ee.getCause(); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - } - } - - if (t != null) { - Log.w(TAG, "Uncaught exception", t); - } - } - }; - executor.allowCoreThreadTimeOut(true); - return executor; - } - - private DownloadScanner mScanner; - - private HandlerThread mUpdateThread; - private Handler mUpdateHandler; - - private volatile int mLastStartId; - - /** - * Receives notifications when the data in the content provider changes - */ - private class DownloadManagerContentObserver extends ContentObserver { - public DownloadManagerContentObserver() { - super(new Handler()); - } - - @Override - public void onChange(final boolean selfChange) { - enqueueUpdate(); - } - } - - /** - * Returns an IBinder instance when someone wants to connect to this - * service. Binding to this service is not allowed. - * - * @throws UnsupportedOperationException - */ - @Override - public IBinder onBind(Intent i) { - throw new UnsupportedOperationException("Cannot bind to Download Manager Service"); - } - - /** - * Initializes the service when it is first created - */ - @Override - public void onCreate() { - super.onCreate(); - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Service onCreate"); - } - - if (mSystemFacade == null) { - mSystemFacade = new RealSystemFacade(this); - } - - mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - mDeviceIdleController = IDeviceIdleController.Stub.asInterface( - ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); - try { - mDeviceIdleController.downloadServiceActive(new Binder()); - } catch (RemoteException e) { - } - - mUpdateThread = new HandlerThread(TAG + "-UpdateThread"); - mUpdateThread.start(); - mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback); - - mScanner = new DownloadScanner(this); - - mNotifier = new DownloadNotifier(this); - mNotifier.init(); - - mObserver = new DownloadManagerContentObserver(); - getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, - true, mObserver); - - JobScheduler js = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE); - if (needToScheduleCleanup(js)) { - final JobInfo job = new JobInfo.Builder(CLEANUP_JOB_ID, sCleanupServiceName) - .setPeriodic(CLEANUP_JOB_PERIOD) - .setRequiresCharging(true) - .setRequiresDeviceIdle(true) - .build(); - js.schedule(job); - } - } - - private boolean needToScheduleCleanup(JobScheduler js) { - List<JobInfo> myJobs = js.getAllPendingJobs(); - if (myJobs != null) { - final int N = myJobs.size(); - for (int i = 0; i < N; i++) { - if (myJobs.get(i).getId() == CLEANUP_JOB_ID) { - // It's already been (persistently) scheduled; no need to do it again - return false; - } - } - } - return true; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - int returnValue = super.onStartCommand(intent, flags, startId); - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Service onStart"); - } - mLastStartId = startId; - enqueueUpdate(); - return returnValue; - } - - @Override - public void onDestroy() { - getContentResolver().unregisterContentObserver(mObserver); - mScanner.shutdown(); - mUpdateThread.quit(); - if (Constants.LOGVV) { - Log.v(Constants.TAG, "Service onDestroy"); - } - super.onDestroy(); - } - - /** - * Enqueue an {@link #updateLocked()} pass to occur in future. - */ - public void enqueueUpdate() { - if (mUpdateHandler != null) { - mUpdateHandler.removeMessages(MSG_UPDATE); - mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget(); - } - } - - /** - * Enqueue an {@link #updateLocked()} pass to occur after delay, usually to - * catch any finished operations that didn't trigger an update pass. - */ - private void enqueueFinalUpdate() { - mUpdateHandler.removeMessages(MSG_FINAL_UPDATE); - mUpdateHandler.sendMessageDelayed( - mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1), - 5 * MINUTE_IN_MILLIS); - } - - private static final int MSG_UPDATE = 1; - private static final int MSG_FINAL_UPDATE = 2; - - private Handler.Callback mUpdateCallback = new Handler.Callback() { - @Override - public boolean handleMessage(Message msg) { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - - final int startId = msg.arg1; - if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId); - - // Since database is current source of truth, our "active" status - // depends on database state. We always get one final update pass - // once the real actions have finished and persisted their state. - - // TODO: switch to asking real tasks to derive active state - // TODO: handle media scanner timeouts - - final boolean isActive; - synchronized (mDownloads) { - isActive = updateLocked(); - } - - if (msg.what == MSG_FINAL_UPDATE) { - // Dump thread stacks belonging to pool - for (Map.Entry<Thread, StackTraceElement[]> entry : - Thread.getAllStackTraces().entrySet()) { - if (entry.getKey().getName().startsWith("pool")) { - Log.d(TAG, entry.getKey() + ": " + Arrays.toString(entry.getValue())); - } - } - - // Dump speed and update details - mNotifier.dumpSpeeds(); - - Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive - + "; someone didn't update correctly."); - } - - if (isActive) { - // Still doing useful work, keep service alive. These active - // tasks will trigger another update pass when they're finished. - - // Enqueue delayed update pass to catch finished operations that - // didn't trigger an update pass; these are bugs. - enqueueFinalUpdate(); - - } else { - // No active tasks, and any pending update messages can be - // ignored, since any updates important enough to initiate tasks - // will always be delivered with a new startId. - - if (stopSelfResult(startId)) { - if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped"); - getContentResolver().unregisterContentObserver(mObserver); - mScanner.shutdown(); - try { - mDeviceIdleController.downloadServiceInactive(); - } catch (RemoteException e) { - } - mUpdateThread.quit(); - } - } - - return true; - } - }; - - /** - * Update {@link #mDownloads} to match {@link DownloadProvider} state. - * Depending on current download state it may enqueue {@link DownloadThread} - * instances, request {@link DownloadScanner} scans, update user-visible - * notifications, and/or schedule future actions with {@link AlarmManager}. - * <p> - * Should only be called from {@link #mUpdateThread} as after being - * requested through {@link #enqueueUpdate()}. - * - * @return If there are active tasks being processed, as of the database - * snapshot taken in this update. - */ - private boolean updateLocked() { - final long now = mSystemFacade.currentTimeMillis(); - - boolean isActive = false; - long nextActionMillis = Long.MAX_VALUE; - - final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet()); - - final ContentResolver resolver = getContentResolver(); - final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, - null, null, null, null); - try { - final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor); - final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID); - while (cursor.moveToNext()) { - final long id = cursor.getLong(idColumn); - staleIds.remove(id); - - DownloadInfo info = mDownloads.get(id); - if (info != null) { - updateDownload(reader, info, now); - } else { - info = insertDownloadLocked(reader, now); - } - - if (info.mDeleted) { - // Delete download if requested, but only after cleaning up - if (!TextUtils.isEmpty(info.mMediaProviderUri)) { - resolver.delete(Uri.parse(info.mMediaProviderUri), null, null); - } - - deleteFileIfExists(info.mFileName); - resolver.delete(info.getAllDownloadsUri(), null, null); - - } else { - // Kick off download task if ready - final boolean activeDownload = info.startDownloadIfReady(mExecutor); - - // Kick off media scan if completed - final boolean activeScan = info.startScanIfReady(mScanner); - - if (DEBUG_LIFECYCLE && (activeDownload || activeScan)) { - Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload - + ", activeScan=" + activeScan); - } - - isActive |= activeDownload; - isActive |= activeScan; - } - - // Keep track of nearest next action - nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis); - } - } finally { - cursor.close(); - } - - // Clean up stale downloads that disappeared - for (Long id : staleIds) { - deleteDownloadLocked(id); - } - - // Update notifications visible to user - mNotifier.updateWith(mDownloads.values()); - - // Set alarm when next action is in future. It's okay if the service - // continues to run in meantime, since it will kick off an update pass. - if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) { - if (Constants.LOGV) { - Log.v(TAG, "scheduling start in " + nextActionMillis + "ms"); - } - - final Intent intent = new Intent(Constants.ACTION_RETRY); - intent.setClass(this, DownloadReceiver.class); - mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis, - PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)); - } - - return isActive; - } - - /** - * Keeps a local copy of the info about a download, and initiates the - * download if appropriate. - */ - private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) { - final DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade, mNotifier); - mDownloads.put(info.mId, info); - - if (Constants.LOGVV) { - Log.v(Constants.TAG, "processing inserted download " + info.mId); - } - - return info; - } - - /** - * Updates the local copy of the info about a download. - */ - private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) { - reader.updateFromDatabase(info); - if (Constants.LOGVV) { - Log.v(Constants.TAG, "processing updated download " + info.mId + - ", status: " + info.mStatus); - } - } - - /** - * Removes the local copy of the info about a download. - */ - private void deleteDownloadLocked(long id) { - DownloadInfo info = mDownloads.get(id); - if (info.mStatus == Downloads.Impl.STATUS_RUNNING) { - info.mStatus = Downloads.Impl.STATUS_CANCELED; - } - if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) { - if (Constants.LOGVV) { - Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName); - } - deleteFileIfExists(info.mFileName); - } - mDownloads.remove(info.mId); - } - - private void deleteFileIfExists(String path) { - if (!TextUtils.isEmpty(path)) { - if (Constants.LOGVV) { - Log.d(TAG, "deleteFileIfExists() deleting " + path); - } - final File file = new File(path); - if (file.exists() && !file.delete()) { - Log.w(TAG, "file: '" + path + "' couldn't be deleted"); - } - } - } - - @Override - protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { - final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); - synchronized (mDownloads) { - final List<Long> ids = Lists.newArrayList(mDownloads.keySet()); - Collections.sort(ids); - for (Long id : ids) { - final DownloadInfo info = mDownloads.get(id); - info.dump(pw); - } - } - } -} diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java index 9de563fb..c559367f 100644 --- a/src/com/android/providers/downloads/DownloadThread.java +++ b/src/com/android/providers/downloads/DownloadThread.java @@ -16,11 +16,18 @@ package com.android.providers.downloads; +import static android.provider.Downloads.Impl.COLUMN_CONTROL; +import static android.provider.Downloads.Impl.COLUMN_DELETED; +import static android.provider.Downloads.Impl.COLUMN_STATUS; +import static android.provider.Downloads.Impl.CONTROL_PAUSED; import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST; import static android.provider.Downloads.Impl.STATUS_CANCELED; import static android.provider.Downloads.Impl.STATUS_CANNOT_RESUME; import static android.provider.Downloads.Impl.STATUS_FILE_ERROR; import static android.provider.Downloads.Impl.STATUS_HTTP_DATA_ERROR; +import static android.provider.Downloads.Impl.STATUS_PAUSED_BY_APP; +import static android.provider.Downloads.Impl.STATUS_QUEUED_FOR_WIFI; +import static android.provider.Downloads.Impl.STATUS_RUNNING; import static android.provider.Downloads.Impl.STATUS_SUCCESS; import static android.provider.Downloads.Impl.STATUS_TOO_MANY_REDIRECTS; import static android.provider.Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE; @@ -28,7 +35,9 @@ import static android.provider.Downloads.Impl.STATUS_UNKNOWN_ERROR; import static android.provider.Downloads.Impl.STATUS_WAITING_FOR_NETWORK; import static android.provider.Downloads.Impl.STATUS_WAITING_TO_RETRY; import static android.text.format.DateUtils.SECOND_IN_MILLIS; + import static com.android.providers.downloads.Constants.TAG; + import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; import static java.net.HttpURLConnection.HTTP_MOVED_PERM; import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; @@ -38,6 +47,8 @@ import static java.net.HttpURLConnection.HTTP_PRECON_FAILED; import static java.net.HttpURLConnection.HTTP_SEE_OTHER; import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; +import android.app.job.JobInfo; +import android.app.job.JobParameters; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -51,19 +62,16 @@ import android.net.NetworkPolicyManager; import android.net.TrafficStats; import android.net.Uri; import android.os.ParcelFileDescriptor; -import android.os.PowerManager; import android.os.Process; import android.os.SystemClock; -import android.os.WorkSource; import android.provider.Downloads; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.util.Log; +import android.util.MathUtils; import android.util.Pair; -import com.android.providers.downloads.DownloadInfo.NetworkState; - import libcore.io.IoUtils; import java.io.File; @@ -89,7 +97,7 @@ import java.net.URLConnection; * Failed network requests are retried several times before giving up. Local * disk errors fail immediately and are not retried. */ -public class DownloadThread implements Runnable { +public class DownloadThread extends Thread { // TODO: bind each download to a specific network interface to avoid state // checking races once we have ConnectivityManager API @@ -104,6 +112,10 @@ public class DownloadThread implements Runnable { private final Context mContext; private final SystemFacade mSystemFacade; private final DownloadNotifier mNotifier; + private final NetworkPolicyManager mNetworkPolicy; + + private final DownloadJobService mJobService; + private final JobParameters mParams; private final long mId; @@ -134,6 +146,14 @@ public class DownloadThread implements Runnable { public String mErrorMsg; + private static final String NOT_CANCELED = COLUMN_STATUS + " != '" + STATUS_CANCELED + "'"; + private static final String NOT_DELETED = COLUMN_DELETED + " == '0'"; + private static final String NOT_PAUSED = "(" + COLUMN_CONTROL + " IS NULL OR " + + COLUMN_CONTROL + " != '" + CONTROL_PAUSED + "')"; + + private static final String SELECTION_VALID = NOT_CANCELED + " AND " + NOT_DELETED + " AND " + + NOT_PAUSED; + public DownloadInfoDelta(DownloadInfo info) { mUri = info.mUri; mFileName = info.mFileName; @@ -179,8 +199,12 @@ public class DownloadThread implements Runnable { */ public void writeToDatabaseOrThrow() throws StopRequestException { if (mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), - buildContentValues(), Downloads.Impl.COLUMN_DELETED + " == '0'", null) == 0) { - throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!"); + buildContentValues(), SELECTION_VALID, null) == 0) { + if (mInfo.queryDownloadControl() == CONTROL_PAUSED) { + throw new StopRequestException(STATUS_PAUSED_BY_APP, "Download paused!"); + } else { + throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!"); + } } } } @@ -197,6 +221,8 @@ public class DownloadThread implements Runnable { private long mLastUpdateBytes = 0; private long mLastUpdateTime = 0; + private Network mNetwork; + private int mNetworkType = ConnectivityManager.TYPE_NONE; /** Historical bytes/second speed of this download. */ @@ -206,11 +232,17 @@ public class DownloadThread implements Runnable { /** Bytes transferred since current sample started. */ private long mSpeedSampleBytes; - public DownloadThread(Context context, SystemFacade systemFacade, DownloadNotifier notifier, - DownloadInfo info) { - mContext = context; - mSystemFacade = systemFacade; - mNotifier = notifier; + /** Flag indicating that thread must be halted */ + private volatile boolean mShutdownRequested; + + public DownloadThread(DownloadJobService service, JobParameters params, DownloadInfo info) { + mContext = service; + mSystemFacade = Helpers.getSystemFacade(mContext); + mNotifier = Helpers.getDownloadNotifier(mContext); + mNetworkPolicy = mContext.getSystemService(NetworkPolicyManager.class); + + mJobService = service; + mParams = params; mId = info.mId; mInfo = info; @@ -223,29 +255,32 @@ public class DownloadThread implements Runnable { // Skip when download already marked as finished; this download was // probably started again while racing with UpdateThread. - if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mId) - == Downloads.Impl.STATUS_SUCCESS) { + if (mInfo.queryDownloadStatus() == Downloads.Impl.STATUS_SUCCESS) { logDebug("Already finished; skipping"); return; } - final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext); - PowerManager.WakeLock wakeLock = null; - final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - try { - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); - wakeLock.setWorkSource(new WorkSource(mInfo.mUid)); - wakeLock.acquire(); - // while performing download, register for rules updates - netPolicy.registerListener(mPolicyListener); + mNetworkPolicy.registerListener(mPolicyListener); logDebug("Starting"); + mInfoDelta.mStatus = STATUS_RUNNING; + mInfoDelta.writeToDatabase(); + + // Use the caller's default network to make this connection, since + // they might be subject to restrictions that we shouldn't let them + // circumvent. + mNetwork = mSystemFacade.getActiveNetwork(mInfo.mUid); + if (mNetwork == null) { + throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, + "No network associated with requesting UID"); + } + // Remember which network this download started on; used to // determine if errors were due to network changes. - final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid); + final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork); if (info != null) { mNetworkType = info.getType(); } @@ -288,7 +323,7 @@ public class DownloadThread implements Runnable { } if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) { - final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid); + final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork); if (info != null && info.getType() == mNetworkType && info.isConnected()) { // Underlying network is still intact, use normal backoff mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY; @@ -321,20 +356,27 @@ public class DownloadThread implements Runnable { mInfoDelta.writeToDatabase(); - if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) { - mInfo.sendIntentIfRequested(); - } - TrafficStats.clearThreadStatsTag(); TrafficStats.clearThreadStatsUid(); - netPolicy.unregisterListener(mPolicyListener); + mNetworkPolicy.unregisterListener(mPolicyListener); + } - if (wakeLock != null) { - wakeLock.release(); - wakeLock = null; + if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) { + mInfo.sendIntentIfRequested(); + if (mInfo.shouldScanFile()) { + DownloadScanner.requestScanBlocking(mContext, mInfo); } + } else if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY + || mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK) { + Helpers.scheduleJob(mContext, DownloadInfo.queryDownloadInfo(mContext, mId)); } + + mJobService.jobFinishedInternal(mParams, false); + } + + public void requestShutdown() { + mShutdownRequested = true; } /** @@ -352,15 +394,6 @@ public class DownloadThread implements Runnable { throw new StopRequestException(STATUS_BAD_REQUEST, e); } - // Use the caller's default network to make this connection, since they might be subject to - // restrictions that we shouldn't let them circumvent. - final Network network = mSystemFacade.getActiveNetwork(mInfo.mUid); - if (network == null) { - throw new StopRequestException(Downloads.Impl.STATUS_WAITING_FOR_NETWORK, - "no network associated with requesting UID"); - } - logDebug("Using network: " + network); - boolean cleartextTrafficPermitted = mSystemFacade.isCleartextTrafficPermitted(mInfo.mUid); int redirectionCount = 0; while (redirectionCount++ < Constants.MAX_REDIRECTS) { @@ -379,7 +412,7 @@ public class DownloadThread implements Runnable { // Check that the caller is allowed to make network connections. If so, make one on // their behalf to open the url. checkConnectivity(); - conn = (HttpURLConnection) network.openConnection(url); + conn = (HttpURLConnection) mNetwork.openConnection(url); conn.setInstanceFollowRedirects(false); conn.setConnectTimeout(DEFAULT_TIMEOUT); conn.setReadTimeout(DEFAULT_TIMEOUT); @@ -542,7 +575,7 @@ public class DownloadThread implements Runnable { } finally { if (drmClient != null) { - drmClient.release(); + drmClient.close(); } IoUtils.closeQuietly(in); @@ -565,7 +598,12 @@ public class DownloadThread implements Runnable { throws StopRequestException { final byte buffer[] = new byte[Constants.BUFFER_SIZE]; while (true) { - checkPausedOrCanceled(); + if (mPolicyDirty) checkConnectivity(); + + if (mShutdownRequested) { + throw new StopRequestException(STATUS_HTTP_DATA_ERROR, + "Local halt requested; job probably timed out"); + } int len = -1; try { @@ -665,38 +703,19 @@ public class DownloadThread implements Runnable { // checking connectivity will apply current policy mPolicyDirty = false; - final NetworkState networkUsable = mInfo.checkCanUseNetwork(mInfoDelta.mTotalBytes); - if (networkUsable != NetworkState.OK) { - int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK; - if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) { - status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; - mInfo.notifyPauseDueToSize(true); - } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) { - status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI; - mInfo.notifyPauseDueToSize(false); - } - throw new StopRequestException(status, networkUsable.name()); - } - } + final boolean allowMetered = mInfo + .getRequiredNetworkType(mInfoDelta.mTotalBytes) != JobInfo.NETWORK_TYPE_UNMETERED; + final boolean allowRoaming = mInfo.isRoamingAllowed(); - /** - * Check if the download has been paused or canceled, stopping the request - * appropriately if it has been. - */ - private void checkPausedOrCanceled() throws StopRequestException { - synchronized (mInfo) { - if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) { - throw new StopRequestException( - Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner"); - } - if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED || mInfo.mDeleted) { - throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled"); - } + final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork); + if (info == null || !info.isConnected()) { + throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is disconnected"); } - - // if policy has been changed, trigger connectivity check - if (mPolicyDirty) { - checkConnectivity(); + if (info.isRoaming() && !allowRoaming) { + throw new StopRequestException(STATUS_WAITING_FOR_NETWORK, "Network is roaming"); + } + if (info.isMetered() && !allowMetered) { + throw new StopRequestException(STATUS_QUEUED_FOR_WIFI, "Network is metered"); } } @@ -781,17 +800,8 @@ public class DownloadThread implements Runnable { private void parseUnavailableHeaders(HttpURLConnection conn) { long retryAfter = conn.getHeaderFieldInt("Retry-After", -1); - if (retryAfter < 0) { - retryAfter = 0; - } else { - if (retryAfter < Constants.MIN_RETRY_AFTER) { - retryAfter = Constants.MIN_RETRY_AFTER; - } else if (retryAfter > Constants.MAX_RETRY_AFTER) { - retryAfter = Constants.MAX_RETRY_AFTER; - } - retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1); - } - + retryAfter = MathUtils.constrain(retryAfter, Constants.MIN_RETRY_AFTER, + Constants.MAX_RETRY_AFTER); mInfoDelta.mRetryAfter = (int) (retryAfter * SECOND_IN_MILLIS); } diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java index d01cbff2..b0737452 100644 --- a/src/com/android/providers/downloads/Helpers.java +++ b/src/com/android/providers/downloads/Helpers.java @@ -20,12 +20,24 @@ import static android.os.Environment.buildExternalStorageAppCacheDirs; import static android.os.Environment.buildExternalStorageAppFilesDirs; import static android.os.Environment.buildExternalStorageAppMediaDirs; import static android.os.Environment.buildExternalStorageAppObbDirs; +import static android.provider.Downloads.Impl.FLAG_REQUIRES_CHARGING; +import static android.provider.Downloads.Impl.FLAG_REQUIRES_DEVICE_IDLE; +import static android.provider.Downloads.Impl.VISIBILITY_VISIBLE; +import static android.provider.Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED; + import static com.android.providers.downloads.Constants.TAG; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.content.ComponentName; import android.content.Context; +import android.database.Cursor; import android.net.Uri; import android.os.Environment; import android.os.FileUtils; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; import android.os.SystemClock; import android.os.UserHandle; import android.os.storage.StorageManager; @@ -34,6 +46,8 @@ import android.provider.Downloads; import android.util.Log; import android.webkit.MimeTypeMap; +import com.google.common.annotations.VisibleForTesting; + import java.io.File; import java.io.IOException; import java.util.Random; @@ -53,9 +67,112 @@ public class Helpers { private static final Object sUniqueLock = new Object(); + private static HandlerThread sAsyncHandlerThread; + private static Handler sAsyncHandler; + + private static SystemFacade sSystemFacade; + private static DownloadNotifier sNotifier; + private Helpers() { } + public synchronized static Handler getAsyncHandler() { + if (sAsyncHandlerThread == null) { + sAsyncHandlerThread = new HandlerThread("sAsyncHandlerThread", + Process.THREAD_PRIORITY_BACKGROUND); + sAsyncHandlerThread.start(); + sAsyncHandler = new Handler(sAsyncHandlerThread.getLooper()); + } + return sAsyncHandler; + } + + @VisibleForTesting + public synchronized static void setSystemFacade(SystemFacade systemFacade) { + sSystemFacade = systemFacade; + } + + public synchronized static SystemFacade getSystemFacade(Context context) { + if (sSystemFacade == null) { + sSystemFacade = new RealSystemFacade(context); + } + return sSystemFacade; + } + + public synchronized static DownloadNotifier getDownloadNotifier(Context context) { + if (sNotifier == null) { + sNotifier = new DownloadNotifier(context); + } + return sNotifier; + } + + public static String getString(Cursor cursor, String col) { + return cursor.getString(cursor.getColumnIndexOrThrow(col)); + } + + public static int getInt(Cursor cursor, String col) { + return cursor.getInt(cursor.getColumnIndexOrThrow(col)); + } + + public static void scheduleJob(Context context, long downloadId) { + scheduleJob(context, DownloadInfo.queryDownloadInfo(context, downloadId)); + } + + /** + * Schedule (or reschedule) a job for the given {@link DownloadInfo} using + * its current state to define job constraints. + */ + public static void scheduleJob(Context context, DownloadInfo info) { + if (info == null) return; + + final JobScheduler scheduler = context.getSystemService(JobScheduler.class); + + // Tear down any existing job for this download + final int jobId = (int) info.mId; + scheduler.cancel(jobId); + + // Skip scheduling if download is paused or finished + if (!info.isReadyToSchedule()) return; + + final JobInfo.Builder builder = new JobInfo.Builder(jobId, + new ComponentName(context, DownloadJobService.class)); + + // When this download will show a notification, run with a higher + // priority, since it's effectively a foreground service + switch (info.mVisibility) { + case VISIBILITY_VISIBLE: + case VISIBILITY_VISIBLE_NOTIFY_COMPLETED: + // TODO: force app out of doze, since they're showing a notification + builder.setPriority(JobInfo.PRIORITY_FOREGROUND_APP); + break; + } + + // We might have a backoff constraint due to errors + final long latency = info.getMinimumLatency(); + if (latency > 0) { + builder.setMinimumLatency(latency); + } + + // We always require a network, but the type of network might be further + // restricted based on download request or user override + builder.setRequiredNetworkType(info.getRequiredNetworkType(info.mTotalBytes)); + + if ((info.mFlags & FLAG_REQUIRES_CHARGING) != 0) { + builder.setRequiresCharging(true); + } + if ((info.mFlags & FLAG_REQUIRES_DEVICE_IDLE) != 0) { + builder.setRequiresDeviceIdle(true); + } + + // If package name was filtered during insert (probably due to being + // invalid), blame based on the requesting UID instead + String packageName = info.mPackage; + if (packageName == null) { + packageName = context.getPackageManager().getPackagesForUid(info.mUid)[0]; + } + + scheduler.scheduleAsPackage(builder.build(), packageName, UserHandle.myUserId(), TAG); + } + /* * Parse the Content-Disposition HTTP Header. The format of the header * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html diff --git a/src/com/android/providers/downloads/RealSystemFacade.java b/src/com/android/providers/downloads/RealSystemFacade.java index 48df2a09..da4e01ed 100644 --- a/src/com/android/providers/downloads/RealSystemFacade.java +++ b/src/com/android/providers/downloads/RealSystemFacade.java @@ -16,8 +16,6 @@ package com.android.providers.downloads; -import com.android.internal.util.ArrayUtils; - import android.app.DownloadManager; import android.content.Context; import android.content.Intent; @@ -28,8 +26,8 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkInfo; -import android.telephony.TelephonyManager; -import android.util.Log; + +import com.android.internal.util.ArrayUtils; class RealSystemFacade implements SystemFacade { private Context mContext; @@ -44,60 +42,27 @@ class RealSystemFacade implements SystemFacade { } @Override - public NetworkInfo getActiveNetworkInfo(int uid) { - ConnectivityManager connectivity = - (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivity == null) { - Log.w(Constants.TAG, "couldn't get connectivity manager"); - return null; - } - - final NetworkInfo activeInfo = connectivity.getActiveNetworkInfoForUid(uid); - if (activeInfo == null && Constants.LOGVV) { - Log.v(Constants.TAG, "network is not available"); - } - return activeInfo; - } - - @Override public Network getActiveNetwork(int uid) { - ConnectivityManager connectivity = - (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - return connectivity.getActiveNetworkForUid(uid); - } - - @Override - public boolean isActiveNetworkMetered() { - final ConnectivityManager conn = ConnectivityManager.from(mContext); - return conn.isActiveNetworkMetered(); + return mContext.getSystemService(ConnectivityManager.class) + .getActiveNetworkForUid(uid); } @Override - public boolean isNetworkRoaming() { - ConnectivityManager connectivity = - (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivity == null) { - Log.w(Constants.TAG, "couldn't get connectivity manager"); - return false; - } - - NetworkInfo info = connectivity.getActiveNetworkInfo(); - boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE); - boolean isRoaming = isMobile && TelephonyManager.getDefault().isNetworkRoaming(); - if (Constants.LOGVV && isRoaming) { - Log.v(Constants.TAG, "network is roaming"); - } - return isRoaming; + public NetworkInfo getNetworkInfo(Network network) { + return mContext.getSystemService(ConnectivityManager.class) + .getNetworkInfo(network); } @Override - public Long getMaxBytesOverMobile() { - return DownloadManager.getMaxBytesOverMobile(mContext); + public long getMaxBytesOverMobile() { + final Long value = DownloadManager.getMaxBytesOverMobile(mContext); + return (value == null) ? Long.MAX_VALUE : value; } @Override - public Long getRecommendedMaxBytesOverMobile() { - return DownloadManager.getRecommendedMaxBytesOverMobile(mContext); + public long getRecommendedMaxBytesOverMobile() { + final Long value = DownloadManager.getRecommendedMaxBytesOverMobile(mContext); + return (value == null) ? Long.MAX_VALUE : value; } @Override diff --git a/src/com/android/providers/downloads/SizeLimitActivity.java b/src/com/android/providers/downloads/SizeLimitActivity.java deleted file mode 100644 index d25277d9..00000000 --- a/src/com/android/providers/downloads/SizeLimitActivity.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.providers.downloads; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.ContentValues; -import android.content.DialogInterface; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.provider.Downloads; -import android.text.format.Formatter; -import android.util.Log; - -import java.util.LinkedList; -import java.util.Queue; - -/** - * Activity to show dialogs to the user when a download exceeds a limit on download sizes for - * mobile networks. This activity gets started by the background download service when a download's - * size is discovered to be exceeded one of these thresholds. - */ -public class SizeLimitActivity extends Activity - implements DialogInterface.OnCancelListener, DialogInterface.OnClickListener { - private Dialog mDialog; - private Queue<Intent> mDownloadsToShow = new LinkedList<Intent>(); - private Uri mCurrentUri; - private Intent mCurrentIntent; - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - } - - @Override - protected void onResume() { - super.onResume(); - Intent intent = getIntent(); - if (intent != null) { - mDownloadsToShow.add(intent); - setIntent(null); - showNextDialog(); - } - if (mDialog != null && !mDialog.isShowing()) { - mDialog.show(); - } - } - - private void showNextDialog() { - if (mDialog != null) { - return; - } - - if (mDownloadsToShow.isEmpty()) { - finish(); - return; - } - - mCurrentIntent = mDownloadsToShow.poll(); - mCurrentUri = mCurrentIntent.getData(); - Cursor cursor = getContentResolver().query(mCurrentUri, null, null, null, null); - try { - if (!cursor.moveToFirst()) { - Log.e(Constants.TAG, "Empty cursor for URI " + mCurrentUri); - dialogClosed(); - return; - } - showDialog(cursor); - } finally { - cursor.close(); - } - } - - private void showDialog(Cursor cursor) { - int size = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES)); - String sizeString = Formatter.formatFileSize(this, size); - String queueText = getString(R.string.button_queue_for_wifi); - boolean isWifiRequired = - mCurrentIntent.getExtras().getBoolean(DownloadInfo.EXTRA_IS_WIFI_REQUIRED); - - AlertDialog.Builder builder = new AlertDialog.Builder(this, AlertDialog.THEME_HOLO_DARK); - if (isWifiRequired) { - builder.setTitle(R.string.wifi_required_title) - .setMessage(getString(R.string.wifi_required_body, sizeString, queueText)) - .setPositiveButton(R.string.button_queue_for_wifi, this) - .setNegativeButton(R.string.button_cancel_download, this); - } else { - builder.setTitle(R.string.wifi_recommended_title) - .setMessage(getString(R.string.wifi_recommended_body, sizeString, queueText)) - .setPositiveButton(R.string.button_start_now, this) - .setNegativeButton(R.string.button_queue_for_wifi, this); - } - mDialog = builder.setOnCancelListener(this).show(); - } - - @Override - public void onCancel(DialogInterface dialog) { - dialogClosed(); - } - - private void dialogClosed() { - mDialog = null; - mCurrentUri = null; - showNextDialog(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - boolean isRequired = - mCurrentIntent.getExtras().getBoolean(DownloadInfo.EXTRA_IS_WIFI_REQUIRED); - if (isRequired && which == AlertDialog.BUTTON_NEGATIVE) { - getContentResolver().delete(mCurrentUri, null, null); - } else if (!isRequired && which == AlertDialog.BUTTON_POSITIVE) { - ContentValues values = new ContentValues(); - values.put(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT, true); - getContentResolver().update(mCurrentUri, values , null, null); - } - dialogClosed(); - } -} diff --git a/src/com/android/providers/downloads/SystemFacade.java b/src/com/android/providers/downloads/SystemFacade.java index 7f97b919..e7852e95 100644 --- a/src/com/android/providers/downloads/SystemFacade.java +++ b/src/com/android/providers/downloads/SystemFacade.java @@ -27,33 +27,22 @@ interface SystemFacade { */ public long currentTimeMillis(); - /** - * @return Currently active network, or null if there's no active - * connection. - */ - public NetworkInfo getActiveNetworkInfo(int uid); - public Network getActiveNetwork(int uid); - public boolean isActiveNetworkMetered(); - - /** - * @see android.telephony.TelephonyManager#isNetworkRoaming - */ - public boolean isNetworkRoaming(); + public NetworkInfo getNetworkInfo(Network network); /** * @return maximum size, in bytes, of downloads that may go over a mobile connection; or null if * there's no limit */ - public Long getMaxBytesOverMobile(); + public long getMaxBytesOverMobile(); /** * @return recommended maximum size, in bytes, of downloads that may go over a mobile * connection; or null if there's no recommended limit. The user will have the option to bypass * this limit. */ - public Long getRecommendedMaxBytesOverMobile(); + public long getRecommendedMaxBytesOverMobile(); /** * Send a broadcast intent. |