summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers
diff options
context:
space:
mode:
authorRicardo Cerqueira <cyanogenmod@cerqueira.org>2013-07-25 00:46:33 +0100
committerRicardo Cerqueira <cyanogenmod@cerqueira.org>2013-07-25 00:46:33 +0100
commit8834e86030ab48890308b3aaa005c11cf968424f (patch)
tree33e7361d00da57cf7e4e7305cb5d849773671a74 /src/com/android/providers
parent459fc32974f03d27a7b70ea8acadbc44e2360f11 (diff)
parent2435eb9c89bd46511624b9d89ab22766f82dbfcb (diff)
downloadandroid_packages_providers_DownloadProvider-8834e86030ab48890308b3aaa005c11cf968424f.tar.gz
android_packages_providers_DownloadProvider-8834e86030ab48890308b3aaa005c11cf968424f.tar.bz2
android_packages_providers_DownloadProvider-8834e86030ab48890308b3aaa005c11cf968424f.zip
Merge tag 'android-4.3_r2.1' into cm-10.2
Android 4.3 release 2.1 Conflicts: res/values-cs/strings.xml Change-Id: Idceee08cb4d6f69e4f8cc4c043142852ac932e7f
Diffstat (limited to 'src/com/android/providers')
-rw-r--r--src/com/android/providers/downloads/Constants.java3
-rw-r--r--src/com/android/providers/downloads/DownloadDrmHelper.java58
-rw-r--r--src/com/android/providers/downloads/DownloadHandler.java123
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java260
-rw-r--r--src/com/android/providers/downloads/DownloadNotifier.java63
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java55
-rw-r--r--src/com/android/providers/downloads/DownloadScanner.java157
-rw-r--r--src/com/android/providers/downloads/DownloadService.java569
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java889
-rw-r--r--src/com/android/providers/downloads/DrmConvertSession.java171
-rw-r--r--src/com/android/providers/downloads/Helpers.java74
-rw-r--r--src/com/android/providers/downloads/OpenHelper.java10
-rw-r--r--src/com/android/providers/downloads/RealSystemFacade.java9
-rw-r--r--src/com/android/providers/downloads/StopRequestException.java30
-rw-r--r--src/com/android/providers/downloads/StorageManager.java26
-rw-r--r--src/com/android/providers/downloads/SystemFacade.java5
16 files changed, 1084 insertions, 1418 deletions
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
index 8d806182..08ef4665 100644
--- a/src/com/android/providers/downloads/Constants.java
+++ b/src/com/android/providers/downloads/Constants.java
@@ -48,9 +48,6 @@ public class Constants {
/** The column that is used to remember whether the media scanner was invoked */
public static final String MEDIA_SCANNED = "scanned";
- /** The column that is used to count retries */
- public static final String FAILED_CONNECTIONS = "numfailed";
-
/** 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";
diff --git a/src/com/android/providers/downloads/DownloadDrmHelper.java b/src/com/android/providers/downloads/DownloadDrmHelper.java
index 10cb792c..d1358246 100644
--- a/src/com/android/providers/downloads/DownloadDrmHelper.java
+++ b/src/com/android/providers/downloads/DownloadDrmHelper.java
@@ -19,7 +19,8 @@ package com.android.providers.downloads;
import android.content.Context;
import android.drm.DrmManagerClient;
-import android.util.Log;
+
+import java.io.File;
public class DownloadDrmHelper {
@@ -32,31 +33,6 @@ public class DownloadDrmHelper {
public static final String EXTENSION_INTERNAL_FWDL = ".fl";
/**
- * Checks if the Media Type is a DRM Media Type
- *
- * @param drmManagerClient A DrmManagerClient
- * @param mimetype Media Type to check
- * @return True if the Media Type is DRM else false
- */
- public static boolean isDrmMimeType(Context context, String mimetype) {
- boolean result = false;
- if (context != null) {
- try {
- DrmManagerClient drmClient = new DrmManagerClient(context);
- if (drmClient != null && mimetype != null && mimetype.length() > 0) {
- result = drmClient.canHandle("", mimetype);
- }
- } catch (IllegalArgumentException e) {
- Log.w(Constants.TAG,
- "DrmManagerClient instance could not be created, context is Illegal.");
- } catch (IllegalStateException e) {
- Log.w(Constants.TAG, "DrmManagerClient didn't initialize properly.");
- }
- }
- return result;
- }
-
- /**
* Checks if the Media Type needs to be DRM converted
*
* @param mimetype Media type of the content
@@ -83,28 +59,20 @@ public class DownloadDrmHelper {
}
/**
- * Gets the original mime type of DRM protected content.
- *
- * @param context The context
- * @param path Path to the file
- * @param containingMime The current mime type of of the file i.e. the
- * containing mime type
- * @return The original mime type of the file if DRM protected else the
- * currentMime
+ * Return the original MIME type of the given file, using the DRM framework
+ * if the file is protected content.
*/
- public static String getOriginalMimeType(Context context, String path, String containingMime) {
- String result = containingMime;
- DrmManagerClient drmClient = new DrmManagerClient(context);
+ public static String getOriginalMimeType(Context context, File file, String currentMime) {
+ final DrmManagerClient client = new DrmManagerClient(context);
try {
- if (drmClient.canHandle(path, null)) {
- result = drmClient.getOriginalMimeType(path);
+ final String rawFile = file.toString();
+ if (client.canHandle(rawFile, null)) {
+ return client.getOriginalMimeType(rawFile);
+ } else {
+ return currentMime;
}
- } catch (IllegalArgumentException ex) {
- Log.w(Constants.TAG,
- "Can't get original mime type since path is null or empty string.");
- } catch (IllegalStateException ex) {
- Log.w(Constants.TAG, "DrmManagerClient didn't initialize properly.");
+ } finally {
+ client.release();
}
- return result;
}
}
diff --git a/src/com/android/providers/downloads/DownloadHandler.java b/src/com/android/providers/downloads/DownloadHandler.java
deleted file mode 100644
index 2f02864e..00000000
--- a/src/com/android/providers/downloads/DownloadHandler.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2011 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.content.res.Resources;
-import android.util.Log;
-import android.util.LongSparseArray;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-
-public class DownloadHandler {
- private static final String TAG = "DownloadHandler";
-
- @GuardedBy("this")
- private final LinkedHashMap<Long, DownloadInfo> mDownloadsQueue =
- new LinkedHashMap<Long, DownloadInfo>();
- @GuardedBy("this")
- private final HashMap<Long, DownloadInfo> mDownloadsInProgress =
- new HashMap<Long, DownloadInfo>();
- @GuardedBy("this")
- private final LongSparseArray<Long> mCurrentSpeed = new LongSparseArray<Long>();
-
- private final int mMaxConcurrentDownloadsAllowed = Resources.getSystem().getInteger(
- com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed);
-
- private static final DownloadHandler sDownloadHandler = new DownloadHandler();
-
- public static DownloadHandler getInstance() {
- return sDownloadHandler;
- }
-
- public synchronized void enqueueDownload(DownloadInfo info) {
- if (!mDownloadsQueue.containsKey(info.mId)) {
- if (Constants.LOGV) {
- Log.i(TAG, "enqueued download. id: " + info.mId + ", uri: " + info.mUri);
- }
- mDownloadsQueue.put(info.mId, info);
- startDownloadThreadLocked();
- }
- }
-
- private void startDownloadThreadLocked() {
- Iterator<Long> keys = mDownloadsQueue.keySet().iterator();
- ArrayList<Long> ids = new ArrayList<Long>();
- while (mDownloadsInProgress.size() < mMaxConcurrentDownloadsAllowed && keys.hasNext()) {
- Long id = keys.next();
- DownloadInfo info = mDownloadsQueue.get(id);
- info.startDownloadThread();
- ids.add(id);
- mDownloadsInProgress.put(id, mDownloadsQueue.get(id));
- if (Constants.LOGV) {
- Log.i(TAG, "started download for : " + id);
- }
- }
- for (Long id : ids) {
- mDownloadsQueue.remove(id);
- }
- }
-
- public synchronized boolean hasDownloadInQueue(long id) {
- return mDownloadsQueue.containsKey(id) || mDownloadsInProgress.containsKey(id);
- }
-
- public synchronized void dequeueDownload(long id) {
- mDownloadsInProgress.remove(id);
- mCurrentSpeed.remove(id);
- startDownloadThreadLocked();
- if (mDownloadsInProgress.size() == 0 && mDownloadsQueue.size() == 0) {
- notifyAll();
- }
- }
-
- public synchronized void setCurrentSpeed(long id, long speed) {
- mCurrentSpeed.put(id, speed);
- }
-
- public synchronized long getCurrentSpeed(long id) {
- return mCurrentSpeed.get(id, -1L);
- }
-
- // right now this is only used by tests. but there is no reason why it can't be used
- // by any module using DownloadManager (TODO add API to DownloadManager.java)
- public synchronized void waitUntilDownloadsTerminate() throws InterruptedException {
- if (mDownloadsInProgress.size() == 0 && mDownloadsQueue.size() == 0) {
- if (Constants.LOGVV) {
- Log.i(TAG, "nothing to wait on");
- }
- return;
- }
- if (Constants.LOGVV) {
- for (DownloadInfo info : mDownloadsInProgress.values()) {
- Log.i(TAG, "** progress: " + info.mId + ", " + info.mUri);
- }
- for (DownloadInfo info : mDownloadsQueue.values()) {
- Log.i(TAG, "** in Q: " + info.mId + ", " + info.mUri);
- }
- }
- if (Constants.LOGVV) {
- Log.i(TAG, "waiting for 5 sec");
- }
- // wait upto 5 sec
- wait(5 * 1000);
- }
-}
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index 5172b696..7a912d5a 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -31,20 +31,26 @@ import android.os.Environment;
import android.provider.Downloads;
import android.provider.Downloads.Impl;
import android.text.TextUtils;
-import android.util.Log;
import android.util.Pair;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.IndentingPrintWriter;
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;
/**
* Stores information about an individual download.
*/
public class DownloadInfo {
+ // TODO: move towards these in-memory objects being sources of truth, and
+ // periodically pushing to provider.
+
public static class Reader {
private ContentResolver mResolver;
private Cursor mCursor;
@@ -54,8 +60,10 @@ public class DownloadInfo {
mCursor = cursor;
}
- public DownloadInfo newDownloadInfo(Context context, SystemFacade systemFacade) {
- DownloadInfo info = new DownloadInfo(context, systemFacade);
+ public DownloadInfo newDownloadInfo(Context context, SystemFacade systemFacade,
+ StorageManager storageManager, DownloadNotifier notifier) {
+ final DownloadInfo info = new DownloadInfo(
+ context, systemFacade, storageManager, notifier);
updateFromDatabase(info);
readRequestHeaders(info);
return info;
@@ -71,7 +79,7 @@ public class DownloadInfo {
info.mDestination = getInt(Downloads.Impl.COLUMN_DESTINATION);
info.mVisibility = getInt(Downloads.Impl.COLUMN_VISIBILITY);
info.mStatus = getInt(Downloads.Impl.COLUMN_STATUS);
- info.mNumFailed = getInt(Constants.FAILED_CONNECTIONS);
+ info.mNumFailed = getInt(Downloads.Impl.COLUMN_FAILED_CONNECTIONS);
int retryRedirect = getInt(Constants.RETRY_AFTER_X_REDIRECT_COUNT);
info.mRetryAfter = retryRedirect & 0xfffffff;
info.mLastMod = getLong(Downloads.Impl.COLUMN_LAST_MODIFICATION);
@@ -146,44 +154,49 @@ public class DownloadInfo {
}
}
- // the following NETWORK_* constants are used to indicates specfic reasons for disallowing a
- // download from using a network, since specific causes can require special handling
-
- /**
- * The network is usable for the given download.
- */
- public static final int NETWORK_OK = 1;
-
- /**
- * There is no network connectivity.
- */
- public static final int NETWORK_NO_CONNECTION = 2;
-
- /**
- * The download exceeds the maximum size for this network.
- */
- public static final int NETWORK_UNUSABLE_DUE_TO_SIZE = 3;
-
- /**
- * The download exceeds the recommended maximum size for this network, the user must confirm for
- * this download to proceed without WiFi.
- */
- public static final int NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE = 4;
-
/**
- * The current connection is roaming, and the download can't proceed over a roaming connection.
+ * Constants used to indicate network state for a specific download, after
+ * applying any requested constraints.
*/
- public static final int NETWORK_CANNOT_USE_ROAMING = 5;
-
- /**
- * The app requesting the download specific that it can't use the current network connection.
- */
- public static final int NETWORK_TYPE_DISALLOWED_BY_REQUESTOR = 6;
-
- /**
- * Current network is blocked for requesting application.
- */
- public static final int NETWORK_BLOCKED = 7;
+ 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
@@ -191,7 +204,6 @@ public class DownloadInfo {
*/
public static final String EXTRA_IS_WIFI_REQUIRED = "isWifiRequired";
-
public long mId;
public String mUri;
public boolean mNoIntegrity;
@@ -229,12 +241,28 @@ public class DownloadInfo {
public int mFuzz;
private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
- private SystemFacade mSystemFacade;
- private Context mContext;
- private DownloadInfo(Context context, SystemFacade systemFacade) {
+ /**
+ * 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 StorageManager mStorageManager;
+ private final DownloadNotifier mNotifier;
+
+ private DownloadInfo(Context context, SystemFacade systemFacade, StorageManager storageManager,
+ DownloadNotifier notifier) {
mContext = context;
mSystemFacade = systemFacade;
+ mStorageManager = storageManager;
+ mNotifier = notifier;
mFuzz = Helpers.sRandom.nextInt(1001);
}
@@ -285,14 +313,9 @@ public class DownloadInfo {
}
/**
- * Returns whether this download (which the download manager hasn't seen yet)
- * should be started.
+ * Returns whether this download should be enqueued.
*/
- private boolean isReadyToStart(long now) {
- if (DownloadHandler.getInstance().hasDownloadInQueue(mId)) {
- // already running
- return false;
- }
+ private boolean isReadyToDownload() {
if (mControl == Downloads.Impl.CONTROL_PAUSED) {
// the download is paused, so it's not going to start
return false;
@@ -306,10 +329,11 @@ public class DownloadInfo {
case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
- return checkCanUseNetwork() == NETWORK_OK;
+ return checkCanUseNetwork() == NetworkState.OK;
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?
@@ -337,21 +361,20 @@ public class DownloadInfo {
/**
* Returns whether this download is allowed to use the network.
- * @return one of the NETWORK_* constants
*/
- public int checkCanUseNetwork() {
+ public NetworkState checkCanUseNetwork() {
final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mUid);
if (info == null || !info.isConnected()) {
- return NETWORK_NO_CONNECTION;
+ return NetworkState.NO_CONNECTION;
}
if (DetailedState.BLOCKED.equals(info.getDetailedState())) {
- return NETWORK_BLOCKED;
+ return NetworkState.BLOCKED;
}
- if (!isRoamingAllowed() && mSystemFacade.isNetworkRoaming()) {
- return NETWORK_CANNOT_USE_ROAMING;
+ if (mSystemFacade.isNetworkRoaming() && !isRoamingAllowed()) {
+ return NetworkState.CANNOT_USE_ROAMING;
}
- if (!mAllowMetered && mSystemFacade.isActiveNetworkMetered()) {
- return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR;
+ if (mSystemFacade.isActiveNetworkMetered() && !mAllowMetered) {
+ return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR;
}
return checkIsNetworkTypeAllowed(info.getType());
}
@@ -365,45 +388,16 @@ public class DownloadInfo {
}
/**
- * @return a non-localized string appropriate for logging corresponding to one of the
- * NETWORK_* constants.
- */
- public String getLogMessageForNetworkError(int networkError) {
- switch (networkError) {
- case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
- return "download size exceeds recommended limit for mobile network";
-
- case NETWORK_UNUSABLE_DUE_TO_SIZE:
- return "download size exceeds limit for mobile network";
-
- case NETWORK_NO_CONNECTION:
- return "no network connection available";
-
- case NETWORK_CANNOT_USE_ROAMING:
- return "download cannot use the current network connection because it is roaming";
-
- case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
- return "download was requested to not use the current network type";
-
- case NETWORK_BLOCKED:
- return "network is blocked for requesting application";
-
- default:
- return "unknown error with network connectivity";
- }
- }
-
- /**
* 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 int checkIsNetworkTypeAllowed(int networkType) {
+ private NetworkState checkIsNetworkTypeAllowed(int networkType) {
if (mIsPublicApi) {
final int flag = translateNetworkTypeToApiFlag(networkType);
final boolean allowAllNetworkTypes = mAllowedNetworkTypes == ~0;
if (!allowAllNetworkTypes && (flag & mAllowedNetworkTypes) == 0) {
- return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR;
+ return NetworkState.TYPE_DISALLOWED_BY_REQUESTOR;
}
}
return checkSizeAllowedForNetwork(networkType);
@@ -433,42 +427,68 @@ public class DownloadInfo {
* Check if the download's size prohibits it from running over the current network.
* @return one of the NETWORK_* constants
*/
- private int checkSizeAllowedForNetwork(int networkType) {
+ private NetworkState checkSizeAllowedForNetwork(int networkType) {
if (mTotalBytes <= 0) {
- return NETWORK_OK; // we don't know the size yet
+ return NetworkState.OK; // we don't know the size yet
}
if (networkType == ConnectivityManager.TYPE_WIFI) {
- return NETWORK_OK; // anything goes over wifi
+ return NetworkState.OK; // anything goes over wifi
}
Long maxBytesOverMobile = mSystemFacade.getMaxBytesOverMobile();
if (maxBytesOverMobile != null && mTotalBytes > maxBytesOverMobile) {
- return NETWORK_UNUSABLE_DUE_TO_SIZE;
+ return NetworkState.UNUSABLE_DUE_TO_SIZE;
}
if (mBypassRecommendedSizeLimit == 0) {
Long recommendedMaxBytesOverMobile = mSystemFacade.getRecommendedMaxBytesOverMobile();
if (recommendedMaxBytesOverMobile != null
&& mTotalBytes > recommendedMaxBytesOverMobile) {
- return NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE;
+ return NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE;
}
}
- return NETWORK_OK;
+ return NetworkState.OK;
}
- void startIfReady(long now, StorageManager storageManager) {
- if (!isReadyToStart(now)) {
- return;
- }
+ /**
+ * 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);
+ }
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "Service spawning thread to handle download " + mId);
+ mTask = new DownloadThread(
+ mContext, mSystemFacade, this, mStorageManager, mNotifier);
+ mSubmittedTask = executor.submit(mTask);
+ }
+ return isReady;
}
- 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);
+ }
+
+ /**
+ * 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;
}
- DownloadHandler.getInstance().enqueueDownload(this);
}
public boolean isOnCache() {
@@ -529,15 +549,15 @@ public class DownloadInfo {
}
/**
- * Returns the amount of time (as measured from the "now" parameter)
- * at which a download will be active.
- * 0 = immediately - service should stick around to handle this download.
- * -1 = never - service can go away without ever waking up.
- * positive value - service must wake up in the future, as specified in ms from "now"
+ * 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.
*/
- long nextAction(long now) {
+ public long nextActionMillis(long now) {
if (Downloads.Impl.isStatusCompleted(mStatus)) {
- return -1;
+ return Long.MAX_VALUE;
}
if (mStatus != Downloads.Impl.STATUS_WAITING_TO_RETRY) {
return 0;
@@ -552,7 +572,7 @@ public class DownloadInfo {
/**
* Returns whether a file should be scanned
*/
- boolean shouldScanFile() {
+ public boolean shouldScanFile() {
return (mMediaScanned == 0)
&& (mDestination == Downloads.Impl.DESTINATION_EXTERNAL ||
mDestination == Downloads.Impl.DESTINATION_FILE_URI ||
@@ -570,12 +590,6 @@ public class DownloadInfo {
mContext.startActivity(intent);
}
- void startDownloadThread() {
- DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this,
- StorageManager.getInstance(mContext));
- mSystemFacade.startThread(downloader);
- }
-
/**
* Query and return status of requested download.
*/
diff --git a/src/com/android/providers/downloads/DownloadNotifier.java b/src/com/android/providers/downloads/DownloadNotifier.java
index f3878654..ac52eba2 100644
--- a/src/com/android/providers/downloads/DownloadNotifier.java
+++ b/src/com/android/providers/downloads/DownloadNotifier.java
@@ -19,6 +19,8 @@ package com.android.providers.downloads;
import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE;
import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION;
+import static android.provider.Downloads.Impl.STATUS_RUNNING;
+import static com.android.providers.downloads.Constants.TAG;
import android.app.DownloadManager;
import android.app.Notification;
@@ -29,9 +31,12 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
+import android.os.SystemClock;
import android.provider.Downloads;
import android.text.TextUtils;
import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.LongSparseLongArray;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Maps;
@@ -66,6 +71,20 @@ public class DownloadNotifier {
@GuardedBy("mActiveNotifs")
private final HashMap<String, Long> mActiveNotifs = Maps.newHashMap();
+ /**
+ * Current speed of active downloads, mapped from {@link DownloadInfo#mId}
+ * 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
+ * {@link SystemClock#elapsedRealtime()}.
+ */
+ @GuardedBy("mDownloadSpeed")
+ private final LongSparseLongArray mDownloadTouch = new LongSparseLongArray();
+
public DownloadNotifier(Context context) {
mContext = context;
mNotifManager = (NotificationManager) context.getSystemService(
@@ -77,6 +96,22 @@ public class DownloadNotifier {
}
/**
+ * Notify the current speed of an active download, used for calculating
+ * estimated remaining time.
+ */
+ public void notifyDownloadSpeed(long id, long bytesPerSecond) {
+ synchronized (mDownloadSpeed) {
+ if (bytesPerSecond != 0) {
+ mDownloadSpeed.put(id, bytesPerSecond);
+ mDownloadTouch.put(id, SystemClock.elapsedRealtime());
+ } else {
+ mDownloadSpeed.delete(id);
+ mDownloadTouch.delete(id);
+ }
+ }
+ }
+
+ /**
* Update {@link NotificationManager} to reflect the given set of
* {@link DownloadInfo}, adding, collapsing, and removing as needed.
*/
@@ -140,6 +175,7 @@ public class DownloadNotifier {
final DownloadInfo info = cluster.iterator().next();
final Uri uri = ContentUris.withAppendedId(
Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, info.mId);
+ builder.setAutoCancel(true);
final String action;
if (Downloads.Impl.isStatusError(info.mStatus)) {
@@ -167,16 +203,16 @@ public class DownloadNotifier {
String remainingText = null;
String percentText = null;
if (type == TYPE_ACTIVE) {
- final DownloadHandler handler = DownloadHandler.getInstance();
-
long current = 0;
long total = 0;
long speed = 0;
- for (DownloadInfo info : cluster) {
- if (info.mTotalBytes != -1) {
- current += info.mCurrentBytes;
- total += info.mTotalBytes;
- speed += handler.getCurrentSpeed(info.mId);
+ synchronized (mDownloadSpeed) {
+ for (DownloadInfo info : cluster) {
+ if (info.mTotalBytes != -1) {
+ current += info.mCurrentBytes;
+ total += info.mTotalBytes;
+ speed += mDownloadSpeed.get(info.mId);
+ }
}
}
@@ -283,6 +319,17 @@ public class DownloadNotifier {
return ids;
}
+ public void dumpSpeeds() {
+ synchronized (mDownloadSpeed) {
+ for (int i = 0; i < mDownloadSpeed.size(); i++) {
+ final long id = mDownloadSpeed.keyAt(i);
+ final long delta = SystemClock.elapsedRealtime() - mDownloadTouch.get(id);
+ Log.d(TAG, "Download " + id + " speed " + mDownloadSpeed.valueAt(i) + "bps, "
+ + delta + "ms ago");
+ }
+ }
+ }
+
/**
* Build tag used for collapsing several {@link DownloadInfo} into a single
* {@link Notification}.
@@ -309,7 +356,7 @@ public class DownloadNotifier {
}
private static boolean isActiveAndVisible(DownloadInfo download) {
- return Downloads.Impl.isStatusInformational(download.mStatus) &&
+ return download.mStatus == STATUS_RUNNING &&
(download.mVisibility == VISIBILITY_VISIBLE
|| download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
}
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index c554e41d..e0b5842d 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -37,17 +37,23 @@ import android.os.Binder;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.Process;
+import android.os.SELinux;
+import android.provider.BaseColumns;
import android.provider.Downloads;
import android.provider.OpenableColumns;
import android.text.TextUtils;
+import android.text.format.DateUtils;
import android.util.Log;
+import com.android.internal.util.IndentingPrintWriter;
import com.google.android.collect.Maps;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
+import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -384,7 +390,7 @@ public final class DownloadProvider extends ContentProvider {
Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
- Constants.FAILED_CONNECTIONS + " INTEGER, " +
+ Downloads.Impl.COLUMN_FAILED_CONNECTIONS + " INTEGER, " +
Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
@@ -436,8 +442,7 @@ public final class DownloadProvider extends ContentProvider {
appInfo = getContext().getPackageManager().
getApplicationInfo("com.android.defcontainer", 0);
} catch (NameNotFoundException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ Log.wtf(Constants.TAG, "Could not get ApplicationInfo for com.android.defconatiner", e);
}
if (appInfo != null) {
mDefContainerUid = appInfo.uid;
@@ -446,7 +451,12 @@ public final class DownloadProvider extends ContentProvider {
// saves us by getting some initialization code in DownloadService out of the way.
Context context = getContext();
context.startService(new Intent(context, DownloadService.class));
- mDownloadsDataDir = StorageManager.getInstance(getContext()).getDownloadDataDirectory();
+ mDownloadsDataDir = StorageManager.getDownloadDataDirectory(getContext());
+ try {
+ SELinux.restorecon(mDownloadsDataDir.getCanonicalPath());
+ } catch (IOException e) {
+ Log.wtf(Constants.TAG, "Could not get canonical path for download directory", e);
+ }
return true;
}
@@ -1199,6 +1209,41 @@ public final class DownloadProvider extends ContentProvider {
return ret;
}
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 120);
+
+ pw.println("Downloads updated in last hour:");
+ pw.increaseIndent();
+
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ final long modifiedAfter = mSystemFacade.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS;
+ final Cursor cursor = db.query(DB_TABLE, null,
+ Downloads.Impl.COLUMN_LAST_MODIFICATION + ">" + modifiedAfter, null, null, null,
+ Downloads.Impl._ID + " ASC");
+ try {
+ final String[] cols = cursor.getColumnNames();
+ final int idCol = cursor.getColumnIndex(BaseColumns._ID);
+ while (cursor.moveToNext()) {
+ pw.println("Download #" + cursor.getInt(idCol) + ":");
+ pw.increaseIndent();
+ for (int i = 0; i < cols.length; i++) {
+ // Omit sensitive data when dumping
+ if (Downloads.Impl.COLUMN_COOKIE_DATA.equals(cols[i])) {
+ continue;
+ }
+ pw.printPair(cols[i], cursor.getString(i));
+ }
+ pw.println();
+ pw.decreaseIndent();
+ }
+ } finally {
+ cursor.close();
+ }
+
+ pw.decreaseIndent();
+ }
+
private void logVerboseOpenFileInfo(Uri uri, String mode) {
Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
+ ", uid: " + Binder.getCallingUid());
@@ -1229,7 +1274,7 @@ public final class DownloadProvider extends ContentProvider {
Log.v(Constants.TAG, "file exists in openFile");
}
}
- cursor.close();
+ cursor.close();
}
}
diff --git a/src/com/android/providers/downloads/DownloadScanner.java b/src/com/android/providers/downloads/DownloadScanner.java
new file mode 100644
index 00000000..ca795062
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadScanner.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2013 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.LOGV;
+import static com.android.providers.downloads.Constants.TAG;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.provider.Downloads;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.google.common.collect.Maps;
+
+import java.util.HashMap;
+
+/**
+ * Manages asynchronous scanning of completed downloads.
+ */
+public class DownloadScanner implements MediaScannerConnectionClient {
+ private static final long SCAN_TIMEOUT = MINUTE_IN_MILLIS;
+
+ private final Context mContext;
+ private final MediaScannerConnection mConnection;
+
+ private static class ScanRequest {
+ public final long id;
+ public final String path;
+ public final String mimeType;
+ public final long requestRealtime;
+
+ public ScanRequest(long id, String path, String mimeType) {
+ this.id = id;
+ this.path = path;
+ this.mimeType = mimeType;
+ this.requestRealtime = SystemClock.elapsedRealtime();
+ }
+
+ public void exec(MediaScannerConnection conn) {
+ conn.scanFile(path, mimeType);
+ }
+ }
+
+ @GuardedBy("mConnection")
+ private HashMap<String, ScanRequest> mPending = Maps.newHashMap();
+
+ public DownloadScanner(Context context) {
+ mContext = context;
+ mConnection = new MediaScannerConnection(context, this);
+ }
+
+ /**
+ * Check if requested scans are still pending. Scans may timeout after an
+ * internal duration.
+ */
+ public boolean hasPendingScans() {
+ synchronized (mConnection) {
+ if (mPending.isEmpty()) {
+ return false;
+ } else {
+ // Check if pending scans have timed out
+ final long nowRealtime = SystemClock.elapsedRealtime();
+ for (ScanRequest req : mPending.values()) {
+ if (nowRealtime < req.requestRealtime + SCAN_TIMEOUT) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Request that given {@link DownloadInfo} be scanned at some point in
+ * future. Enqueues the request to be scanned asynchronously.
+ *
+ * @see #hasPendingScans()
+ */
+ public void requestScan(DownloadInfo info) {
+ if (LOGV) Log.v(TAG, "requestScan() for " + info.mFileName);
+ synchronized (mConnection) {
+ final ScanRequest req = new ScanRequest(info.mId, info.mFileName, info.mMimeType);
+ mPending.put(req.path, req);
+
+ if (mConnection.isConnected()) {
+ req.exec(mConnection);
+ } else {
+ mConnection.connect();
+ }
+ }
+ }
+
+ public void shutdown() {
+ mConnection.disconnect();
+ }
+
+ @Override
+ public void onMediaScannerConnected() {
+ synchronized (mConnection) {
+ for (ScanRequest req : mPending.values()) {
+ req.exec(mConnection);
+ }
+ }
+ }
+
+ @Override
+ public void onScanCompleted(String path, Uri uri) {
+ final ScanRequest req;
+ synchronized (mConnection) {
+ req = mPending.remove(path);
+ }
+ if (req == null) {
+ Log.w(TAG, "Missing request for path " + path);
+ return;
+ }
+
+ // Update scanned column, which will kick off a database update pass,
+ // eventually deciding if overall service is ready for teardown.
+ final ContentValues values = new ContentValues();
+ values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1);
+ if (uri != null) {
+ values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString());
+ }
+
+ final ContentResolver resolver = mContext.getContentResolver();
+ final Uri downloadUri = ContentUris.withAppendedId(
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, req.id);
+ final int rows = resolver.update(downloadUri, values, null, null);
+ if (rows == 0) {
+ // Local row disappeared during scan; download was probably deleted
+ // so clean up now-orphaned media entry.
+ resolver.delete(uri, null, null);
+ }
+ }
+}
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
index b97346b2..7d746cca 100644
--- a/src/com/android/providers/downloads/DownloadService.java
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -16,25 +16,25 @@
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.content.ComponentName;
-import android.content.ContentValues;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
-import android.content.ServiceConnection;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.database.Cursor;
-import android.media.IMediaScannerListener;
-import android.media.IMediaScannerService;
import android.net.Uri;
import android.os.Handler;
+import android.os.HandlerThread;
import android.os.IBinder;
+import android.os.Message;
import android.os.Process;
-import android.os.RemoteException;
import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
@@ -44,22 +44,41 @@ 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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
/**
- * Performs the background downloads requested by applications that use the Downloads provider.
+ * 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 {
- /** amount of time to wait to connect to MediaScannerService before timing out */
- private static final long WAIT_TIMEOUT = 10 * 1000;
+ // 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 StorageManager mStorageManager;
/** Observer to get notified when the content observer's data changes */
private DownloadManagerContentObserver mObserver;
@@ -74,118 +93,41 @@ public class DownloadService extends Service {
* content provider changes or disappears.
*/
@GuardedBy("mDownloads")
- private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
+ private final Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
- /**
- * The thread that updates the internal download list from the content
- * provider.
- */
- @VisibleForTesting
- UpdateThread mUpdateThread;
+ private final ExecutorService mExecutor = buildDownloadExecutor();
- /**
- * Whether the internal download list should be updated from the content
- * provider.
- */
- private boolean mPendingUpdate;
+ private static ExecutorService buildDownloadExecutor() {
+ final int maxConcurrent = Resources.getSystem().getInteger(
+ com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed);
- /**
- * The ServiceConnection object that tells us when we're connected to and disconnected from
- * the Media Scanner
- */
- private MediaScannerConnection mMediaScannerConnection;
+ // 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>());
+ executor.allowCoreThreadTimeOut(true);
+ return executor;
+ }
- private boolean mMediaScannerConnecting;
+ private DownloadScanner mScanner;
- /**
- * The IPC interface to the Media Scanner
- */
- private IMediaScannerService mMediaScannerService;
+ private HandlerThread mUpdateThread;
+ private Handler mUpdateHandler;
- @VisibleForTesting
- SystemFacade mSystemFacade;
-
- private StorageManager mStorageManager;
+ private volatile int mLastStartId;
/**
* Receives notifications when the data in the content provider changes
*/
private class DownloadManagerContentObserver extends ContentObserver {
-
public DownloadManagerContentObserver() {
super(new Handler());
}
- /**
- * Receives notification when the data in the observed content
- * provider changes.
- */
@Override
public void onChange(final boolean selfChange) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Service ContentObserver received notification");
- }
- updateFromProvider();
- }
-
- }
-
- /**
- * Gets called back when the connection to the media
- * scanner is established or lost.
- */
- public class MediaScannerConnection implements ServiceConnection {
- public void onServiceConnected(ComponentName className, IBinder service) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Connected to Media Scanner");
- }
- synchronized (DownloadService.this) {
- try {
- mMediaScannerConnecting = false;
- mMediaScannerService = IMediaScannerService.Stub.asInterface(service);
- if (mMediaScannerService != null) {
- updateFromProvider();
- }
- } finally {
- // notify anyone waiting on successful connection to MediaService
- DownloadService.this.notifyAll();
- }
- }
- }
-
- public void disconnectMediaScanner() {
- synchronized (DownloadService.this) {
- mMediaScannerConnecting = false;
- if (mMediaScannerService != null) {
- mMediaScannerService = null;
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Disconnecting from Media Scanner");
- }
- try {
- unbindService(this);
- } catch (IllegalArgumentException ex) {
- Log.w(Constants.TAG, "unbindService failed: " + ex);
- } finally {
- // notify anyone waiting on unsuccessful connection to MediaService
- DownloadService.this.notifyAll();
- }
- }
- }
- }
-
- public void onServiceDisconnected(ComponentName className) {
- try {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Disconnected from Media Scanner");
- }
- } finally {
- synchronized (DownloadService.this) {
- mMediaScannerService = null;
- mMediaScannerConnecting = false;
- // notify anyone waiting on disconnect from MediaService
- DownloadService.this.notifyAll();
- }
- }
+ enqueueUpdate();
}
}
@@ -214,19 +156,21 @@ public class DownloadService extends Service {
mSystemFacade = new RealSystemFacade(this);
}
- mObserver = new DownloadManagerContentObserver();
- getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- true, mObserver);
+ mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+ mStorageManager = new StorageManager(this);
- mMediaScannerService = null;
- mMediaScannerConnecting = false;
- mMediaScannerConnection = new MediaScannerConnection();
+ mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
+ mUpdateThread.start();
+ mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
+
+ mScanner = new DownloadScanner(this);
mNotifier = new DownloadNotifier(this);
mNotifier.cancelAll();
- mStorageManager = StorageManager.getInstance(getApplicationContext());
- updateFromProvider();
+ mObserver = new DownloadManagerContentObserver();
+ getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ true, mObserver);
}
@Override
@@ -235,16 +179,16 @@ public class DownloadService extends Service {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Service onStart");
}
- updateFromProvider();
+ mLastStartId = startId;
+ enqueueUpdate();
return returnValue;
}
- /**
- * Cleans up when the service is destroyed
- */
@Override
public void onDestroy() {
getContentResolver().unregisterContentObserver(mObserver);
+ mScanner.shutdown();
+ mUpdateThread.quit();
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Service onDestroy");
}
@@ -252,182 +196,179 @@ public class DownloadService extends Service {
}
/**
- * Parses data from the content provider into private array
+ * Enqueue an {@link #updateLocked()} pass to occur in future.
*/
- private void updateFromProvider() {
- synchronized (this) {
- mPendingUpdate = true;
- if (mUpdateThread == null) {
- mUpdateThread = new UpdateThread();
- mSystemFacade.startThread(mUpdateThread);
- }
- }
+ private void enqueueUpdate() {
+ mUpdateHandler.removeMessages(MSG_UPDATE);
+ mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget();
}
- private class UpdateThread extends Thread {
- public UpdateThread() {
- super("Download Service");
- }
+ /**
+ * 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 void run() {
+ public boolean handleMessage(Message msg) {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- boolean keepService = false;
- // for each update from the database, remember which download is
- // supposed to get restarted soonest in the future
- long wakeUp = Long.MAX_VALUE;
- for (;;) {
- synchronized (DownloadService.this) {
- if (mUpdateThread != this) {
- throw new IllegalStateException(
- "multiple UpdateThreads in DownloadService");
- }
- if (!mPendingUpdate) {
- mUpdateThread = null;
- if (!keepService) {
- stopSelf();
- }
- if (wakeUp != Long.MAX_VALUE) {
- scheduleAlarm(wakeUp);
- }
- return;
+
+ 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()));
}
- mPendingUpdate = false;
}
- synchronized (mDownloads) {
- long now = mSystemFacade.currentTimeMillis();
- boolean mustScan = false;
- keepService = false;
- wakeUp = Long.MAX_VALUE;
- Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet());
-
- Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- null, null, null, null);
- if (cursor == null) {
- continue;
- }
- try {
- DownloadInfo.Reader reader =
- new DownloadInfo.Reader(getContentResolver(), cursor);
- int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
- if (Constants.LOGVV) {
- Log.i(Constants.TAG, "number of rows from downloads-db: " +
- cursor.getCount());
- }
- for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
- long id = cursor.getLong(idColumn);
- idsNoLongerInDatabase.remove(id);
- DownloadInfo info = mDownloads.get(id);
- if (info != null) {
- updateDownload(reader, info, now);
- } else {
- info = insertDownloadLocked(reader, now);
- }
-
- if (info.shouldScanFile() && !scanFile(info, true, false)) {
- mustScan = true;
- keepService = true;
- }
- if (info.hasCompletionNotification()) {
- keepService = true;
- }
- long next = info.nextAction(now);
- if (next == 0) {
- keepService = true;
- } else if (next > 0 && next < wakeUp) {
- wakeUp = next;
- }
- }
- } finally {
- cursor.close();
- }
+ // Dump speed and update details
+ mNotifier.dumpSpeeds();
- for (Long id : idsNoLongerInDatabase) {
- deleteDownloadLocked(id);
- }
+ Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive
+ + "; someone didn't update correctly.");
+ }
- // is there a need to start the DownloadService? yes, if there are rows to be
- // deleted.
- if (!mustScan) {
- for (DownloadInfo info : mDownloads.values()) {
- if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) {
- mustScan = true;
- keepService = true;
- break;
- }
- }
- }
- mNotifier.updateWith(mDownloads.values());
- if (mustScan) {
- bindMediaScanner();
- } else {
- mMediaScannerConnection.disconnectMediaScanner();
+ if (isActive) {
+ // Still doing useful work, keep service alive. These active
+ // tasks will trigger another update pass when they're finished.
+
+ // Enqueue delayed update pass to catch finished operations that
+ // didn't trigger an update pass; these are bugs.
+ enqueueFinalUpdate();
+
+ } else {
+ // No active tasks, and any pending update messages can be
+ // ignored, since any updates important enough to initiate tasks
+ // will always be delivered with a new startId.
+
+ if (stopSelfResult(startId)) {
+ if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped");
+ getContentResolver().unregisterContentObserver(mObserver);
+ mScanner.shutdown();
+ mUpdateThread.quit();
+ }
+ }
+
+ return true;
+ }
+ };
+
+ /**
+ * Update {@link #mDownloads} to match {@link DownloadProvider} state.
+ * Depending on current download state it may enqueue {@link DownloadThread}
+ * instances, request {@link DownloadScanner} scans, update user-visible
+ * notifications, and/or schedule future actions with {@link AlarmManager}.
+ * <p>
+ * Should only be called from {@link #mUpdateThread} as after being
+ * requested through {@link #enqueueUpdate()}.
+ *
+ * @return If there are active tasks being processed, as of the database
+ * snapshot taken in this update.
+ */
+ private boolean updateLocked() {
+ final long now = mSystemFacade.currentTimeMillis();
+
+ boolean isActive = false;
+ long nextActionMillis = Long.MAX_VALUE;
+
+ final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet());
+
+ final ContentResolver resolver = getContentResolver();
+ final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ null, null, null, null);
+ try {
+ final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
+ final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
+ while (cursor.moveToNext()) {
+ final long id = cursor.getLong(idColumn);
+ staleIds.remove(id);
+
+ DownloadInfo info = mDownloads.get(id);
+ if (info != null) {
+ updateDownload(reader, info, now);
+ } else {
+ info = insertDownloadLocked(reader, now);
+ }
+
+ if (info.mDeleted) {
+ // Delete download if requested, but only after cleaning up
+ if (!TextUtils.isEmpty(info.mMediaProviderUri)) {
+ resolver.delete(Uri.parse(info.mMediaProviderUri), null, null);
}
- // look for all rows with deleted flag set and delete the rows from the database
- // permanently
- for (DownloadInfo info : mDownloads.values()) {
- if (info.mDeleted) {
- // this row is to be deleted from the database. but does it have
- // mediaProviderUri?
- if (TextUtils.isEmpty(info.mMediaProviderUri)) {
- if (info.shouldScanFile()) {
- // initiate rescan of the file to - which will populate
- // mediaProviderUri column in this row
- if (!scanFile(info, false, true)) {
- throw new IllegalStateException("scanFile failed!");
- }
- continue;
- }
- } else {
- // yes it has mediaProviderUri column already filled in.
- // delete it from MediaProvider database.
- getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null,
- null);
- }
- // delete the file
- deleteFileIfExists(info.mFileName);
- // delete from the downloads db
- getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- Downloads.Impl._ID + " = ? ",
- new String[]{String.valueOf(info.mId)});
- }
+ 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();
}
- private void bindMediaScanner() {
- if (!mMediaScannerConnecting) {
- Intent intent = new Intent();
- intent.setClassName("com.android.providers.media",
- "com.android.providers.media.MediaScannerService");
- mMediaScannerConnecting = true;
- bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
- }
+ // Clean up stale downloads that disappeared
+ for (Long id : staleIds) {
+ deleteDownloadLocked(id);
}
- private void scheduleAlarm(long wakeUp) {
- AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
- if (alarms == null) {
- Log.e(Constants.TAG, "couldn't get alarm manager");
- return;
- }
+ // 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(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
+ Log.v(TAG, "scheduling start in " + nextActionMillis + "ms");
}
- Intent intent = new Intent(Constants.ACTION_RETRY);
- intent.setClassName("com.android.providers.downloads",
- DownloadReceiver.class.getName());
- alarms.set(
- AlarmManager.RTC_WAKEUP,
- mSystemFacade.currentTimeMillis() + wakeUp,
- PendingIntent.getBroadcast(DownloadService.this, 0, intent,
- PendingIntent.FLAG_ONE_SHOT));
+ 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;
}
/**
@@ -435,14 +376,14 @@ public class DownloadService extends Service {
* download if appropriate.
*/
private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) {
- DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
+ final DownloadInfo info = reader.newDownloadInfo(
+ this, mSystemFacade, mStorageManager, mNotifier);
mDownloads.put(info.mId, info);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "processing inserted download " + info.mId);
}
- info.startIfReady(now, mStorageManager);
return info;
}
@@ -450,15 +391,11 @@ public class DownloadService extends Service {
* Updates the local copy of the info about a download.
*/
private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
- int oldVisibility = info.mVisibility;
- int oldStatus = info.mStatus;
-
reader.updateFromDatabase(info);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "processing updated download " + info.mId +
", status: " + info.mStatus);
}
- info.startIfReady(now, mStorageManager);
}
/**
@@ -473,88 +410,20 @@ public class DownloadService extends Service {
if (Constants.LOGVV) {
Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
}
- new File(info.mFileName).delete();
+ deleteFileIfExists(info.mFileName);
}
mDownloads.remove(info.mId);
}
- /**
- * Attempts to scan the file if necessary.
- * @return true if the file has been properly scanned.
- */
- private boolean scanFile(DownloadInfo info, final boolean updateDatabase,
- final boolean deleteFile) {
- synchronized (this) {
- if (mMediaScannerService == null) {
- // not bound to mediaservice. but if in the process of connecting to it, wait until
- // connection is resolved
- while (mMediaScannerConnecting) {
- Log.d(Constants.TAG, "waiting for mMediaScannerService service: ");
- try {
- this.wait(WAIT_TIMEOUT);
- } catch (InterruptedException e1) {
- throw new IllegalStateException("wait interrupted");
- }
- }
- }
- // do we have mediaservice?
- if (mMediaScannerService == null) {
- // no available MediaService And not even in the process of connecting to it
- return false;
- }
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "Scanning file " + info.mFileName);
- }
- try {
- final Uri key = info.getAllDownloadsUri();
- final long id = info.mId;
- mMediaScannerService.requestScanFile(info.mFileName, info.mMimeType,
- new IMediaScannerListener.Stub() {
- public void scanCompleted(String path, Uri uri) {
- if (updateDatabase) {
- // Mark this as 'scanned' in the database
- // so that it is NOT subject to re-scanning by MediaScanner
- // next time this database row row is encountered
- ContentValues values = new ContentValues();
- values.put(Constants.MEDIA_SCANNED, 1);
- if (uri != null) {
- values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
- uri.toString());
- }
- getContentResolver().update(key, values, null, null);
- } else if (deleteFile) {
- if (uri != null) {
- // use the Uri returned to delete it from the MediaProvider
- getContentResolver().delete(uri, null, null);
- }
- // delete the file and delete its row from the downloads db
- deleteFileIfExists(path);
- getContentResolver().delete(
- Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- Downloads.Impl._ID + " = ? ",
- new String[]{String.valueOf(id)});
- }
- }
- });
- return true;
- } catch (RemoteException e) {
- Log.w(Constants.TAG, "Failed to scan file " + info.mFileName);
- return false;
- }
- }
- }
-
private void deleteFileIfExists(String path) {
- try {
- if (!TextUtils.isEmpty(path)) {
- if (Constants.LOGVV) {
- Log.d(TAG, "deleteFileIfExists() deleting " + path);
- }
- File file = new File(path);
- file.delete();
+ 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");
}
- } catch (Exception e) {
- Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
}
}
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index 34bc8e34..6a0eb47e 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -16,16 +16,33 @@
package com.android.providers.downloads;
+import static android.provider.Downloads.Impl.STATUS_BAD_REQUEST;
+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_TOO_MANY_REDIRECTS;
+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;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_PARTIAL;
+import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
+import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.drm.DrmManagerClient;
+import android.drm.DrmOutputStream;
+import android.net.ConnectivityManager;
import android.net.INetworkPolicyListener;
+import android.net.NetworkInfo;
import android.net.NetworkPolicyManager;
-import android.net.Proxy;
import android.net.TrafficStats;
-import android.net.http.AndroidHttpClient;
import android.os.FileUtils;
import android.os.PowerManager;
import android.os.Process;
@@ -35,39 +52,51 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
-import org.apache.http.Header;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.conn.params.ConnRouteParams;
+import com.android.providers.downloads.DownloadInfo.NetworkState;
import java.io.File;
-import java.io.FileNotFoundException;
+import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.SyncFailedException;
-import java.net.URI;
-import java.net.URISyntaxException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+
+import libcore.io.IoUtils;
/**
- * Runs an actual download
+ * Task which executes a given {@link DownloadInfo}: making network requests,
+ * persisting data to disk, and updating {@link DownloadProvider}.
*/
-public class DownloadThread extends Thread {
+public class DownloadThread implements Runnable {
+
+ // TODO: bind each download to a specific network interface to avoid state
+ // checking races once we have ConnectivityManager API
+
+ private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
+ private static final int HTTP_TEMP_REDIRECT = 307;
+
+ private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS);
private final Context mContext;
private final DownloadInfo mInfo;
private final SystemFacade mSystemFacade;
private final StorageManager mStorageManager;
- private DrmConvertSession mDrmConvertSession;
+ private final DownloadNotifier mNotifier;
private volatile boolean mPolicyDirty;
public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
- StorageManager storageManager) {
+ StorageManager storageManager, DownloadNotifier notifier) {
mContext = context;
mSystemFacade = systemFacade;
mInfo = info;
mStorageManager = storageManager;
+ mNotifier = notifier;
}
/**
@@ -86,12 +115,8 @@ public class DownloadThread extends Thread {
*/
static class State {
public String mFilename;
- public FileOutputStream mStream;
public String mMimeType;
- public boolean mCountRetry = false;
public int mRetryAfter = 0;
- public int mRedirectCount = 0;
- public String mNewUri;
public boolean mGotData = false;
public String mRequestUri;
public long mTotalBytes = -1;
@@ -100,6 +125,7 @@ public class DownloadThread extends Thread {
public boolean mContinuingDownload = false;
public long mBytesNotified = 0;
public long mTimeLastNotification = 0;
+ public int mNetworkType = ConnectivityManager.TYPE_NONE;
/** Historical bytes/second speed of this download. */
public long mSpeed;
@@ -108,6 +134,13 @@ public class DownloadThread extends Thread {
/** Bytes transferred since current sample started. */
public long mSpeedSampleBytes;
+ public long mContentLength = -1;
+ public String mContentDisposition;
+ public String mContentLocation;
+
+ public int mRedirectionCount;
+ public URL mUrl;
+
public State(DownloadInfo info) {
mMimeType = Intent.normalizeMimeType(info.mMimeType);
mRequestUri = info.mUri;
@@ -115,33 +148,23 @@ public class DownloadThread extends Thread {
mTotalBytes = info.mTotalBytes;
mCurrentBytes = info.mCurrentBytes;
}
- }
- /**
- * State within executeDownload()
- */
- private static class InnerState {
- public String mHeaderContentLength;
- public String mHeaderContentDisposition;
- public String mHeaderContentLocation;
+ public void resetBeforeExecute() {
+ // Reset any state from previous execution
+ mContentLength = -1;
+ mContentDisposition = null;
+ mContentLocation = null;
+ mRedirectionCount = 0;
+ }
}
- /**
- * Raised from methods called by executeDownload() to indicate that the download should be
- * retried immediately.
- */
- private class RetryDownload extends Throwable {}
-
- /**
- * Executes the download in a separate thread
- */
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
runInternal();
} finally {
- DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
+ mNotifier.notifyDownloadSpeed(mInfo.mId, 0);
}
}
@@ -155,9 +178,9 @@ public class DownloadThread extends Thread {
}
State state = new State(mInfo);
- AndroidHttpClient client = null;
PowerManager.WakeLock wakeLock = null;
int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
+ int numFailed = mInfo.mNumFailed;
String errorMsg = null;
final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
@@ -170,39 +193,29 @@ public class DownloadThread extends Thread {
// while performing download, register for rules updates
netPolicy.registerListener(mPolicyListener);
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
- }
+ Log.i(Constants.TAG, "Download " + mInfo.mId + " starting");
- client = AndroidHttpClient.newInstance(userAgent(), mContext);
+ // Remember which network this download started on; used to
+ // determine if errors were due to network changes.
+ final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
+ if (info != null) {
+ state.mNetworkType = info.getType();
+ }
- // network traffic on this thread should be counted against the
- // requesting uid, and is tagged with well-known value.
+ // Network traffic on this thread should be counted against the
+ // requesting UID, and is tagged with well-known value.
TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
TrafficStats.setThreadStatsUid(mInfo.mUid);
- boolean finished = false;
- while(!finished) {
- Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId);
- // Set or unset proxy, which may have changed since last GET request.
- // setDefaultProxy() supports null as proxy parameter.
- ConnRouteParams.setDefaultProxy(client.getParams(),
- Proxy.getPreferredHttpHost(mContext, state.mRequestUri));
- HttpGet request = new HttpGet(state.mRequestUri);
- try {
- executeDownload(state, client, request);
- finished = true;
- } catch (RetryDownload exc) {
- // fall through
- } finally {
- request.abort();
- request = null;
- }
+ try {
+ // TODO: migrate URL sanity checking into client side of API
+ state.mUrl = new URL(state.mRequestUri);
+ } catch (MalformedURLException e) {
+ throw new StopRequestException(STATUS_BAD_REQUEST, e);
}
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
- }
+ executeDownload(state);
+
finalizeDestinationFile(state);
finalStatus = Downloads.Impl.STATUS_SUCCESS;
} catch (StopRequestException error) {
@@ -213,9 +226,37 @@ public class DownloadThread extends Thread {
if (Constants.LOGV) {
Log.w(Constants.TAG, msg, error);
}
- finalStatus = error.mFinalStatus;
+ finalStatus = error.getFinalStatus();
+
+ // Nobody below our level should request retries, since we handle
+ // failure counts at this level.
+ if (finalStatus == STATUS_WAITING_TO_RETRY) {
+ throw new IllegalStateException("Execution should always throw final error codes");
+ }
+
+ // Some errors should be retryable, unless we fail too many times.
+ if (isStatusRetryable(finalStatus)) {
+ if (state.mGotData) {
+ numFailed = 1;
+ } else {
+ numFailed += 1;
+ }
+
+ if (numFailed < Constants.MAX_RETRIES) {
+ final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
+ if (info != null && info.getType() == state.mNetworkType
+ && info.isConnected()) {
+ // Underlying network is still intact, use normal backoff
+ finalStatus = STATUS_WAITING_TO_RETRY;
+ } else {
+ // Network changed, retry on any next available
+ finalStatus = STATUS_WAITING_FOR_NETWORK;
+ }
+ }
+ }
+
// fall through to finally block
- } catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions
+ } catch (Throwable ex) {
errorMsg = ex.getMessage();
String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
Log.w(Constants.TAG, msg, ex);
@@ -225,14 +266,11 @@ public class DownloadThread extends Thread {
TrafficStats.clearThreadStatsTag();
TrafficStats.clearThreadStatsUid();
- if (client != null) {
- client.close();
- client = null;
- }
cleanupDestination(state, finalStatus);
- notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
- state.mGotData, state.mFilename,
- state.mNewUri, state.mMimeType, errorMsg);
+ notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed);
+
+ Log.i(Constants.TAG, "Download " + mInfo.mId + " finished with status "
+ + Downloads.Impl.statusToString(finalStatus));
netPolicy.unregisterListener(mPolicyListener);
@@ -245,16 +283,12 @@ public class DownloadThread extends Thread {
}
/**
- * Fully execute a single download request - setup and send the request, handle the response,
- * and transfer the data to the destination file.
+ * Fully execute a single download request. Setup and send the request,
+ * handle the response, and transfer the data to the destination file.
*/
- private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
- throws StopRequestException, RetryDownload {
- InnerState innerState = new InnerState();
- byte data[] = new byte[Constants.BUFFER_SIZE];
-
- setupDestinationFile(state, innerState);
- addRequestHeaders(state, request);
+ private void executeDownload(State state) throws StopRequestException {
+ state.resetBeforeExecute();
+ setupDestinationFile(state);
// skip when already finished; remove after fixing race in 5217390
if (state.mCurrentBytes == state.mTotalBytes) {
@@ -263,19 +297,136 @@ public class DownloadThread extends Thread {
return;
}
- // check just before sending the request to avoid using an invalid connection at all
- checkConnectivity();
-
- HttpResponse response = sendRequest(state, client, request);
- handleExceptionalStatus(state, innerState, response);
+ while (state.mRedirectionCount++ < Constants.MAX_REDIRECTS) {
+ // Open connection and follow any redirects until we have a useful
+ // response with body.
+ HttpURLConnection conn = null;
+ try {
+ checkConnectivity();
+ conn = (HttpURLConnection) state.mUrl.openConnection();
+ conn.setInstanceFollowRedirects(false);
+ conn.setConnectTimeout(DEFAULT_TIMEOUT);
+ conn.setReadTimeout(DEFAULT_TIMEOUT);
+
+ addRequestHeaders(state, conn);
+
+ final int responseCode = conn.getResponseCode();
+ switch (responseCode) {
+ case HTTP_OK:
+ if (state.mContinuingDownload) {
+ throw new StopRequestException(
+ STATUS_CANNOT_RESUME, "Expected partial, but received OK");
+ }
+ processResponseHeaders(state, conn);
+ transferData(state, conn);
+ return;
+
+ case HTTP_PARTIAL:
+ if (!state.mContinuingDownload) {
+ throw new StopRequestException(
+ STATUS_CANNOT_RESUME, "Expected OK, but received partial");
+ }
+ transferData(state, conn);
+ return;
+
+ case HTTP_MOVED_PERM:
+ case HTTP_MOVED_TEMP:
+ case HTTP_SEE_OTHER:
+ case HTTP_TEMP_REDIRECT:
+ final String location = conn.getHeaderField("Location");
+ state.mUrl = new URL(state.mUrl, location);
+ if (responseCode == HTTP_MOVED_PERM) {
+ // Push updated URL back to database
+ state.mRequestUri = state.mUrl.toString();
+ }
+ continue;
+
+ case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
+ throw new StopRequestException(
+ STATUS_CANNOT_RESUME, "Requested range not satisfiable");
+
+ case HTTP_UNAVAILABLE:
+ parseRetryAfterHeaders(state, conn);
+ throw new StopRequestException(
+ HTTP_UNAVAILABLE, conn.getResponseMessage());
+
+ case HTTP_INTERNAL_ERROR:
+ throw new StopRequestException(
+ HTTP_INTERNAL_ERROR, conn.getResponseMessage());
+
+ default:
+ StopRequestException.throwUnhandledHttpError(
+ responseCode, conn.getResponseMessage());
+ }
+ } catch (IOException e) {
+ // Trouble with low-level sockets
+ throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "received response for " + mInfo.mUri);
+ } finally {
+ if (conn != null) conn.disconnect();
+ }
}
- processResponseHeaders(state, innerState, response);
- InputStream entityStream = openResponseEntity(state, response);
- transferData(state, innerState, data, entityStream);
+ throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
+ }
+
+ /**
+ * Transfer data from the given connection to the destination file.
+ */
+ private void transferData(State state, HttpURLConnection conn) throws StopRequestException {
+ DrmManagerClient drmClient = null;
+ InputStream in = null;
+ OutputStream out = null;
+ FileDescriptor outFd = null;
+ try {
+ try {
+ in = conn.getInputStream();
+ } catch (IOException e) {
+ throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
+ }
+
+ try {
+ if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
+ drmClient = new DrmManagerClient(mContext);
+ final RandomAccessFile file = new RandomAccessFile(
+ new File(state.mFilename), "rw");
+ out = new DrmOutputStream(drmClient, file, state.mMimeType);
+ outFd = file.getFD();
+ } else {
+ out = new FileOutputStream(state.mFilename, true);
+ outFd = ((FileOutputStream) out).getFD();
+ }
+ } catch (IOException e) {
+ throw new StopRequestException(STATUS_FILE_ERROR, e);
+ }
+
+ // Start streaming data, periodically watch for pause/cancel
+ // commands and checking disk space as needed.
+ transferData(state, in, out);
+
+ try {
+ if (out instanceof DrmOutputStream) {
+ ((DrmOutputStream) out).finish();
+ }
+ } catch (IOException e) {
+ throw new StopRequestException(STATUS_FILE_ERROR, e);
+ }
+
+ } finally {
+ if (drmClient != null) {
+ drmClient.release();
+ }
+
+ IoUtils.closeQuietly(in);
+
+ try {
+ if (out != null) out.flush();
+ if (outFd != null) outFd.sync();
+ } catch (IOException e) {
+ } finally {
+ IoUtils.closeQuietly(out);
+ }
+ }
}
/**
@@ -285,40 +436,38 @@ public class DownloadThread extends Thread {
// checking connectivity will apply current policy
mPolicyDirty = false;
- int networkUsable = mInfo.checkCanUseNetwork();
- if (networkUsable != DownloadInfo.NETWORK_OK) {
+ final NetworkState networkUsable = mInfo.checkCanUseNetwork();
+ if (networkUsable != NetworkState.OK) {
int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
- if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
+ if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) {
status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
mInfo.notifyPauseDueToSize(true);
- } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
+ } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
mInfo.notifyPauseDueToSize(false);
}
- throw new StopRequestException(status,
- mInfo.getLogMessageForNetworkError(networkUsable));
+ throw new StopRequestException(status, networkUsable.name());
}
}
/**
- * Transfer as much data as possible from the HTTP response to the destination file.
- * @param data buffer to use to read data
- * @param entityStream stream for reading the HTTP response entity
+ * Transfer as much data as possible from the HTTP response to the
+ * destination file.
*/
- private void transferData(
- State state, InnerState innerState, byte[] data, InputStream entityStream)
+ private void transferData(State state, InputStream in, OutputStream out)
throws StopRequestException {
+ final byte data[] = new byte[Constants.BUFFER_SIZE];
for (;;) {
- int bytesRead = readFromResponse(state, innerState, data, entityStream);
+ int bytesRead = readFromResponse(state, data, in);
if (bytesRead == -1) { // success, end of stream already reached
- handleEndOfStream(state, innerState);
+ handleEndOfStream(state);
return;
}
state.mGotData = true;
- writeDataToDestination(state, data, bytesRead);
+ writeDataToDestination(state, data, bytesRead, out);
state.mCurrentBytes += bytesRead;
- reportProgress(state, innerState);
+ reportProgress(state);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
@@ -332,11 +481,10 @@ public class DownloadThread extends Thread {
/**
* Called after a successful completion to take any necessary action on the downloaded file.
*/
- private void finalizeDestinationFile(State state) throws StopRequestException {
+ private void finalizeDestinationFile(State state) {
if (state.mFilename != null) {
// make sure the file is readable
FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
- syncDestination(state);
}
}
@@ -345,11 +493,6 @@ public class DownloadThread extends Thread {
* the downloaded file.
*/
private void cleanupDestination(State state, int finalStatus) {
- if (mDrmConvertSession != null) {
- finalStatus = mDrmConvertSession.close(state.mFilename);
- }
-
- closeDestination(state);
if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
if (Constants.LOGVV) {
Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
@@ -360,53 +503,6 @@ public class DownloadThread extends Thread {
}
/**
- * Sync the destination file to storage.
- */
- private void syncDestination(State state) {
- FileOutputStream downloadedFileStream = null;
- try {
- downloadedFileStream = new FileOutputStream(state.mFilename, true);
- downloadedFileStream.getFD().sync();
- } catch (FileNotFoundException ex) {
- Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
- } catch (SyncFailedException ex) {
- Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
- } catch (IOException ex) {
- Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
- } catch (RuntimeException ex) {
- Log.w(Constants.TAG, "exception while syncing file: ", ex);
- } finally {
- if(downloadedFileStream != null) {
- try {
- downloadedFileStream.close();
- } catch (IOException ex) {
- Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
- } catch (RuntimeException ex) {
- Log.w(Constants.TAG, "exception while closing file: ", ex);
- }
- }
- }
- }
-
- /**
- * Close the destination output stream.
- */
- private void closeDestination(State state) {
- try {
- // close the file
- if (state.mStream != null) {
- state.mStream.close();
- state.mStream = null;
- }
- } catch (IOException ex) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
- }
- // nothing can really be done if the file can't be closed
- }
- }
-
- /**
* Check if the download has been paused or canceled, stopping the request appropriately if it
* has been.
*/
@@ -430,7 +526,7 @@ public class DownloadThread extends Thread {
/**
* Report download progress through the database if necessary.
*/
- private void reportProgress(State state, InnerState innerState) {
+ private void reportProgress(State state) {
final long now = SystemClock.elapsedRealtime();
final long sampleDelta = now - state.mSpeedSampleStart;
@@ -444,10 +540,13 @@ public class DownloadThread extends Thread {
state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4;
}
+ // Only notify once we have a full sample window
+ if (state.mSpeedSampleStart != 0) {
+ mNotifier.notifyDownloadSpeed(mInfo.mId, state.mSpeed);
+ }
+
state.mSpeedSampleStart = now;
state.mSpeedSampleBytes = state.mCurrentBytes;
-
- DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed);
}
if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
@@ -465,37 +564,25 @@ public class DownloadThread extends Thread {
* @param data buffer containing the data to write
* @param bytesRead how many bytes to write from the buffer
*/
- private void writeDataToDestination(State state, byte[] data, int bytesRead)
+ private void writeDataToDestination(State state, byte[] data, int bytesRead, OutputStream out)
throws StopRequestException {
- for (;;) {
+ mStorageManager.verifySpaceBeforeWritingToFile(
+ mInfo.mDestination, state.mFilename, bytesRead);
+
+ boolean forceVerified = false;
+ while (true) {
try {
- if (state.mStream == null) {
- state.mStream = new FileOutputStream(state.mFilename, true);
- }
- mStorageManager.verifySpaceBeforeWritingToFile(mInfo.mDestination, state.mFilename,
- bytesRead);
- if (!DownloadDrmHelper.isDrmConvertNeeded(mInfo.mMimeType)) {
- state.mStream.write(data, 0, bytesRead);
- } else {
- byte[] convertedData = mDrmConvertSession.convert(data, bytesRead);
- if (convertedData != null) {
- state.mStream.write(convertedData, 0, convertedData.length);
- } else {
- throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
- "Error converting drm data.");
- }
- }
+ out.write(data, 0, bytesRead);
return;
} catch (IOException ex) {
- // couldn't write to file. are we out of space? check.
- // TODO this check should only be done once. why is this being done
- // in a while(true) loop (see the enclosing statement: for(;;)
- if (state.mStream != null) {
+ // TODO: better differentiate between DRM and disk failures
+ if (!forceVerified) {
+ // couldn't write to file. are we out of space? check.
mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
- }
- } finally {
- if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
- closeDestination(state);
+ forceVerified = true;
+ } else {
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
+ "Failed to write data: " + ex);
}
}
}
@@ -505,29 +592,30 @@ public class DownloadThread extends Thread {
* Called when we've reached the end of the HTTP response stream, to update the database and
* check for consistency.
*/
- private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException {
+ private void handleEndOfStream(State state) throws StopRequestException {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
- if (innerState.mHeaderContentLength == null) {
+ if (state.mContentLength == -1) {
values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
}
mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
- boolean lengthMismatched = (innerState.mHeaderContentLength != null)
- && (state.mCurrentBytes != Integer.parseInt(innerState.mHeaderContentLength));
+ final boolean lengthMismatched = (state.mContentLength != -1)
+ && (state.mCurrentBytes != state.mContentLength);
if (lengthMismatched) {
if (cannotResume(state)) {
- throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
- "mismatched content length");
+ throw new StopRequestException(STATUS_CANNOT_RESUME,
+ "mismatched content length; unable to resume");
} else {
- throw new StopRequestException(getFinalStatusForHttpError(state),
+ throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
"closed socket before end of file");
}
}
}
private boolean cannotResume(State state) {
- return state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null;
+ return (state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null)
+ || DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType);
}
/**
@@ -536,91 +624,51 @@ public class DownloadThread extends Thread {
* @param entityStream stream for reading the HTTP response entity
* @return the number of bytes actually read or -1 if the end of the stream has been reached
*/
- private int readFromResponse(State state, InnerState innerState, byte[] data,
- InputStream entityStream) throws StopRequestException {
+ private int readFromResponse(State state, byte[] data, InputStream entityStream)
+ throws StopRequestException {
try {
return entityStream.read(data);
} catch (IOException ex) {
- logNetworkState(mInfo.mUid);
+ // TODO: handle stream errors the same as other retries
+ if ("unexpected end of stream".equals(ex.getMessage())) {
+ return -1;
+ }
+
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
if (cannotResume(state)) {
- String message = "while reading response: " + ex.toString()
- + ", can't resume interrupted download with no ETag";
- throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
- message, ex);
+ throw new StopRequestException(STATUS_CANNOT_RESUME,
+ "Failed reading response: " + ex + "; unable to resume", ex);
} else {
- throw new StopRequestException(getFinalStatusForHttpError(state),
- "while reading response: " + ex.toString(), ex);
+ throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
+ "Failed reading response: " + ex, ex);
}
}
}
/**
- * Open a stream for the HTTP response entity, handling I/O errors.
- * @return an InputStream to read the response entity
+ * Prepare target file based on given network response. Derives filename and
+ * target size as needed.
*/
- private InputStream openResponseEntity(State state, HttpResponse response)
+ private void processResponseHeaders(State state, HttpURLConnection conn)
throws StopRequestException {
- try {
- return response.getEntity().getContent();
- } catch (IOException ex) {
- logNetworkState(mInfo.mUid);
- throw new StopRequestException(getFinalStatusForHttpError(state),
- "while getting entity: " + ex.toString(), ex);
- }
- }
+ // TODO: fallocate the entire file if header gave us specific length
- private void logNetworkState(int uid) {
- if (Constants.LOGX) {
- Log.i(Constants.TAG,
- "Net " + (Helpers.isNetworkAvailable(mSystemFacade, uid) ? "Up" : "Down"));
- }
- }
-
- /**
- * Read HTTP response headers and take appropriate action, including setting up the destination
- * file and updating the database.
- */
- private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
- throws StopRequestException {
- if (state.mContinuingDownload) {
- // ignore response headers on resume requests
- return;
- }
-
- readResponseHeaders(state, innerState, response);
- if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
- mDrmConvertSession = DrmConvertSession.open(mContext, state.mMimeType);
- if (mDrmConvertSession == null) {
- throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, "Mimetype "
- + state.mMimeType + " can not be converted.");
- }
- }
+ readResponseHeaders(state, conn);
state.mFilename = Helpers.generateSaveFile(
mContext,
mInfo.mUri,
mInfo.mHint,
- innerState.mHeaderContentDisposition,
- innerState.mHeaderContentLocation,
+ state.mContentDisposition,
+ state.mContentLocation,
state.mMimeType,
mInfo.mDestination,
- (innerState.mHeaderContentLength != null) ?
- Long.parseLong(innerState.mHeaderContentLength) : 0,
- mInfo.mIsPublicApi, mStorageManager);
- try {
- state.mStream = new FileOutputStream(state.mFilename);
- } catch (FileNotFoundException exc) {
- throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
- "while opening destination file: " + exc.toString(), exc);
- }
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
- }
+ state.mContentLength,
+ mStorageManager);
- updateDatabaseFromHeaders(state, innerState);
+ updateDatabaseFromHeaders(state);
// check connectivity again now that we know the total size
checkConnectivity();
}
@@ -629,7 +677,7 @@ public class DownloadThread extends Thread {
* Update necessary database fields based on values of HTTP response headers that have been
* read.
*/
- private void updateDatabaseFromHeaders(State state, InnerState innerState) {
+ private void updateDatabaseFromHeaders(State state) {
ContentValues values = new ContentValues();
values.put(Downloads.Impl._DATA, state.mFilename);
if (state.mHeaderETag != null) {
@@ -645,219 +693,48 @@ public class DownloadThread extends Thread {
/**
* Read headers from the HTTP response and store them into local state.
*/
- private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
+ private void readResponseHeaders(State state, HttpURLConnection conn)
throws StopRequestException {
- Header header = response.getFirstHeader("Content-Disposition");
- if (header != null) {
- innerState.mHeaderContentDisposition = header.getValue();
- }
- header = response.getFirstHeader("Content-Location");
- if (header != null) {
- innerState.mHeaderContentLocation = header.getValue();
- }
- if (state.mMimeType == null) {
- header = response.getFirstHeader("Content-Type");
- if (header != null) {
- state.mMimeType = Intent.normalizeMimeType(header.getValue());
- }
- }
- header = response.getFirstHeader("ETag");
- if (header != null) {
- state.mHeaderETag = header.getValue();
- }
- String headerTransferEncoding = null;
- header = response.getFirstHeader("Transfer-Encoding");
- if (header != null) {
- headerTransferEncoding = header.getValue();
- }
- if (headerTransferEncoding == null) {
- header = response.getFirstHeader("Content-Length");
- if (header != null) {
- innerState.mHeaderContentLength = header.getValue();
- state.mTotalBytes = mInfo.mTotalBytes =
- Long.parseLong(innerState.mHeaderContentLength);
- }
- } else {
- // Ignore content-length with transfer-encoding - 2616 4.4 3
- if (Constants.LOGVV) {
- Log.v(Constants.TAG,
- "ignoring content-length because of xfer-encoding");
- }
- }
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Content-Disposition: " +
- innerState.mHeaderContentDisposition);
- Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
- Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
- Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
- Log.v(Constants.TAG, "ETag: " + state.mHeaderETag);
- Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
- }
-
- boolean noSizeInfo = innerState.mHeaderContentLength == null
- && (headerTransferEncoding == null
- || !headerTransferEncoding.equalsIgnoreCase("chunked"));
- if (!mInfo.mNoIntegrity && noSizeInfo) {
- throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
- "can't know size of download, giving up");
- }
- }
+ state.mContentDisposition = conn.getHeaderField("Content-Disposition");
+ state.mContentLocation = conn.getHeaderField("Content-Location");
- /**
- * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
- */
- private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
- throws StopRequestException, RetryDownload {
- int statusCode = response.getStatusLine().getStatusCode();
- if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
- handleServiceUnavailable(state, response);
- }
- if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
- handleRedirect(state, response, statusCode);
+ if (state.mMimeType == null) {
+ state.mMimeType = Intent.normalizeMimeType(conn.getContentType());
}
- if (Constants.LOGV) {
- Log.i(Constants.TAG, "recevd_status = " + statusCode +
- ", mContinuingDownload = " + state.mContinuingDownload);
- }
- int expectedStatus = state.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
- if (statusCode != expectedStatus) {
- handleOtherStatus(state, innerState, statusCode);
- }
- }
+ state.mHeaderETag = conn.getHeaderField("ETag");
- /**
- * Handle a status that we don't know how to deal with properly.
- */
- private void handleOtherStatus(State state, InnerState innerState, int statusCode)
- throws StopRequestException {
- if (statusCode == 416) {
- // range request failed. it should never fail.
- throw new IllegalStateException("Http Range request failure: totalBytes = " +
- state.mTotalBytes + ", bytes recvd so far: " + state.mCurrentBytes);
- }
- int finalStatus;
- if (Downloads.Impl.isStatusError(statusCode)) {
- finalStatus = statusCode;
- } else if (statusCode >= 300 && statusCode < 400) {
- finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
- } else if (state.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
- finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
+ final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
+ if (transferEncoding == null) {
+ state.mContentLength = getHeaderFieldLong(conn, "Content-Length", -1);
} else {
- finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
+ Log.i(TAG, "Ignoring Content-Length since Transfer-Encoding is also defined");
+ state.mContentLength = -1;
}
- throw new StopRequestException(finalStatus, "http error " +
- statusCode + ", mContinuingDownload: " + state.mContinuingDownload);
- }
- /**
- * Handle a 3xx redirect status.
- */
- private void handleRedirect(State state, HttpResponse response, int statusCode)
- throws StopRequestException, RetryDownload {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
- }
- if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
- throw new StopRequestException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS,
- "too many redirects");
- }
- Header header = response.getFirstHeader("Location");
- if (header == null) {
- return;
- }
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Location :" + header.getValue());
- }
-
- String newUri;
- try {
- newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
- } catch(URISyntaxException ex) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
- + " for " + mInfo.mUri);
- }
- throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
- "Couldn't resolve redirect URI");
- }
- ++state.mRedirectCount;
- state.mRequestUri = newUri;
- if (statusCode == 301 || statusCode == 303) {
- // use the new URI for all future requests (should a retry/resume be necessary)
- state.mNewUri = newUri;
- }
- throw new RetryDownload();
- }
+ state.mTotalBytes = state.mContentLength;
+ mInfo.mTotalBytes = state.mContentLength;
- /**
- * Handle a 503 Service Unavailable status by processing the Retry-After header.
- */
- private void handleServiceUnavailable(State state, HttpResponse response)
- throws StopRequestException {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "got HTTP response code 503");
- }
- state.mCountRetry = true;
- Header header = response.getFirstHeader("Retry-After");
- if (header != null) {
- try {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Retry-After :" + header.getValue());
- }
- state.mRetryAfter = Integer.parseInt(header.getValue());
- if (state.mRetryAfter < 0) {
- state.mRetryAfter = 0;
- } else {
- if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
- state.mRetryAfter = Constants.MIN_RETRY_AFTER;
- } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
- state.mRetryAfter = Constants.MAX_RETRY_AFTER;
- }
- state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
- state.mRetryAfter *= 1000;
- }
- } catch (NumberFormatException ex) {
- // ignored - retryAfter stays 0 in this case.
- }
- }
- throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
- "got 503 Service Unavailable, will retry later");
- }
-
- /**
- * Send the request to the server, handling any I/O exceptions.
- */
- private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
- throws StopRequestException {
- try {
- return client.execute(request);
- } catch (IllegalArgumentException ex) {
- throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
- "while trying to execute request: " + ex.toString(), ex);
- } catch (IOException ex) {
- logNetworkState(mInfo.mUid);
- throw new StopRequestException(getFinalStatusForHttpError(state),
- "while trying to execute request: " + ex.toString(), ex);
+ final boolean noSizeInfo = state.mContentLength == -1
+ && (transferEncoding == null || !transferEncoding.equalsIgnoreCase("chunked"));
+ if (!mInfo.mNoIntegrity && noSizeInfo) {
+ throw new StopRequestException(STATUS_CANNOT_RESUME,
+ "can't know size of download, giving up");
}
}
- private int getFinalStatusForHttpError(State state) {
- int networkUsable = mInfo.checkCanUseNetwork();
- if (networkUsable != DownloadInfo.NETWORK_OK) {
- switch (networkUsable) {
- case DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE:
- case DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
- return Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
- default:
- return Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
- }
- } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
- state.mCountRetry = true;
- return Downloads.Impl.STATUS_WAITING_TO_RETRY;
+ private void parseRetryAfterHeaders(State state, HttpURLConnection conn) {
+ state.mRetryAfter = conn.getHeaderFieldInt("Retry-After", -1);
+ if (state.mRetryAfter < 0) {
+ state.mRetryAfter = 0;
} else {
- Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
- return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
+ if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
+ state.mRetryAfter = Constants.MIN_RETRY_AFTER;
+ } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
+ state.mRetryAfter = Constants.MAX_RETRY_AFTER;
+ }
+ state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
+ state.mRetryAfter *= 1000;
}
}
@@ -865,8 +742,7 @@ public class DownloadThread extends Thread {
* Prepare the destination file to receive data. If the file already exists, we'll set up
* appropriately for resumption.
*/
- private void setupDestinationFile(State state, InnerState innerState)
- throws StopRequestException {
+ private void setupDestinationFile(State state) throws StopRequestException {
if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
if (Constants.LOGV) {
Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
@@ -913,15 +789,9 @@ public class DownloadThread extends Thread {
Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
", and starting with file of length: " + fileLength);
}
- try {
- state.mStream = new FileOutputStream(state.mFilename, true);
- } catch (FileNotFoundException exc) {
- throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
- "while opening destination for resuming: " + exc.toString(), exc);
- }
state.mCurrentBytes = (int) fileLength;
if (mInfo.mTotalBytes != -1) {
- innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
+ state.mContentLength = mInfo.mTotalBytes;
}
state.mHeaderETag = mInfo.mETag;
state.mContinuingDownload = true;
@@ -933,30 +803,30 @@ public class DownloadThread extends Thread {
}
}
}
-
- if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
- closeDestination(state);
- }
}
/**
* Add custom headers for this download to the HTTP request.
*/
- private void addRequestHeaders(State state, HttpGet request) {
+ private void addRequestHeaders(State state, HttpURLConnection conn) {
for (Pair<String, String> header : mInfo.getHeaders()) {
- request.addHeader(header.first, header.second);
+ conn.addRequestProperty(header.first, header.second);
}
+ // Only splice in user agent when not already defined
+ if (conn.getRequestProperty("User-Agent") == null) {
+ conn.addRequestProperty("User-Agent", userAgent());
+ }
+
+ // Defeat transparent gzip compression, since it doesn't allow us to
+ // easily resume partial downloads.
+ conn.setRequestProperty("Accept-Encoding", "identity");
+
if (state.mContinuingDownload) {
if (state.mHeaderETag != null) {
- request.addHeader("If-Match", state.mHeaderETag);
- }
- request.addHeader("Range", "bytes=" + state.mCurrentBytes + "-");
- if (Constants.LOGV) {
- Log.i(Constants.TAG, "Adding Range header: " +
- "bytes=" + state.mCurrentBytes + "-");
- Log.i(Constants.TAG, " totalBytes = " + state.mTotalBytes);
+ conn.addRequestProperty("If-Match", state.mHeaderETag);
}
+ conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
}
}
@@ -964,35 +834,27 @@ public class DownloadThread extends Thread {
* Stores information about the completed download, and notifies the initiating application.
*/
private void notifyDownloadCompleted(
- int status, boolean countRetry, int retryAfter, boolean gotData,
- String filename, String uri, String mimeType, String errorMsg) {
- notifyThroughDatabase(
- status, countRetry, retryAfter, gotData, filename, uri, mimeType,
- errorMsg);
- if (Downloads.Impl.isStatusCompleted(status)) {
+ State state, int finalStatus, String errorMsg, int numFailed) {
+ notifyThroughDatabase(state, finalStatus, errorMsg, numFailed);
+ if (Downloads.Impl.isStatusCompleted(finalStatus)) {
mInfo.sendIntentIfRequested();
}
}
private void notifyThroughDatabase(
- int status, boolean countRetry, int retryAfter, boolean gotData,
- String filename, String uri, String mimeType, String errorMsg) {
+ State state, int finalStatus, String errorMsg, int numFailed) {
ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_STATUS, status);
- values.put(Downloads.Impl._DATA, filename);
- if (uri != null) {
- values.put(Downloads.Impl.COLUMN_URI, uri);
- }
- values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
+ values.put(Downloads.Impl.COLUMN_STATUS, finalStatus);
+ values.put(Downloads.Impl._DATA, state.mFilename);
+ values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
- values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
- if (!countRetry) {
- values.put(Constants.FAILED_CONNECTIONS, 0);
- } else if (gotData) {
- values.put(Constants.FAILED_CONNECTIONS, 1);
- } else {
- values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
+ values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, numFailed);
+ values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, state.mRetryAfter);
+
+ if (!TextUtils.equals(mInfo.mUri, state.mRequestUri)) {
+ values.put(Downloads.Impl.COLUMN_URI, state.mRequestUri);
}
+
// save the error message. could be useful to developers.
if (!TextUtils.isEmpty(errorMsg)) {
values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
@@ -1021,4 +883,27 @@ public class DownloadThread extends Thread {
mPolicyDirty = true;
}
};
+
+ public static long getHeaderFieldLong(URLConnection conn, String field, long defaultValue) {
+ try {
+ return Long.parseLong(conn.getHeaderField(field));
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Return if given status is eligible to be treated as
+ * {@link android.provider.Downloads.Impl#STATUS_WAITING_TO_RETRY}.
+ */
+ public static boolean isStatusRetryable(int status) {
+ switch (status) {
+ case STATUS_HTTP_DATA_ERROR:
+ case HTTP_UNAVAILABLE:
+ case HTTP_INTERNAL_ERROR:
+ return true;
+ default:
+ return false;
+ }
+ }
}
diff --git a/src/com/android/providers/downloads/DrmConvertSession.java b/src/com/android/providers/downloads/DrmConvertSession.java
deleted file mode 100644
index d10edf14..00000000
--- a/src/com/android/providers/downloads/DrmConvertSession.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2011 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.content.Context;
-import android.drm.DrmConvertedStatus;
-import android.drm.DrmManagerClient;
-import android.util.Log;
-import android.provider.Downloads;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.RandomAccessFile;
-
-
-public class DrmConvertSession {
- private DrmManagerClient mDrmClient;
- private int mConvertSessionId;
-
- private DrmConvertSession(DrmManagerClient drmClient, int convertSessionId) {
- mDrmClient = drmClient;
- mConvertSessionId = convertSessionId;
- }
-
- /**
- * Start of converting a file.
- *
- * @param context The context of the application running the convert session.
- * @param mimeType Mimetype of content that shall be converted.
- * @return A convert session or null in case an error occurs.
- */
- public static DrmConvertSession open(Context context, String mimeType) {
- DrmManagerClient drmClient = null;
- int convertSessionId = -1;
- if (context != null && mimeType != null && !mimeType.equals("")) {
- try {
- drmClient = new DrmManagerClient(context);
- try {
- convertSessionId = drmClient.openConvertSession(mimeType);
- } catch (IllegalArgumentException e) {
- Log.w(Constants.TAG, "Conversion of Mimetype: " + mimeType
- + " is not supported.", e);
- } catch (IllegalStateException e) {
- Log.w(Constants.TAG, "Could not access Open DrmFramework.", e);
- }
- } catch (IllegalArgumentException e) {
- Log.w(Constants.TAG,
- "DrmManagerClient instance could not be created, context is Illegal.");
- } catch (IllegalStateException e) {
- Log.w(Constants.TAG, "DrmManagerClient didn't initialize properly.");
- }
- }
-
- if (drmClient == null || convertSessionId < 0) {
- return null;
- } else {
- return new DrmConvertSession(drmClient, convertSessionId);
- }
- }
- /**
- * Convert a buffer of data to protected format.
- *
- * @param buffer Buffer filled with data to convert.
- * @param size The number of bytes that shall be converted.
- * @return A Buffer filled with converted data, if execution is ok, in all
- * other case null.
- */
- public byte [] convert(byte[] inBuffer, int size) {
- byte[] result = null;
- if (inBuffer != null) {
- DrmConvertedStatus convertedStatus = null;
- try {
- if (size != inBuffer.length) {
- byte[] buf = new byte[size];
- System.arraycopy(inBuffer, 0, buf, 0, size);
- convertedStatus = mDrmClient.convertData(mConvertSessionId, buf);
- } else {
- convertedStatus = mDrmClient.convertData(mConvertSessionId, inBuffer);
- }
-
- if (convertedStatus != null &&
- convertedStatus.statusCode == DrmConvertedStatus.STATUS_OK &&
- convertedStatus.convertedData != null) {
- result = convertedStatus.convertedData;
- }
- } catch (IllegalArgumentException e) {
- Log.w(Constants.TAG, "Buffer with data to convert is illegal. Convertsession: "
- + mConvertSessionId, e);
- } catch (IllegalStateException e) {
- Log.w(Constants.TAG, "Could not convert data. Convertsession: " +
- mConvertSessionId, e);
- }
- } else {
- throw new IllegalArgumentException("Parameter inBuffer is null");
- }
- return result;
- }
-
- /**
- * Ends a conversion session of a file.
- *
- * @param fileName The filename of the converted file.
- * @return Downloads.Impl.STATUS_SUCCESS if execution is ok.
- * Downloads.Impl.STATUS_FILE_ERROR in case converted file can not
- * be accessed. Downloads.Impl.STATUS_NOT_ACCEPTABLE if a problem
- * occurs when accessing drm framework.
- * Downloads.Impl.STATUS_UNKNOWN_ERROR if a general error occurred.
- */
- public int close(String filename) {
- DrmConvertedStatus convertedStatus = null;
- int result = Downloads.Impl.STATUS_UNKNOWN_ERROR;
- if (mDrmClient != null && mConvertSessionId >= 0) {
- try {
- convertedStatus = mDrmClient.closeConvertSession(mConvertSessionId);
- if (convertedStatus == null ||
- convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
- convertedStatus.convertedData == null) {
- result = Downloads.Impl.STATUS_NOT_ACCEPTABLE;
- } else {
- RandomAccessFile rndAccessFile = null;
- try {
- rndAccessFile = new RandomAccessFile(filename, "rw");
- rndAccessFile.seek(convertedStatus.offset);
- rndAccessFile.write(convertedStatus.convertedData);
- result = Downloads.Impl.STATUS_SUCCESS;
- } catch (FileNotFoundException e) {
- result = Downloads.Impl.STATUS_FILE_ERROR;
- Log.w(Constants.TAG, "File: " + filename + " could not be found.", e);
- } catch (IOException e) {
- result = Downloads.Impl.STATUS_FILE_ERROR;
- Log.w(Constants.TAG, "Could not access File: " + filename + " .", e);
- } catch (IllegalArgumentException e) {
- result = Downloads.Impl.STATUS_FILE_ERROR;
- Log.w(Constants.TAG, "Could not open file in mode: rw", e);
- } catch (SecurityException e) {
- Log.w(Constants.TAG, "Access to File: " + filename +
- " was denied denied by SecurityManager.", e);
- } finally {
- if (rndAccessFile != null) {
- try {
- rndAccessFile.close();
- } catch (IOException e) {
- result = Downloads.Impl.STATUS_FILE_ERROR;
- Log.w(Constants.TAG, "Failed to close File:" + filename
- + ".", e);
- }
- }
- }
- }
- } catch (IllegalStateException e) {
- Log.w(Constants.TAG, "Could not close convertsession. Convertsession: " +
- mConvertSessionId, e);
- }
- }
- return result;
- }
-}
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 359f6fa4..33205557 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -17,10 +17,6 @@
package com.android.providers.downloads;
import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Environment;
import android.os.SystemClock;
@@ -29,6 +25,7 @@ import android.util.Log;
import android.webkit.MimeTypeMap;
import java.io.File;
+import java.io.IOException;
import java.util.Random;
import java.util.Set;
import java.util.regex.Matcher;
@@ -44,6 +41,8 @@ public class Helpers {
private static final Pattern CONTENT_DISPOSITION_PATTERN =
Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
+ private static final Object sUniqueLock = new Object();
+
private Helpers() {
}
@@ -77,8 +76,10 @@ public class Helpers {
String mimeType,
int destination,
long contentLength,
- boolean isPublicApi, StorageManager storageManager) throws StopRequestException {
- checkCanHandleDownload(context, mimeType, destination, isPublicApi);
+ StorageManager storageManager) throws StopRequestException {
+ if (contentLength < 0) {
+ contentLength = 0;
+ }
String path;
File base = null;
if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
@@ -90,10 +91,10 @@ public class Helpers {
destination);
}
storageManager.verifySpace(destination, path, contentLength);
- path = getFullPath(path, mimeType, destination, base);
if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path);
}
+ path = getFullPath(path, mimeType, destination, base);
return path;
}
@@ -130,47 +131,20 @@ public class Helpers {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "target file: " + filename + extension);
}
- return chooseUniqueFilename(destination, filename, extension, recoveryDir);
- }
- private static void checkCanHandleDownload(Context context, String mimeType, int destination,
- boolean isPublicApi) throws StopRequestException {
- if (isPublicApi) {
- return;
- }
+ synchronized (sUniqueLock) {
+ final String path = chooseUniqueFilenameLocked(
+ destination, filename, extension, recoveryDir);
- if (destination == Downloads.Impl.DESTINATION_EXTERNAL
- || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
- if (mimeType == null) {
- throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
- "external download with no mime type not allowed");
- }
- if (!DownloadDrmHelper.isDrmMimeType(context, mimeType)) {
- // Check to see if we are allowed to download this file. Only files
- // that can be handled by the platform can be downloaded.
- // special case DRM files, which we should always allow downloading.
- Intent intent = new Intent(Intent.ACTION_VIEW);
-
- // We can provide data as either content: or file: URIs,
- // so allow both. (I think it would be nice if we just did
- // everything as content: URIs)
- // Actually, right now the download manager's UId restrictions
- // prevent use from using content: so it's got to be file: or
- // nothing
-
- PackageManager pm = context.getPackageManager();
- intent.setDataAndType(Uri.fromParts("file", "", null), mimeType);
- ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
- //Log.i(Constants.TAG, "*** FILENAME QUERY " + intent + ": " + list);
-
- if (ri == null) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "no handler found for type " + mimeType);
- }
- throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
- "no handler found for this download type");
- }
+ // Claim this filename inside lock to prevent other threads from
+ // clobbering us. We're not paranoid enough to use O_EXCL.
+ try {
+ new File(path).createNewFile();
+ } catch (IOException e) {
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
+ "Failed to create target file " + path, e);
}
+ return path;
}
}
@@ -321,7 +295,7 @@ public class Helpers {
return extension;
}
- private static String chooseUniqueFilename(int destination, String filename,
+ private static String chooseUniqueFilenameLocked(int destination, String filename,
String extension, boolean recoveryDir) throws StopRequestException {
String fullFilename = filename + extension;
if (!new File(fullFilename).exists()
@@ -365,14 +339,6 @@ public class Helpers {
}
/**
- * Returns whether the network is available
- */
- public static boolean isNetworkAvailable(SystemFacade system, int uid) {
- final NetworkInfo info = system.getActiveNetworkInfo(uid);
- return info != null && info.isConnected();
- }
-
- /**
* Checks whether the filename looks legitimate
*/
static boolean isFilenameValid(String filename, File downloadsDataDir) {
diff --git a/src/com/android/providers/downloads/OpenHelper.java b/src/com/android/providers/downloads/OpenHelper.java
index 5a92316b..184cdb3d 100644
--- a/src/com/android/providers/downloads/OpenHelper.java
+++ b/src/com/android/providers/downloads/OpenHelper.java
@@ -31,6 +31,8 @@ import android.database.Cursor;
import android.net.Uri;
import android.provider.Downloads.Impl.RequestHeaders;
+import java.io.File;
+
public class OpenHelper {
/**
* Build an {@link Intent} to view the download at current {@link Cursor}
@@ -48,9 +50,9 @@ public class OpenHelper {
}
final Uri localUri = getCursorUri(cursor, COLUMN_LOCAL_URI);
- final String filename = getCursorString(cursor, COLUMN_LOCAL_FILENAME);
+ final File file = getCursorFile(cursor, COLUMN_LOCAL_FILENAME);
String mimeType = getCursorString(cursor, COLUMN_MEDIA_TYPE);
- mimeType = DownloadDrmHelper.getOriginalMimeType(context, filename, mimeType);
+ mimeType = DownloadDrmHelper.getOriginalMimeType(context, file, mimeType);
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -131,4 +133,8 @@ public class OpenHelper {
private static long getCursorLong(Cursor cursor, String column) {
return cursor.getLong(cursor.getColumnIndexOrThrow(column));
}
+
+ private static File getCursorFile(Cursor cursor, String column) {
+ return new File(cursor.getString(cursor.getColumnIndexOrThrow(column)));
+ }
}
diff --git a/src/com/android/providers/downloads/RealSystemFacade.java b/src/com/android/providers/downloads/RealSystemFacade.java
index 228c7165..fa4f3488 100644
--- a/src/com/android/providers/downloads/RealSystemFacade.java
+++ b/src/com/android/providers/downloads/RealSystemFacade.java
@@ -32,10 +32,12 @@ class RealSystemFacade implements SystemFacade {
mContext = context;
}
+ @Override
public long currentTimeMillis() {
return System.currentTimeMillis();
}
+ @Override
public NetworkInfo getActiveNetworkInfo(int uid) {
ConnectivityManager connectivity =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -57,6 +59,7 @@ class RealSystemFacade implements SystemFacade {
return conn.isActiveNetworkMetered();
}
+ @Override
public boolean isNetworkRoaming() {
ConnectivityManager connectivity =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -74,6 +77,7 @@ class RealSystemFacade implements SystemFacade {
return isRoaming;
}
+ @Override
public Long getMaxBytesOverMobile() {
return DownloadManager.getMaxBytesOverMobile(mContext);
}
@@ -92,9 +96,4 @@ class RealSystemFacade implements SystemFacade {
public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
}
-
- @Override
- public void startThread(Thread thread) {
- thread.start();
- }
}
diff --git a/src/com/android/providers/downloads/StopRequestException.java b/src/com/android/providers/downloads/StopRequestException.java
index 0ccf53cb..a2b642d8 100644
--- a/src/com/android/providers/downloads/StopRequestException.java
+++ b/src/com/android/providers/downloads/StopRequestException.java
@@ -15,6 +15,9 @@
*/
package com.android.providers.downloads;
+import static android.provider.Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
+import static android.provider.Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
+
/**
* Raised to indicate that the current request should be stopped immediately.
*
@@ -23,15 +26,36 @@ package com.android.providers.downloads;
* URI, headers, or destination filename.
*/
class StopRequestException extends Exception {
- public int mFinalStatus;
+ private final int mFinalStatus;
public StopRequestException(int finalStatus, String message) {
super(message);
mFinalStatus = finalStatus;
}
- public StopRequestException(int finalStatus, String message, Throwable throwable) {
- super(message, throwable);
+ public StopRequestException(int finalStatus, Throwable t) {
+ super(t);
+ mFinalStatus = finalStatus;
+ }
+
+ public StopRequestException(int finalStatus, String message, Throwable t) {
+ super(message, t);
mFinalStatus = finalStatus;
}
+
+ public int getFinalStatus() {
+ return mFinalStatus;
+ }
+
+ public static StopRequestException throwUnhandledHttpError(int code, String message)
+ throws StopRequestException {
+ final String error = "Unhandled HTTP response: " + code + " " + message;
+ if (code >= 400 && code < 600) {
+ throw new StopRequestException(code, error);
+ } else if (code >= 300 && code < 400) {
+ throw new StopRequestException(STATUS_UNHANDLED_REDIRECT, error);
+ } else {
+ throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, error);
+ }
+ }
}
diff --git a/src/com/android/providers/downloads/StorageManager.java b/src/com/android/providers/downloads/StorageManager.java
index 915d141b..deb412e7 100644
--- a/src/com/android/providers/downloads/StorageManager.java
+++ b/src/com/android/providers/downloads/StorageManager.java
@@ -71,12 +71,6 @@ class StorageManager {
*/
private final File mDownloadDataDir;
- /** the Singleton instance of this class.
- * TODO: once DownloadService is refactored into a long-living object, there is no need
- * for this Singleton'ing.
- */
- private static StorageManager sSingleton = null;
-
/** how often do we need to perform checks on space to make sure space is available */
private static final int FREQUENCY_OF_CHECKS_ON_SPACE_AVAILABILITY = 1024 * 1024; // 1MB
private int mBytesDownloadedSinceLastCheckOnSpace = 0;
@@ -84,19 +78,9 @@ class StorageManager {
/** misc members */
private final Context mContext;
- /**
- * maintains Singleton instance of this class
- */
- synchronized static StorageManager getInstance(Context context) {
- if (sSingleton == null) {
- sSingleton = new StorageManager(context);
- }
- return sSingleton;
- }
-
- private StorageManager(Context context) { // constructor is private
+ public StorageManager(Context context) {
mContext = context;
- mDownloadDataDir = context.getCacheDir();
+ mDownloadDataDir = getDownloadDataDirectory(context);
mExternalStorageDir = Environment.getExternalStorageDirectory();
mSystemCacheDir = Environment.getDownloadCacheDirectory();
startThreadToCleanupDatabaseAndPurgeFileSystem();
@@ -308,6 +292,10 @@ class StorageManager {
return mDownloadDataDir;
}
+ public static File getDownloadDataDirectory(Context context) {
+ return context.getCacheDir();
+ }
+
/**
* Deletes purgeable files from the cache partition. This also deletes
* the matching database entries. Files are deleted in LRU order until
@@ -370,7 +358,7 @@ class StorageManager {
* This is not a very common occurrence. So, do this only once in a while.
*/
private void removeSpuriousFiles() {
- if (true || Constants.LOGV) {
+ if (Constants.LOGV) {
Log.i(Constants.TAG, "in removeSpuriousFiles");
}
// get a list of all files in system cache dir and downloads data dir
diff --git a/src/com/android/providers/downloads/SystemFacade.java b/src/com/android/providers/downloads/SystemFacade.java
index fda97e08..15fc31f9 100644
--- a/src/com/android/providers/downloads/SystemFacade.java
+++ b/src/com/android/providers/downloads/SystemFacade.java
@@ -61,9 +61,4 @@ interface SystemFacade {
* Returns true if the specified UID owns the specified package name.
*/
public boolean userOwnsPackage(int uid, String pckg) throws NameNotFoundException;
-
- /**
- * Start a thread.
- */
- public void startThread(Thread thread);
}