summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml4
-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
-rw-r--r--tests/Android.mk2
-rw-r--r--tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java25
-rw-r--r--tests/src/com/android/providers/downloads/AbstractPublicApiTest.java81
-rw-r--r--tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java32
-rw-r--r--tests/src/com/android/providers/downloads/FakeInputStream.java58
-rw-r--r--tests/src/com/android/providers/downloads/FakeSystemFacade.java46
-rw-r--r--tests/src/com/android/providers/downloads/MockitoHelper.java53
-rw-r--r--tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java172
-rw-r--r--tests/src/com/android/providers/downloads/ThreadingTest.java57
-rw-r--r--ui/AndroidManifest.xml6
-rw-r--r--ui/res/layout/download_list.xml8
-rw-r--r--ui/res/layout/download_list_item.xml24
-rw-r--r--ui/res/layout/list_group_header.xml13
-rw-r--r--ui/res/menu/download_menu.xml2
-rw-r--r--ui/res/values-ca/strings.xml2
-rw-r--r--ui/res/values-es/strings.xml2
-rw-r--r--ui/res/values-fa/strings.xml2
-rw-r--r--ui/res/values-hi/strings.xml2
-rw-r--r--ui/res/values/dimen.xml2
-rw-r--r--ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java2
-rw-r--r--ui/src/com/android/providers/downloads/ui/DownloadItem.java15
-rw-r--r--ui/src/com/android/providers/downloads/ui/DownloadList.java4
39 files changed, 1551 insertions, 1565 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7a1ce39..3024a17 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -57,6 +57,7 @@
<application android:process="android.process.media"
android:label="@string/app_label">
+
<provider android:name=".DownloadProvider"
android:authorities="downloads" android:exported="true">
<!-- Anyone can access /my_downloads, the provider internally restricts access by UID for
@@ -72,6 +73,9 @@
<!-- Apps with access to /all_downloads/... can grant permissions, allowing them to share
downloaded files with other viewers -->
<grant-uri-permission android:pathPrefix="/all_downloads/"/>
+ <!-- Apps with access to /my_downloads/... can grant permissions, allowing them to share
+ downloaded files with other viewers -->
+ <grant-uri-permission android:pathPrefix="/my_downloads/"/>
</provider>
<service android:name=".DownloadService"
android:permission="android.permission.ACCESS_DOWNLOAD_MANAGER" />
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
index 8d80618..08ef466 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 10cb792..d135824 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 2f02864..0000000
--- 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 5172b69..7a912d5 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 f387865..ac52eba 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 c554e41..e0b5842 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 0000000..ca79506
--- /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 b97346b..7d746cc 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 34bc8e3..6a0eb47 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 d10edf1..0000000
--- 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 359f6fa..3320555 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 5a92316..184cdb3 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 228c716..fa4f348 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 0ccf53c..a2b642d 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 915d141..deb412e 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 fda97e0..15fc31f 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);
}
diff --git a/tests/Android.mk b/tests/Android.mk
index ff3e1d4..655ec16 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -8,7 +8,7 @@ LOCAL_MODULE_TAGS := tests
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_INSTRUMENTATION_FOR := DownloadProvider
LOCAL_JAVA_LIBRARIES := android.test.runner
-LOCAL_STATIC_JAVA_LIBRARIES := mockwebserver littlemock dexmaker
+LOCAL_STATIC_JAVA_LIBRARIES := mockwebserver dexmaker mockito-target
LOCAL_PACKAGE_NAME := DownloadProviderTests
LOCAL_CERTIFICATE := media
diff --git a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
index a65693f..e59aff0 100644
--- a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
@@ -16,7 +16,7 @@
package com.android.providers.downloads;
-import static com.google.testing.littlemock.LittleMock.mock;
+import static org.mockito.Mockito.mock;
import android.app.NotificationManager;
import android.content.ComponentName;
@@ -34,6 +34,7 @@ import android.test.mock.MockContentResolver;
import android.util.Log;
import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockStreamResponse;
import com.google.mockwebserver.MockWebServer;
import com.google.mockwebserver.RecordedRequest;
import com.google.mockwebserver.SocketPolicy;
@@ -52,11 +53,11 @@ public abstract class AbstractDownloadProviderFunctionalTest extends
protected static final String LOG_TAG = "DownloadProviderFunctionalTest";
private static final String PROVIDER_AUTHORITY = "downloads";
protected static final long RETRY_DELAY_MILLIS = 61 * 1000;
- protected static final String FILE_CONTENT = "hello world hello world hello world hello world";
- protected static final int HTTP_OK = 200;
- protected static final int HTTP_PARTIAL_CONTENT = 206;
- protected static final int HTTP_NOT_FOUND = 404;
- protected static final int HTTP_SERVICE_UNAVAILABLE = 503;
+
+ protected static final String
+ FILE_CONTENT = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ private final MockitoHelper mMockitoHelper = new MockitoHelper();
protected MockWebServer mServer;
protected MockContentResolverWithNotify mResolver;
@@ -149,6 +150,7 @@ public abstract class AbstractDownloadProviderFunctionalTest extends
@Override
protected void setUp() throws Exception {
super.setUp();
+ mMockitoHelper.setUp(getClass());
// Since we're testing a system app, AppDataDirGuesser doesn't find our
// cache dir, so set it explicitly.
@@ -161,6 +163,7 @@ public abstract class AbstractDownloadProviderFunctionalTest extends
setContext(mTestContext);
setupService();
getService().mSystemFacade = mSystemFacade;
+ mSystemFacade.setUp();
assertTrue(isDatabaseEmpty()); // ensure we're not messing with real data
mServer = new MockWebServer();
mServer.play();
@@ -170,6 +173,7 @@ public abstract class AbstractDownloadProviderFunctionalTest extends
protected void tearDown() throws Exception {
cleanUpDownloads();
mServer.shutdown();
+ mMockitoHelper.tearDown();
super.tearDown();
}
@@ -217,6 +221,10 @@ public abstract class AbstractDownloadProviderFunctionalTest extends
mServer.enqueue(resp);
}
+ void enqueueResponse(MockStreamResponse resp) {
+ mServer.enqueue(resp);
+ }
+
MockResponse buildResponse(int status, String body) {
return new MockResponse().setResponseCode(status).setBody(body)
.setHeader("Content-type", "text/plain")
@@ -246,11 +254,6 @@ public abstract class AbstractDownloadProviderFunctionalTest extends
return mServer.getUrl(path).toString();
}
- public void runService() throws Exception {
- startService(null);
- mSystemFacade.runAllThreads();
- }
-
protected String readStream(InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
try {
diff --git a/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java b/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
index cda607a..348dbd1 100644
--- a/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
@@ -16,17 +16,22 @@
package com.android.providers.downloads;
+import static android.app.DownloadManager.STATUS_FAILED;
+import static android.app.DownloadManager.STATUS_SUCCESSFUL;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
import android.app.DownloadManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
-import android.provider.Downloads;
+import android.os.SystemClock;
import android.util.Log;
-import java.io.FileInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
+import java.util.concurrent.TimeoutException;
/**
* Code common to tests that use the download manager public API.
@@ -44,6 +49,10 @@ public abstract class AbstractPublicApiTest extends AbstractDownloadProviderFunc
return (int) getLongField(DownloadManager.COLUMN_STATUS);
}
+ public int getReason() {
+ return (int) getLongField(DownloadManager.COLUMN_REASON);
+ }
+
public int getStatusIfExists() {
Cursor cursor = mManager.query(new DownloadManager.Query().setFilterById(mId));
try {
@@ -86,7 +95,8 @@ public abstract class AbstractPublicApiTest extends AbstractDownloadProviderFunc
ParcelFileDescriptor downloadedFile = mManager.openDownloadedFile(mId);
assertTrue("Invalid file descriptor: " + downloadedFile,
downloadedFile.getFileDescriptor().valid());
- InputStream stream = new FileInputStream(downloadedFile.getFileDescriptor());
+ final InputStream stream = new ParcelFileDescriptor.AutoCloseInputStream(
+ downloadedFile);
try {
return readStream(stream);
} finally {
@@ -94,9 +104,52 @@ public abstract class AbstractPublicApiTest extends AbstractDownloadProviderFunc
}
}
- void runUntilStatus(int status) throws Exception {
- runService();
- assertEquals(status, getStatus());
+ void runUntilStatus(int status) throws TimeoutException {
+ final long startMillis = mSystemFacade.currentTimeMillis();
+ startService(null);
+ waitForStatus(status, startMillis);
+ }
+
+ void runUntilStatus(int status, long timeout) throws TimeoutException {
+ final long startMillis = mSystemFacade.currentTimeMillis();
+ startService(null);
+ waitForStatus(status, startMillis, timeout);
+ }
+
+ void waitForStatus(int expected, long afterMillis) throws TimeoutException {
+ waitForStatus(expected, afterMillis, 15 * SECOND_IN_MILLIS);
+ }
+
+ void waitForStatus(int expected, long afterMillis, long timeout) throws TimeoutException {
+ int actual = -1;
+
+ final long elapsedTimeout = SystemClock.elapsedRealtime() + timeout;
+ while (SystemClock.elapsedRealtime() < elapsedTimeout) {
+ if (getLongField(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP) >= afterMillis) {
+ actual = getStatus();
+ if (actual == STATUS_SUCCESSFUL || actual == STATUS_FAILED) {
+ assertEquals(expected, actual);
+ return;
+ } else if (actual == expected) {
+ return;
+ }
+
+ if (timeout > MINUTE_IN_MILLIS) {
+ final int percent = (int) (100
+ * getLongField(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
+ / getLongField(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
+ Log.d(LOG_TAG, percent + "% complete");
+ }
+ }
+
+ if (timeout > MINUTE_IN_MILLIS) {
+ SystemClock.sleep(SECOND_IN_MILLIS * 3);
+ } else {
+ SystemClock.sleep(100);
+ }
+ }
+
+ throw new TimeoutException("Expected status " + expected + "; only reached " + actual);
}
// max time to wait before giving up on the current download operation.
@@ -105,22 +158,10 @@ public abstract class AbstractPublicApiTest extends AbstractDownloadProviderFunc
// download thread
private static final int TIME_TO_SLEEP = 1000;
- int runUntilDone() throws InterruptedException {
- int sleepCounter = MAX_TIME_TO_WAIT_FOR_OPERATION * 1000 / TIME_TO_SLEEP;
- for (int i = 0; i < sleepCounter; i++) {
- int status = getStatusIfExists();
- if (status == -1 || Downloads.Impl.isStatusCompleted(getStatus())) {
- // row doesn't exist or the download is done
- return status;
- }
- // download not done yet. sleep a while and try again
- Thread.sleep(TIME_TO_SLEEP);
- }
- return 0; // failed
- }
-
// waits until progress_so_far is >= (progress)%
boolean runUntilProgress(int progress) throws InterruptedException {
+ startService(null);
+
int sleepCounter = MAX_TIME_TO_WAIT_FOR_OPERATION * 1000 / TIME_TO_SLEEP;
int numBytesReceivedSoFar = 0;
int totalBytes = 0;
diff --git a/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java b/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
index 23d300f..dbab203 100644
--- a/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/DownloadProviderFunctionalTest.java
@@ -16,14 +16,17 @@
package com.android.providers.downloads;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+import static java.net.HttpURLConnection.HTTP_OK;
+
import android.content.ContentValues;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Environment;
+import android.os.SystemClock;
import android.provider.Downloads;
import android.test.suitebuilder.annotation.LargeTest;
-import android.util.Log;
import com.google.mockwebserver.MockWebServer;
import com.google.mockwebserver.RecordedRequest;
@@ -31,6 +34,7 @@ import com.google.mockwebserver.RecordedRequest;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
+import java.util.concurrent.TimeoutException;
/**
* This test exercises the entire download manager working together -- it requests downloads through
@@ -109,20 +113,22 @@ public class DownloadProviderFunctionalTest extends AbstractDownloadProviderFunc
}
}
- private void runUntilStatus(Uri downloadUri, int status) throws Exception {
- runService();
- boolean done = false;
- while (!done) {
- int rslt = getDownloadStatus(downloadUri);
- if (rslt == Downloads.Impl.STATUS_RUNNING || rslt == Downloads.Impl.STATUS_PENDING) {
- Log.i(TAG, "status is: " + rslt + ", for: " + downloadUri);
- DownloadHandler.getInstance().waitUntilDownloadsTerminate();
- Thread.sleep(100);
- } else {
- done = true;
+ private void runUntilStatus(Uri downloadUri, int expected) throws Exception {
+ startService(null);
+
+ int actual = -1;
+
+ final long timeout = SystemClock.elapsedRealtime() + (15 * SECOND_IN_MILLIS);
+ while (SystemClock.elapsedRealtime() < timeout) {
+ actual = getDownloadStatus(downloadUri);
+ if (expected == actual) {
+ return;
}
+
+ SystemClock.sleep(100);
}
- assertEquals(status, getDownloadStatus(downloadUri));
+
+ throw new TimeoutException("Expected status " + expected + "; only reached " + actual);
}
protected int getDownloadStatus(Uri downloadUri) {
diff --git a/tests/src/com/android/providers/downloads/FakeInputStream.java b/tests/src/com/android/providers/downloads/FakeInputStream.java
new file mode 100644
index 0000000..179ae6e
--- /dev/null
+++ b/tests/src/com/android/providers/downloads/FakeInputStream.java
@@ -0,0 +1,58 @@
+/*
+ * 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 java.io.InputStream;
+import java.util.Arrays;
+
+/**
+ * Provides fake data for large transfers.
+ */
+public class FakeInputStream extends InputStream {
+ private long mRemaining;
+
+ public FakeInputStream(long length) {
+ mRemaining = length;
+ }
+
+ @Override
+ public int read() {
+ final int value;
+ if (mRemaining > 0) {
+ mRemaining--;
+ return 0;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) {
+ Arrays.checkOffsetAndCount(buffer.length, offset, length);
+
+ if (length > mRemaining) {
+ length = (int) mRemaining;
+ }
+ mRemaining -= length;
+
+ if (length == 0) {
+ return -1;
+ } else {
+ return length;
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/downloads/FakeSystemFacade.java b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
index 481b5cb..d54c122 100644
--- a/tests/src/com/android/providers/downloads/FakeSystemFacade.java
+++ b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
@@ -7,10 +7,7 @@ import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
import java.util.ArrayList;
-import java.util.LinkedList;
import java.util.List;
-import java.util.Queue;
-
public class FakeSystemFacade implements SystemFacade {
long mTimeMillis = 0;
Integer mActiveNetworkType = ConnectivityManager.TYPE_WIFI;
@@ -19,20 +16,32 @@ public class FakeSystemFacade implements SystemFacade {
Long mMaxBytesOverMobile = null;
Long mRecommendedMaxBytesOverMobile = null;
List<Intent> mBroadcastsSent = new ArrayList<Intent>();
- Queue<Thread> mStartedThreads = new LinkedList<Thread>();
- private boolean returnActualTime = false;
+ private boolean mReturnActualTime = false;
+
+ public void setUp() {
+ mTimeMillis = 0;
+ mActiveNetworkType = ConnectivityManager.TYPE_WIFI;
+ mIsRoaming = false;
+ mIsMetered = false;
+ mMaxBytesOverMobile = null;
+ mRecommendedMaxBytesOverMobile = null;
+ mBroadcastsSent.clear();
+ mReturnActualTime = false;
+ }
void incrementTimeMillis(long delta) {
mTimeMillis += delta;
}
+ @Override
public long currentTimeMillis() {
- if (returnActualTime) {
+ if (mReturnActualTime) {
return System.currentTimeMillis();
}
return mTimeMillis;
}
+ @Override
public NetworkInfo getActiveNetworkInfo(int uid) {
if (mActiveNetworkType == null) {
return null;
@@ -48,14 +57,17 @@ public class FakeSystemFacade implements SystemFacade {
return mIsMetered;
}
+ @Override
public boolean isNetworkRoaming() {
return mIsRoaming;
}
+ @Override
public Long getMaxBytesOverMobile() {
return mMaxBytesOverMobile ;
}
+ @Override
public Long getRecommendedMaxBytesOverMobile() {
return mRecommendedMaxBytesOverMobile ;
}
@@ -70,27 +82,7 @@ public class FakeSystemFacade implements SystemFacade {
return true;
}
- public boolean startThreadsWithoutWaiting = false;
- public void setStartThreadsWithoutWaiting(boolean flag) {
- this.startThreadsWithoutWaiting = flag;
- }
-
- @Override
- public void startThread(Thread thread) {
- if (startThreadsWithoutWaiting) {
- thread.start();
- } else {
- mStartedThreads.add(thread);
- }
- }
-
- public void runAllThreads() {
- while (!mStartedThreads.isEmpty()) {
- mStartedThreads.poll().run();
- }
- }
-
public void setReturnActualTime(boolean flag) {
- returnActualTime = flag;
+ mReturnActualTime = flag;
}
}
diff --git a/tests/src/com/android/providers/downloads/MockitoHelper.java b/tests/src/com/android/providers/downloads/MockitoHelper.java
new file mode 100644
index 0000000..485128d
--- /dev/null
+++ b/tests/src/com/android/providers/downloads/MockitoHelper.java
@@ -0,0 +1,53 @@
+/*
+ * 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 android.util.Log;
+
+/**
+ * Helper for Mockito-based test cases.
+ */
+public final class MockitoHelper {
+ private static final String TAG = "MockitoHelper";
+
+ private ClassLoader mOriginalClassLoader;
+ private Thread mContextThread;
+
+ /**
+ * Creates a new helper, which in turn will set the context classloader so
+ * it can load Mockito resources.
+ *
+ * @param packageClass test case class
+ */
+ public void setUp(Class<?> packageClass) throws Exception {
+ // makes a copy of the context classloader
+ mContextThread = Thread.currentThread();
+ mOriginalClassLoader = mContextThread.getContextClassLoader();
+ ClassLoader newClassLoader = packageClass.getClassLoader();
+ Log.v(TAG, "Changing context classloader from " + mOriginalClassLoader
+ + " to " + newClassLoader);
+ mContextThread.setContextClassLoader(newClassLoader);
+ }
+
+ /**
+ * Restores the context classloader to the previous value.
+ */
+ public void tearDown() throws Exception {
+ Log.v(TAG, "Restoring context classloader to " + mOriginalClassLoader);
+ mContextThread.setContextClassLoader(mOriginalClassLoader);
+ }
+}
diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
index 2661a1f..b6fd611 100644
--- a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
@@ -16,13 +16,23 @@
package com.android.providers.downloads;
-import static com.google.testing.littlemock.LittleMock.anyInt;
-import static com.google.testing.littlemock.LittleMock.anyString;
-import static com.google.testing.littlemock.LittleMock.atLeastOnce;
-import static com.google.testing.littlemock.LittleMock.isA;
-import static com.google.testing.littlemock.LittleMock.never;
-import static com.google.testing.littlemock.LittleMock.times;
-import static com.google.testing.littlemock.LittleMock.verify;
+import static android.app.DownloadManager.STATUS_FAILED;
+import static android.app.DownloadManager.STATUS_PAUSED;
+import static android.net.TrafficStats.GB_IN_BYTES;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_PARTIAL;
+import static java.net.HttpURLConnection.HTTP_PRECON_FAILED;
+import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import android.app.DownloadManager;
import android.app.Notification;
@@ -33,20 +43,24 @@ import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Environment;
+import android.os.SystemClock;
import android.provider.Downloads;
import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.text.format.DateUtils;
import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockStreamResponse;
import com.google.mockwebserver.RecordedRequest;
+import com.google.mockwebserver.SocketPolicy;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.net.MalformedURLException;
import java.util.List;
-
+import java.util.concurrent.TimeoutException;
@LargeTest
public class PublicApiFunctionalTest extends AbstractPublicApiTest {
@@ -76,7 +90,6 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
} else {
mTestDirectory.mkdir();
}
- mSystemFacade.setStartThreadsWithoutWaiting(false);
}
@Override
@@ -122,6 +135,24 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
checkCompleteDownload(download);
}
+ @Suppress
+ public void testExtremelyLarge() throws Exception {
+ // NOTE: suppressed since this takes several minutes to run
+ final long length = 3 * GB_IN_BYTES;
+ final InputStream body = new FakeInputStream(length);
+
+ enqueueResponse(new MockStreamResponse().setResponseCode(HTTP_OK).setBody(body, length)
+ .setHeader("Content-type", "text/plain")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+
+ final Download download = enqueueRequest(getRequest()
+ .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "extreme.bin"));
+ download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL, 10 * DateUtils.MINUTE_IN_MILLIS);
+
+ assertEquals(length, download.getLongField(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
+ assertEquals(length, download.getLongField(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
+ }
+
private void checkUriContent(Uri uri) throws FileNotFoundException, IOException {
InputStream inputStream = mResolver.openInputStream(uri);
try {
@@ -191,7 +222,7 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
private MockResponse buildPartialResponse(int start, int end) {
int totalLength = FILE_CONTENT.length();
boolean isFirstResponse = (start == 0);
- int status = isFirstResponse ? HTTP_OK : HTTP_PARTIAL_CONTENT;
+ int status = isFirstResponse ? HTTP_OK : HTTP_PARTIAL;
MockResponse response = buildResponse(status, FILE_CONTENT.substring(start, end))
.setHeader("Content-length", totalLength)
.setHeader("Etag", ETAG);
@@ -385,11 +416,72 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
assertEquals(REQUEST_PATH, lastRequest.getPath());
}
+ public void testRunawayRedirect() throws Exception {
+ for (int i = 0; i < 16; i++) {
+ enqueueResponse(buildEmptyResponse(HTTP_MOVED_TEMP)
+ .setHeader("Location", mServer.getUrl("/" + i).toString()));
+ }
+
+ final Download download = enqueueRequest(getRequest());
+
+ // Ensure that we arrive at failed download, instead of spinning forever
+ download.runUntilStatus(DownloadManager.STATUS_FAILED);
+ assertEquals(DownloadManager.ERROR_TOO_MANY_REDIRECTS, download.getReason());
+ }
+
+ public void testRunawayUnavailable() throws Exception {
+ final int RETRY_DELAY = 120;
+ for (int i = 0; i < 16; i++) {
+ enqueueResponse(
+ buildEmptyResponse(HTTP_UNAVAILABLE).setHeader("Retry-after", RETRY_DELAY));
+ }
+
+ final Download download = enqueueRequest(getRequest());
+ for (int i = 0; i < Constants.MAX_RETRIES - 1; i++) {
+ download.runUntilStatus(DownloadManager.STATUS_PAUSED);
+ mSystemFacade.incrementTimeMillis((RETRY_DELAY + 60) * SECOND_IN_MILLIS);
+ }
+
+ // Ensure that we arrive at failed download, instead of spinning forever
+ download.runUntilStatus(DownloadManager.STATUS_FAILED);
+ }
+
public void testNoEtag() throws Exception {
enqueueResponse(buildPartialResponse(0, 5).removeHeader("Etag"));
runSimpleFailureTest(DownloadManager.ERROR_CANNOT_RESUME);
}
+ public void testEtagChanged() throws Exception {
+ final String A = "kittenz";
+ final String B = "puppiez";
+
+ // 1. Try downloading A, but partial result
+ enqueueResponse(buildResponse(HTTP_OK, A.substring(0, 2))
+ .setHeader("Content-length", A.length())
+ .setHeader("Etag", A));
+
+ // 2. Try resuming A, but fail ETag check
+ enqueueResponse(buildEmptyResponse(HTTP_PRECON_FAILED));
+
+ final Download download = enqueueRequest(getRequest());
+ RecordedRequest req;
+
+ // 1. Try downloading A, but partial result
+ download.runUntilStatus(STATUS_PAUSED);
+ assertEquals(DownloadManager.PAUSED_WAITING_TO_RETRY, download.getReason());
+ req = takeRequest();
+ assertNull(getHeaderValue(req, "Range"));
+ assertNull(getHeaderValue(req, "If-Match"));
+
+ // 2. Try resuming A, but fail ETag check
+ mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
+ download.runUntilStatus(STATUS_FAILED);
+ assertEquals(HTTP_PRECON_FAILED, download.getReason());
+ req = takeRequest();
+ assertEquals("bytes=2-", getHeaderValue(req, "Range"));
+ assertEquals(A, getHeaderValue(req, "If-Match"));
+ }
+
public void testSanitizeMediaType() throws Exception {
enqueueResponse(buildEmptyResponse(HTTP_OK)
.setHeader("Content-Type", "text/html; charset=ISO-8859-4"));
@@ -400,7 +492,7 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
public void testNoContentLength() throws Exception {
enqueueResponse(buildEmptyResponse(HTTP_OK).removeHeader("Content-length"));
- runSimpleFailureTest(DownloadManager.ERROR_HTTP_DATA_ERROR);
+ runSimpleFailureTest(DownloadManager.ERROR_CANNOT_RESUME);
}
public void testInsufficientSpace() throws Exception {
@@ -412,22 +504,24 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
}
public void testCancel() throws Exception {
- mSystemFacade.setStartThreadsWithoutWaiting(true);
// return 'real time' from FakeSystemFacade so that DownloadThread will report progress
mSystemFacade.setReturnActualTime(true);
enqueueResponse(buildContinuingResponse());
Download download = enqueueRequest(getRequest());
- startService(null);
// give the download time to get started and progress to 1% completion
// before cancelling it.
boolean rslt = download.runUntilProgress(1);
assertTrue(rslt);
mManager.remove(download.mId);
- startService(null);
- int status = download.runUntilDone();
- // make sure the row is gone from the database
- assertEquals(-1, status);
- mSystemFacade.setReturnActualTime(false);
+
+ // Verify that row is removed from database
+ final long timeout = SystemClock.elapsedRealtime() + (15 * SECOND_IN_MILLIS);
+ while (download.getStatusIfExists() != -1) {
+ if (SystemClock.elapsedRealtime() > timeout) {
+ throw new TimeoutException("Row wasn't removed");
+ }
+ SystemClock.sleep(100);
+ }
}
public void testDownloadCompleteBroadcast() throws Exception {
@@ -512,9 +606,9 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
public void testContentObserver() throws Exception {
enqueueResponse(buildEmptyResponse(HTTP_OK));
- enqueueRequest(getRequest());
mResolver.resetNotified();
- runService();
+ final Download download = enqueueRequest(getRequest());
+ download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
assertTrue(mResolver.mNotifyWasCalled);
}
@@ -524,10 +618,9 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
final Download download = enqueueRequest(
getRequest().setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN));
download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
- runService();
+ verify(mNotifManager, times(1)).cancelAll();
verify(mNotifManager, never()).notify(anyString(), anyInt(), isA(Notification.class));
- // TODO: verify that it never cancels
}
public void testNotificationVisible() throws Exception {
@@ -536,11 +629,10 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
// only shows in-progress notifications
final Download download = enqueueRequest(getRequest());
download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
- runService();
// TODO: verify different notif types with tags
+ verify(mNotifManager, times(1)).cancelAll();
verify(mNotifManager, atLeastOnce()).notify(anyString(), anyInt(), isA(Notification.class));
- verify(mNotifManager, times(1)).cancel(anyString(), anyInt());
}
public void testNotificationVisibleComplete() throws Exception {
@@ -549,17 +641,16 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
final Download download = enqueueRequest(getRequest().setNotificationVisibility(
DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED));
download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
- runService();
// TODO: verify different notif types with tags
+ verify(mNotifManager, times(1)).cancelAll();
verify(mNotifManager, atLeastOnce()).notify(anyString(), anyInt(), isA(Notification.class));
- verify(mNotifManager, times(1)).cancel(anyString(), anyInt());
}
public void testRetryAfter() throws Exception {
final int delay = 120;
enqueueResponse(
- buildEmptyResponse(HTTP_SERVICE_UNAVAILABLE).setHeader("Retry-after", delay));
+ buildEmptyResponse(HTTP_UNAVAILABLE).setHeader("Retry-after", delay));
enqueueResponse(buildEmptyResponse(HTTP_OK));
Download download = enqueueRequest(getRequest());
@@ -643,19 +734,32 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
* 3) Resume request to complete download
* @return the last request sent to the server, resuming after the interruption
*/
- private RecordedRequest runRedirectionTest(int status)
- throws MalformedURLException, Exception {
+ private RecordedRequest runRedirectionTest(int status) throws Exception {
enqueueResponse(buildEmptyResponse(status)
.setHeader("Location", mServer.getUrl(REDIRECTED_PATH).toString()));
enqueueInterruptedDownloadResponses(5);
- Download download = enqueueRequest(getRequest());
- runService();
+ final Download download = enqueueRequest(getRequest());
+ download.runUntilStatus(DownloadManager.STATUS_PAUSED);
+ mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
+ download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
+
assertEquals(REQUEST_PATH, takeRequest().getPath());
assertEquals(REDIRECTED_PATH, takeRequest().getPath());
- mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
- download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
return takeRequest();
}
+
+ /**
+ * Return value of requested HTTP header, if it exists.
+ */
+ private static String getHeaderValue(RecordedRequest req, String header) {
+ header = header.toLowerCase() + ":";
+ for (String h : req.getHeaders()) {
+ if (h.toLowerCase().startsWith(header)) {
+ return h.substring(header.length()).trim();
+ }
+ }
+ return null;
+ }
}
diff --git a/tests/src/com/android/providers/downloads/ThreadingTest.java b/tests/src/com/android/providers/downloads/ThreadingTest.java
index 8605c76..920f703 100644
--- a/tests/src/com/android/providers/downloads/ThreadingTest.java
+++ b/tests/src/com/android/providers/downloads/ThreadingTest.java
@@ -16,23 +16,27 @@
package com.android.providers.downloads;
+import static java.net.HttpURLConnection.HTTP_OK;
+
import android.app.DownloadManager;
import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Pair;
+
+import com.google.android.collect.Lists;
+import com.google.android.collect.Sets;
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.SocketPolicy;
+
+import java.util.List;
+import java.util.Set;
/**
* Download manager tests that require multithreading.
*/
@LargeTest
public class ThreadingTest extends AbstractPublicApiTest {
- private static class FakeSystemFacadeWithThreading extends FakeSystemFacade {
- @Override
- public void startThread(Thread thread) {
- thread.start();
- }
- }
-
public ThreadingTest() {
- super(new FakeSystemFacadeWithThreading());
+ super(new FakeSystemFacade());
}
@Override
@@ -53,4 +57,41 @@ public class ThreadingTest extends AbstractPublicApiTest {
Thread.sleep(10);
}
}
+
+ public void testFilenameRace() throws Exception {
+ final List<Pair<Download, String>> downloads = Lists.newArrayList();
+
+ // Request dozen files at once with same name
+ for (int i = 0; i < 12; i++) {
+ final String body = "DOWNLOAD " + i + " CONTENTS";
+ enqueueResponse(new MockResponse().setResponseCode(HTTP_OK).setBody(body)
+ .setHeader("Content-type", "text/plain")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+
+ final Download d = enqueueRequest(getRequest());
+ downloads.add(Pair.create(d, body));
+ }
+
+ // Kick off downloads in parallel
+ final long startMillis = mSystemFacade.currentTimeMillis();
+ startService(null);
+
+ for (Pair<Download,String> d : downloads) {
+ d.first.waitForStatus(DownloadManager.STATUS_SUCCESSFUL, startMillis);
+ }
+
+ // Ensure that contents are clean and filenames unique
+ final Set<String> seenFiles = Sets.newHashSet();
+
+ for (Pair<Download, String> d : downloads) {
+ final String file = d.first.getStringField(DownloadManager.COLUMN_LOCAL_FILENAME);
+ if (!seenFiles.add(file)) {
+ fail("Another download already claimed " + file);
+ }
+
+ final String expected = d.second;
+ final String actual = d.first.getContents();
+ assertEquals(expected, actual);
+ }
+ }
}
diff --git a/ui/AndroidManifest.xml b/ui/AndroidManifest.xml
index 04d1863..f707dfb 100644
--- a/ui/AndroidManifest.xml
+++ b/ui/AndroidManifest.xml
@@ -9,11 +9,13 @@
<application android:process="android.process.media"
android:label="@string/app_label"
android:icon="@mipmap/ic_launcher_download"
- android:hardwareAccelerated="true">
+ android:hardwareAccelerated="true"
+ android:supportsRtl="true"
+ android:requiredForAllUsers="true">
+
<activity android:name=".DownloadList"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.DialogWhenLarge">
-
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
diff --git a/ui/res/layout/download_list.xml b/ui/res/layout/download_list.xml
index e4ebf7c..a0ff5ff 100644
--- a/ui/res/layout/download_list.xml
+++ b/ui/res/layout/download_list.xml
@@ -30,16 +30,16 @@
android:layout_weight="1">
<ExpandableListView android:id="@+id/date_ordered_list"
- android:paddingLeft="16dip"
- android:paddingRight="16dip"
+ android:paddingStart="16dip"
+ android:paddingEnd="16dip"
android:paddingBottom="16dip"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarStyle="outsideOverlay" />
<ListView android:id="@+id/size_ordered_list"
- android:paddingLeft="16dip"
- android:paddingRight="16dip"
+ android:paddingStart="16dip"
+ android:paddingEnd="16dip"
android:paddingBottom="16dip"
android:clipToPadding="false"
android:layout_width="match_parent"
diff --git a/ui/res/layout/download_list_item.xml b/ui/res/layout/download_list_item.xml
index e5759d5..2435ba7 100644
--- a/ui/res/layout/download_list_item.xml
+++ b/ui/res/layout/download_list_item.xml
@@ -21,8 +21,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
- android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="8dip"
android:paddingBottom="8dip"
android:columnCount="4"
@@ -40,9 +41,10 @@
android:layout_width="@android:dimen/app_icon_size"
android:layout_height="@android:dimen/app_icon_size"
android:layout_rowSpan="3"
- android:layout_marginRight="8dip"
+ android:layout_marginEnd="8dip"
android:layout_gravity="center_vertical"
- android:scaleType="centerInside" />
+ android:scaleType="centerInside"
+ android:contentDescription="@null" />
<TextView
android:id="@+id/download_title"
@@ -52,7 +54,8 @@
android:singleLine="true"
android:ellipsize="marquee"
android:textStyle="bold"
- android:textAppearance="?android:attr/textAppearance" />
+ android:textAppearance="?android:attr/textAppearance"
+ android:textAlignment="viewStart" />
<TextView
android:id="@+id/domain"
@@ -61,7 +64,8 @@
android:layout_gravity="fill_horizontal"
android:singleLine="true"
android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceSmall" />
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textAlignment="viewStart" />
<TextView
android:id="@+id/size_text"
@@ -69,11 +73,13 @@
android:layout_gravity="fill_horizontal"
android:singleLine="true"
android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceSmall" />
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textAlignment="viewStart" />
<TextView
android:id="@+id/status_text"
- android:layout_marginLeft="8dip"
- android:textAppearance="?android:attr/textAppearanceSmall" />
+ android:layout_marginStart="8dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textAlignment="viewStart" />
</com.android.providers.downloads.ui.DownloadItem>
diff --git a/ui/res/layout/list_group_header.xml b/ui/res/layout/list_group_header.xml
index 2600f8d..466cd6c 100644
--- a/ui/res/layout/list_group_header.xml
+++ b/ui/res/layout/list_group_header.xml
@@ -15,10 +15,9 @@
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeight"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:paddingLeft="43dip"
- android:layout_gravity="center_vertical"
- android:gravity="center_vertical"/>
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:paddingStart="?android:attr/expandableListPreferredItemPaddingLeft"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical" />
diff --git a/ui/res/menu/download_menu.xml b/ui/res/menu/download_menu.xml
index fc57892..a33dd52 100644
--- a/ui/res/menu/download_menu.xml
+++ b/ui/res/menu/download_menu.xml
@@ -17,9 +17,11 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/share_download"
android:icon="@android:drawable/ic_menu_share"
+ android:title="@string/download_share_dialog"
android:showAsAction="always" />
<item android:id="@+id/delete_download"
android:icon="@android:drawable/ic_menu_delete"
+ android:title="@string/delete_download"
android:showAsAction="always" />
</menu>
diff --git a/ui/res/values-ca/strings.xml b/ui/res/values-ca/strings.xml
index 43a8e38..072ca53 100644
--- a/ui/res/values-ca/strings.xml
+++ b/ui/res/values-ca/strings.xml
@@ -31,7 +31,7 @@
<string name="dialog_failed_body" msgid="587545111677064427">"Vols tornar a intentar baixar el fitxer més tard o vols suprimir-lo de la cua?"</string>
<string name="dialog_title_queued_body" msgid="6760681913815015219">"Fitxer en cua"</string>
<string name="dialog_queued_body" msgid="708552801635572720">"Aquest fitxer està en cua per baixar més endavant, per tant, encara no està disponible."</string>
- <string name="dialog_file_missing_body" msgid="3223012612774276284">"No es troba el fitxer que s\'ha baixat."</string>
+ <string name="dialog_file_missing_body" msgid="3223012612774276284">"No es pot trobar el fitxer baixat."</string>
<string name="dialog_insufficient_space_on_external" msgid="8692452156251449195">"No es pot finalitzar la baixada. No hi ha prou espai a l\'emmagatzematge extern."</string>
<string name="dialog_insufficient_space_on_cache" msgid="6313630206163908994">"No es pot finalitzar la baixada. No hi ha prou espai a l\'emmagatzematge intern."</string>
<string name="dialog_cannot_resume" msgid="8664509751358983543">"S\'ha interromput la baixada i no es pot reprendre."</string>
diff --git a/ui/res/values-es/strings.xml b/ui/res/values-es/strings.xml
index 5c72460..37903dc 100644
--- a/ui/res/values-es/strings.xml
+++ b/ui/res/values-es/strings.xml
@@ -45,6 +45,6 @@
<string name="retry_download" msgid="7617100787922717912">"Reintentar"</string>
<string name="deselect_all" msgid="6348198946254776764">"Desmarcar todo"</string>
<string name="select_all" msgid="634074918366265804">"Seleccionar todo"</string>
- <string name="selected_count" msgid="2101564570019753277">"Has seleccionado <xliff:g id="NUMBER">%1$d</xliff:g> de <xliff:g id="TOTAL">%2$d</xliff:g>."</string>
+ <string name="selected_count" msgid="2101564570019753277">"Elegido: <xliff:g id="NUMBER">%1$d</xliff:g> de <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
<string name="download_share_dialog" msgid="3355867339806448955">"Compartir a través de"</string>
</resources>
diff --git a/ui/res/values-fa/strings.xml b/ui/res/values-fa/strings.xml
index fd81dd2..8650ce3 100644
--- a/ui/res/values-fa/strings.xml
+++ b/ui/res/values-fa/strings.xml
@@ -21,7 +21,7 @@
<string name="download_title_sorted_by_size" msgid="1417193166677094813">"دانلودها - مرتب شده بر اساس اندازه"</string>
<string name="no_downloads" msgid="1029667411186146836">"خیر دانلودها."</string>
<string name="missing_title" msgid="830115697868833773">"&lt;ناشناس&gt;"</string>
- <string name="button_sort_by_size" msgid="7331549713691146251">"ترتیب بر اساس اندازه"</string>
+ <string name="button_sort_by_size" msgid="7331549713691146251">"بر اساس اندازه مرتب شود"</string>
<string name="button_sort_by_date" msgid="8800842892684101528">"ترتیب براساس تاریخ"</string>
<string name="download_queued" msgid="104973307780629904">"در صف"</string>
<string name="download_running" msgid="4656462962155580641">"در حال انجام"</string>
diff --git a/ui/res/values-hi/strings.xml b/ui/res/values-hi/strings.xml
index 8e36dcc..4761bd3 100644
--- a/ui/res/values-hi/strings.xml
+++ b/ui/res/values-hi/strings.xml
@@ -46,5 +46,5 @@
<string name="deselect_all" msgid="6348198946254776764">"सभी का चयन रद्द करें"</string>
<string name="select_all" msgid="634074918366265804">"सभी चुनें"</string>
<string name="selected_count" msgid="2101564570019753277">"<xliff:g id="TOTAL">%2$d</xliff:g> में से <xliff:g id="NUMBER">%1$d</xliff:g> चयनित"</string>
- <string name="download_share_dialog" msgid="3355867339806448955">"इसके द्वारा शेयर करें"</string>
+ <string name="download_share_dialog" msgid="3355867339806448955">"इसके द्वारा साझा करें"</string>
</resources>
diff --git a/ui/res/values/dimen.xml b/ui/res/values/dimen.xml
index 6e48f13..7519b87 100644
--- a/ui/res/values/dimen.xml
+++ b/ui/res/values/dimen.xml
@@ -15,5 +15,5 @@
-->
<resources>
- <dimen name="checkmark_area">40dip</dimen>
+ <dimen name="checkmark_area">48dip</dimen>
</resources>
diff --git a/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java b/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java
index 19132a1..f5d7077 100644
--- a/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java
+++ b/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java
@@ -273,7 +273,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
TextView item;
if (null == convertView || !(convertView instanceof TextView)) {
LayoutInflater factory = LayoutInflater.from(mContext);
- item = (TextView) factory.inflate(R.layout.list_group_header, null);
+ item = (TextView) factory.inflate(R.layout.list_group_header, parent, false);
} else {
item = (TextView) convertView;
}
diff --git a/ui/src/com/android/providers/downloads/ui/DownloadItem.java b/ui/src/com/android/providers/downloads/ui/DownloadItem.java
index e24ac4a..0562cd0 100644
--- a/ui/src/com/android/providers/downloads/ui/DownloadItem.java
+++ b/ui/src/com/android/providers/downloads/ui/DownloadItem.java
@@ -18,12 +18,11 @@ package com.android.providers.downloads.ui;
import android.content.Context;
import android.util.AttributeSet;
-import android.view.accessibility.AccessibilityEvent;
import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityEvent;
import android.widget.CheckBox;
import android.widget.Checkable;
import android.widget.GridLayout;
-import android.widget.RelativeLayout;
/**
* This class customizes RelativeLayout to directly handle clicks on the left part of the view and
@@ -83,12 +82,20 @@ public class DownloadItem extends GridLayout implements Checkable {
mDownloadList = downloadList;
}
+ private boolean inCheckArea(MotionEvent event) {
+ if (isLayoutRtl()) {
+ return event.getX() > getWidth() - CHECKMARK_AREA;
+ } else {
+ return event.getX() < CHECKMARK_AREA;
+ }
+ }
+
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = false;
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
- if (event.getX() < CHECKMARK_AREA) {
+ if (inCheckArea(event)) {
mIsInDownEvent = true;
handled = true;
}
@@ -99,7 +106,7 @@ public class DownloadItem extends GridLayout implements Checkable {
break;
case MotionEvent.ACTION_UP:
- if (mIsInDownEvent && event.getX() < CHECKMARK_AREA) {
+ if (mIsInDownEvent && inCheckArea(event)) {
toggle();
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
handled = true;
diff --git a/ui/src/com/android/providers/downloads/ui/DownloadList.java b/ui/src/com/android/providers/downloads/ui/DownloadList.java
index ed36993..fd000d3 100644
--- a/ui/src/com/android/providers/downloads/ui/DownloadList.java
+++ b/ui/src/com/android/providers/downloads/ui/DownloadList.java
@@ -732,7 +732,9 @@ public class DownloadList extends Activity {
Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, item.getKey());
final String mimeType = item.getValue().getMimeType();
attachments.add(uri);
- mimeTypes.add(mimeType);
+ if (mimeType != null) {
+ mimeTypes.add(mimeType);
+ }
}
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
intent.setType(findCommonMimeType(mimeTypes));