summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers')
-rw-r--r--src/com/android/providers/downloads/Constants.java2
-rw-r--r--src/com/android/providers/downloads/DownloadHandler.java42
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java23
-rw-r--r--src/com/android/providers/downloads/DownloadNotification.java287
-rw-r--r--src/com/android/providers/downloads/DownloadNotifier.java318
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java11
-rw-r--r--src/com/android/providers/downloads/DownloadReceiver.java210
-rw-r--r--src/com/android/providers/downloads/DownloadService.java36
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java61
-rw-r--r--src/com/android/providers/downloads/OpenHelper.java125
-rw-r--r--src/com/android/providers/downloads/RealSystemFacade.java43
-rw-r--r--src/com/android/providers/downloads/StorageManager.java11
-rw-r--r--src/com/android/providers/downloads/SystemFacade.java32
13 files changed, 720 insertions, 481 deletions
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
index 8481435f..8d806182 100644
--- a/src/com/android/providers/downloads/Constants.java
+++ b/src/com/android/providers/downloads/Constants.java
@@ -75,6 +75,8 @@ public class Constants {
/** The default extension for binary files if we can't get one at the HTTP level */
public static final String DEFAULT_DL_BINARY_EXTENSION = ".bin";
+ public static final String PROVIDER_PACKAGE_NAME = "com.android.providers.downloads";
+
/**
* When a number has to be appended to the filename, this string is used to separate the
* base filename from the sequence number
diff --git a/src/com/android/providers/downloads/DownloadHandler.java b/src/com/android/providers/downloads/DownloadHandler.java
index 29d34700..2f02864e 100644
--- a/src/com/android/providers/downloads/DownloadHandler.java
+++ b/src/com/android/providers/downloads/DownloadHandler.java
@@ -18,6 +18,9 @@ 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;
@@ -25,31 +28,37 @@ 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>();
- private static final DownloadHandler mDownloadHandler = new DownloadHandler();
+ @GuardedBy("this")
+ private final LongSparseArray<Long> mCurrentSpeed = new LongSparseArray<Long>();
+
private final int mMaxConcurrentDownloadsAllowed = Resources.getSystem().getInteger(
com.android.internal.R.integer.config_MaxConcurrentDownloadsAllowed);
- static DownloadHandler getInstance() {
- return mDownloadHandler;
+ private static final DownloadHandler sDownloadHandler = new DownloadHandler();
+
+ public static DownloadHandler getInstance() {
+ return sDownloadHandler;
}
- synchronized void enqueueDownload(DownloadInfo info) {
+ 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);
- startDownloadThread();
+ startDownloadThreadLocked();
}
}
- private synchronized void startDownloadThread() {
+ private void startDownloadThreadLocked() {
Iterator<Long> keys = mDownloadsQueue.keySet().iterator();
ArrayList<Long> ids = new ArrayList<Long>();
while (mDownloadsInProgress.size() < mMaxConcurrentDownloadsAllowed && keys.hasNext()) {
@@ -67,21 +76,30 @@ public class DownloadHandler {
}
}
- synchronized boolean hasDownloadInQueue(long id) {
+ public synchronized boolean hasDownloadInQueue(long id) {
return mDownloadsQueue.containsKey(id) || mDownloadsInProgress.containsKey(id);
}
- synchronized void dequeueDownload(long mId) {
- mDownloadsInProgress.remove(mId);
- startDownloadThread();
+ 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 {
+ public synchronized void waitUntilDownloadsTerminate() throws InterruptedException {
if (mDownloadsInProgress.size() == 0 && mDownloadsQueue.size() == 0) {
if (Constants.LOGVV) {
Log.i(TAG, "nothing to wait on");
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index e452e5bf..5172b696 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -36,7 +36,6 @@ import android.util.Pair;
import com.android.internal.util.IndentingPrintWriter;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -342,7 +341,7 @@ public class DownloadInfo {
*/
public int checkCanUseNetwork() {
final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mUid);
- if (info == null) {
+ if (info == null || !info.isConnected()) {
return NETWORK_NO_CONNECTION;
}
if (DetailedState.BLOCKED.equals(info.getDetailedState())) {
@@ -576,4 +575,24 @@ public class DownloadInfo {
StorageManager.getInstance(mContext));
mSystemFacade.startThread(downloader);
}
+
+ /**
+ * Query and return status of requested download.
+ */
+ public static int queryDownloadStatus(ContentResolver resolver, long id) {
+ final Cursor cursor = resolver.query(
+ ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id),
+ new String[] { Downloads.Impl.COLUMN_STATUS }, null, null, null);
+ try {
+ if (cursor.moveToFirst()) {
+ return cursor.getInt(0);
+ } else {
+ // TODO: increase strictness of value returned for unknown
+ // downloads; this is safe default for now.
+ return Downloads.Impl.STATUS_PENDING;
+ }
+ } finally {
+ cursor.close();
+ }
+ }
}
diff --git a/src/com/android/providers/downloads/DownloadNotification.java b/src/com/android/providers/downloads/DownloadNotification.java
deleted file mode 100644
index bbd39f60..00000000
--- a/src/com/android/providers/downloads/DownloadNotification.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.providers.downloads;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.provider.Downloads;
-import android.text.TextUtils;
-import android.util.SparseLongArray;
-
-import java.util.Collection;
-import java.util.HashMap;
-
-/**
- * This class handles the updating of the Notification Manager for the
- * cases where there is an ongoing download. Once the download is complete
- * (be it successful or unsuccessful) it is no longer the responsibility
- * of this component to show the download in the notification manager.
- *
- */
-class DownloadNotification {
-
- Context mContext;
- HashMap <String, NotificationItem> mNotifications;
- private SystemFacade mSystemFacade;
-
- /** Time when each {@link DownloadInfo#mId} was first shown. */
- private SparseLongArray mFirstShown = new SparseLongArray();
-
- static final String LOGTAG = "DownloadNotification";
- static final String WHERE_RUNNING =
- "(" + Downloads.Impl.COLUMN_STATUS + " >= '100') AND (" +
- Downloads.Impl.COLUMN_STATUS + " <= '199') AND (" +
- Downloads.Impl.COLUMN_VISIBILITY + " IS NULL OR " +
- Downloads.Impl.COLUMN_VISIBILITY + " == '" + Downloads.Impl.VISIBILITY_VISIBLE + "' OR " +
- Downloads.Impl.COLUMN_VISIBILITY +
- " == '" + Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED + "')";
- static final String WHERE_COMPLETED =
- Downloads.Impl.COLUMN_STATUS + " >= '200' AND " +
- Downloads.Impl.COLUMN_VISIBILITY +
- " == '" + Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED + "'";
-
-
- /**
- * This inner class is used to collate downloads that are owned by
- * the same application. This is so that only one notification line
- * item is used for all downloads of a given application.
- *
- */
- static class NotificationItem {
- // TODO: refactor to mNotifId and avoid building Uris based on it, since
- // they can overflow
- int mId; // This first db _id for the download for the app
- long mTotalCurrent = 0;
- long mTotalTotal = 0;
- int mTitleCount = 0;
- String mPackageName; // App package name
- String mDescription;
- String[] mTitles = new String[2]; // download titles.
- String mPausedText = null;
-
- /*
- * Add a second download to this notification item.
- */
- void addItem(String title, long currentBytes, long totalBytes) {
- mTotalCurrent += currentBytes;
- if (totalBytes <= 0 || mTotalTotal == -1) {
- mTotalTotal = -1;
- } else {
- mTotalTotal += totalBytes;
- }
- if (mTitleCount < 2) {
- mTitles[mTitleCount] = title;
- }
- mTitleCount++;
- }
- }
-
-
- /**
- * Constructor
- * @param ctx The context to use to obtain access to the
- * Notification Service
- */
- DownloadNotification(Context ctx, SystemFacade systemFacade) {
- mContext = ctx;
- mSystemFacade = systemFacade;
- mNotifications = new HashMap<String, NotificationItem>();
- }
-
- /*
- * Update the notification ui.
- */
- public void updateNotification(Collection<DownloadInfo> downloads) {
- updateActiveNotification(downloads);
- updateCompletedNotification(downloads);
- }
-
- private void updateActiveNotification(Collection<DownloadInfo> downloads) {
- // Collate the notifications
- mNotifications.clear();
- for (DownloadInfo download : downloads) {
- if (!isActiveAndVisible(download)) {
- continue;
- }
- String packageName = download.mPackage;
- long max = download.mTotalBytes;
- long progress = download.mCurrentBytes;
- long id = download.mId;
- String title = download.mTitle;
- if (title == null || title.length() == 0) {
- title = mContext.getResources().getString(
- R.string.download_unknown_title);
- }
-
- NotificationItem item;
- if (mNotifications.containsKey(packageName)) {
- item = mNotifications.get(packageName);
- item.addItem(title, progress, max);
- } else {
- item = new NotificationItem();
- item.mId = (int) id;
- item.mPackageName = packageName;
- item.mDescription = download.mDescription;
- item.addItem(title, progress, max);
- mNotifications.put(packageName, item);
- }
- if (download.mStatus == Downloads.Impl.STATUS_QUEUED_FOR_WIFI
- && item.mPausedText == null) {
- item.mPausedText = mContext.getResources().getString(
- R.string.notification_need_wifi_for_size);
- }
- }
-
- // Add the notifications
- for (NotificationItem item : mNotifications.values()) {
- // Build the notification object
- final Notification.Builder builder = new Notification.Builder(mContext);
-
- boolean hasPausedText = (item.mPausedText != null);
- int iconResource = android.R.drawable.stat_sys_download;
- if (hasPausedText) {
- iconResource = android.R.drawable.stat_sys_warning;
- }
- builder.setSmallIcon(iconResource);
- builder.setOngoing(true);
-
- // set notification "when" to be first time this DownloadInfo.mId
- // was encountered, which avoids fighting with other notifs.
- long firstShown = mFirstShown.get(item.mId, -1);
- if (firstShown == -1) {
- firstShown = System.currentTimeMillis();
- mFirstShown.put(item.mId, firstShown);
- }
- builder.setWhen(firstShown);
-
- boolean hasContentText = false;
- StringBuilder title = new StringBuilder(item.mTitles[0]);
- if (item.mTitleCount > 1) {
- title.append(mContext.getString(R.string.notification_filename_separator));
- title.append(item.mTitles[1]);
- if (item.mTitleCount > 2) {
- title.append(mContext.getString(R.string.notification_filename_extras,
- new Object[] { Integer.valueOf(item.mTitleCount - 2) }));
- }
- } else if (!TextUtils.isEmpty(item.mDescription)) {
- builder.setContentText(item.mDescription);
- hasContentText = true;
- }
- builder.setContentTitle(title);
-
- if (hasPausedText) {
- builder.setContentText(item.mPausedText);
- } else {
- builder.setProgress(
- (int) item.mTotalTotal, (int) item.mTotalCurrent, item.mTotalTotal == -1);
- if (hasContentText) {
- builder.setContentInfo(
- buildPercentageLabel(mContext, item.mTotalTotal, item.mTotalCurrent));
- }
- }
-
- Intent intent = new Intent(Constants.ACTION_LIST);
- intent.setClassName("com.android.providers.downloads",
- DownloadReceiver.class.getName());
- intent.setData(
- ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, item.mId));
- intent.putExtra("multiple", item.mTitleCount > 1);
-
- builder.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0));
-
- mSystemFacade.postNotification(item.mId, builder.getNotification());
-
- }
- }
-
- private void updateCompletedNotification(Collection<DownloadInfo> downloads) {
- for (DownloadInfo download : downloads) {
- if (!isCompleteAndVisible(download)) {
- continue;
- }
- notificationForCompletedDownload(download.mId, download.mTitle,
- download.mStatus, download.mDestination, download.mLastMod);
- }
- }
- void notificationForCompletedDownload(long id, String title, int status,
- int destination, long lastMod) {
- // Add the notifications
- Notification.Builder builder = new Notification.Builder(mContext);
- builder.setSmallIcon(android.R.drawable.stat_sys_download_done);
- if (title == null || title.length() == 0) {
- title = mContext.getResources().getString(
- R.string.download_unknown_title);
- }
- Uri contentUri =
- ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
- String caption;
- Intent intent;
- if (Downloads.Impl.isStatusError(status)) {
- caption = mContext.getResources()
- .getString(R.string.notification_download_failed);
- intent = new Intent(Constants.ACTION_LIST);
- } else {
- caption = mContext.getResources()
- .getString(R.string.notification_download_complete);
- if (destination != Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
- intent = new Intent(Constants.ACTION_OPEN);
- } else {
- intent = new Intent(Constants.ACTION_LIST);
- }
- }
- intent.setClassName("com.android.providers.downloads",
- DownloadReceiver.class.getName());
- intent.setData(contentUri);
-
- builder.setWhen(lastMod);
- builder.setContentTitle(title);
- builder.setContentText(caption);
- builder.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0));
-
- intent = new Intent(Constants.ACTION_HIDE);
- intent.setClassName("com.android.providers.downloads",
- DownloadReceiver.class.getName());
- intent.setData(contentUri);
- builder.setDeleteIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0));
-
- mSystemFacade.postNotification(id, builder.getNotification());
- }
-
- private boolean isActiveAndVisible(DownloadInfo download) {
- return 100 <= download.mStatus && download.mStatus < 200
- && download.mVisibility != Downloads.Impl.VISIBILITY_HIDDEN;
- }
-
- private boolean isCompleteAndVisible(DownloadInfo download) {
- return download.mStatus >= 200
- && download.mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
- }
-
- private static String buildPercentageLabel(
- Context context, long totalBytes, long currentBytes) {
- if (totalBytes <= 0) {
- return null;
- } else {
- final int percent = (int) (100 * currentBytes / totalBytes);
- return context.getString(R.string.download_percent, percent);
- }
- }
-}
diff --git a/src/com/android/providers/downloads/DownloadNotifier.java b/src/com/android/providers/downloads/DownloadNotifier.java
new file mode 100644
index 00000000..daae7831
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadNotifier.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2012 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.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 android.app.DownloadManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.provider.Downloads;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Update {@link NotificationManager} to reflect current {@link DownloadInfo}
+ * states. Collapses similar downloads into a single notification, and builds
+ * {@link PendingIntent} that launch towards {@link DownloadReceiver}.
+ */
+public class DownloadNotifier {
+
+ private static final int TYPE_ACTIVE = 1;
+ private static final int TYPE_WAITING = 2;
+ private static final int TYPE_COMPLETE = 3;
+
+ private final Context mContext;
+ private final NotificationManager mNotifManager;
+
+ /**
+ * Currently active notifications, mapped from clustering tag to timestamp
+ * when first shown.
+ *
+ * @see #buildNotificationTag(DownloadInfo)
+ */
+ @GuardedBy("mActiveNotifs")
+ private final HashMap<String, Long> mActiveNotifs = Maps.newHashMap();
+
+ public DownloadNotifier(Context context) {
+ mContext = context;
+ mNotifManager = (NotificationManager) context.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ }
+
+ public void cancelAll() {
+ mNotifManager.cancelAll();
+ }
+
+ /**
+ * Update {@link NotificationManager} to reflect the given set of
+ * {@link DownloadInfo}, adding, collapsing, and removing as needed.
+ */
+ public void updateWith(Collection<DownloadInfo> downloads) {
+ synchronized (mActiveNotifs) {
+ updateWithLocked(downloads);
+ }
+ }
+
+ private void updateWithLocked(Collection<DownloadInfo> downloads) {
+ final Resources res = mContext.getResources();
+
+ // Cluster downloads together
+ final Multimap<String, DownloadInfo> clustered = ArrayListMultimap.create();
+ for (DownloadInfo info : downloads) {
+ final String tag = buildNotificationTag(info);
+ if (tag != null) {
+ clustered.put(tag, info);
+ }
+ }
+
+ // Build notification for each cluster
+ for (String tag : clustered.keySet()) {
+ final int type = getNotificationTagType(tag);
+ final Collection<DownloadInfo> cluster = clustered.get(tag);
+
+ final Notification.Builder builder = new Notification.Builder(mContext);
+
+ // Use time when cluster was first shown to avoid shuffling
+ final long firstShown;
+ if (mActiveNotifs.containsKey(tag)) {
+ firstShown = mActiveNotifs.get(tag);
+ } else {
+ firstShown = System.currentTimeMillis();
+ mActiveNotifs.put(tag, firstShown);
+ }
+ builder.setWhen(firstShown);
+
+ // Show relevant icon
+ if (type == TYPE_ACTIVE) {
+ builder.setSmallIcon(android.R.drawable.stat_sys_download);
+ } else if (type == TYPE_WAITING) {
+ builder.setSmallIcon(android.R.drawable.stat_sys_warning);
+ } else if (type == TYPE_COMPLETE) {
+ builder.setSmallIcon(android.R.drawable.stat_sys_download_done);
+ }
+
+ // Build action intents
+ if (type == TYPE_ACTIVE || type == TYPE_WAITING) {
+ final Intent intent = new Intent(Constants.ACTION_LIST,
+ null, mContext, DownloadReceiver.class);
+ intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
+ getDownloadIds(cluster));
+ builder.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0));
+ builder.setOngoing(true);
+
+ } else if (type == TYPE_COMPLETE) {
+ final DownloadInfo info = cluster.iterator().next();
+ final Uri uri = ContentUris.withAppendedId(
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, info.mId);
+
+ final String action;
+ if (Downloads.Impl.isStatusError(info.mStatus)) {
+ action = Constants.ACTION_LIST;
+ } else {
+ if (info.mDestination != Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
+ action = Constants.ACTION_OPEN;
+ } else {
+ action = Constants.ACTION_LIST;
+ }
+ }
+
+ final Intent intent = new Intent(action, uri, mContext, DownloadReceiver.class);
+ intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
+ getDownloadIds(cluster));
+ builder.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0));
+
+ final Intent hideIntent = new Intent(Constants.ACTION_HIDE,
+ uri, mContext, DownloadReceiver.class);
+ builder.setDeleteIntent(PendingIntent.getBroadcast(mContext, 0, hideIntent, 0));
+ }
+
+ // Calculate and show progress
+ 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);
+ }
+ }
+
+ if (total > 0) {
+ final int percent = (int) ((current * 100) / total);
+ percentText = res.getString(R.string.download_percent, percent);
+
+ if (speed > 0) {
+ final long remainingMillis = ((total - current) * 1000) / speed;
+ remainingText = res.getString(R.string.download_remaining,
+ DateUtils.formatDuration(remainingMillis));
+ }
+
+ builder.setProgress(100, percent, false);
+ } else {
+ builder.setProgress(100, 0, true);
+ }
+ }
+
+ // Build titles and description
+ final Notification notif;
+ if (cluster.size() == 1) {
+ final DownloadInfo info = cluster.iterator().next();
+
+ builder.setContentTitle(getDownloadTitle(res, info));
+
+ if (type == TYPE_ACTIVE) {
+ if (!TextUtils.isEmpty(info.mDescription)) {
+ builder.setContentText(info.mDescription);
+ } else {
+ builder.setContentText(remainingText);
+ }
+ builder.setContentInfo(percentText);
+
+ } else if (type == TYPE_WAITING) {
+ builder.setContentText(
+ res.getString(R.string.notification_need_wifi_for_size));
+
+ } else if (type == TYPE_COMPLETE) {
+ if (Downloads.Impl.isStatusError(info.mStatus)) {
+ builder.setContentText(res.getText(R.string.notification_download_failed));
+ } else if (Downloads.Impl.isStatusSuccess(info.mStatus)) {
+ builder.setContentText(
+ res.getText(R.string.notification_download_complete));
+ }
+ }
+
+ notif = builder.build();
+
+ } else {
+ final Notification.InboxStyle inboxStyle = new Notification.InboxStyle(builder);
+
+ for (DownloadInfo info : cluster) {
+ inboxStyle.addLine(getDownloadTitle(res, info));
+ }
+
+ if (type == TYPE_ACTIVE) {
+ builder.setContentTitle(res.getQuantityString(
+ R.plurals.notif_summary_active, cluster.size(), cluster.size()));
+ builder.setContentText(remainingText);
+ builder.setContentInfo(percentText);
+ inboxStyle.setSummaryText(remainingText);
+
+ } else if (type == TYPE_WAITING) {
+ builder.setContentTitle(res.getQuantityString(
+ R.plurals.notif_summary_waiting, cluster.size(), cluster.size()));
+ builder.setContentText(
+ res.getString(R.string.notification_need_wifi_for_size));
+ inboxStyle.setSummaryText(
+ res.getString(R.string.notification_need_wifi_for_size));
+ }
+
+ notif = inboxStyle.build();
+ }
+
+ mNotifManager.notify(tag, 0, notif);
+ }
+
+ // Remove stale tags that weren't renewed
+ final Iterator<String> it = mActiveNotifs.keySet().iterator();
+ while (it.hasNext()) {
+ final String tag = it.next();
+ if (!clustered.containsKey(tag)) {
+ mNotifManager.cancel(tag, 0);
+ it.remove();
+ }
+ }
+ }
+
+ private static CharSequence getDownloadTitle(Resources res, DownloadInfo info) {
+ if (!TextUtils.isEmpty(info.mTitle)) {
+ return info.mTitle;
+ } else {
+ return res.getString(R.string.download_unknown_title);
+ }
+ }
+
+ private long[] getDownloadIds(Collection<DownloadInfo> infos) {
+ final long[] ids = new long[infos.size()];
+ int i = 0;
+ for (DownloadInfo info : infos) {
+ ids[i++] = info.mId;
+ }
+ return ids;
+ }
+
+ /**
+ * Build tag used for collapsing several {@link DownloadInfo} into a single
+ * {@link Notification}.
+ */
+ private static String buildNotificationTag(DownloadInfo info) {
+ if (info.mStatus == Downloads.Impl.STATUS_QUEUED_FOR_WIFI) {
+ return TYPE_WAITING + ":" + info.mPackage;
+ } else if (isActiveAndVisible(info)) {
+ return TYPE_ACTIVE + ":" + info.mPackage;
+ } else if (isCompleteAndVisible(info)) {
+ // Complete downloads always have unique notifs
+ return TYPE_COMPLETE + ":" + info.mId;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Return the cluster type of the given tag, as created by
+ * {@link #buildNotificationTag(DownloadInfo)}.
+ */
+ private static int getNotificationTagType(String tag) {
+ return Integer.parseInt(tag.substring(0, tag.indexOf(':')));
+ }
+
+ private static boolean isActiveAndVisible(DownloadInfo download) {
+ return Downloads.Impl.isStatusInformational(download.mStatus) &&
+ (download.mVisibility == VISIBILITY_VISIBLE
+ || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+ }
+
+ private static boolean isCompleteAndVisible(DownloadInfo download) {
+ return Downloads.Impl.isStatusCompleted(download.mStatus) &&
+ (download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED
+ || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
+ }
+}
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index 40ebd2bb..c554e41d 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -667,15 +667,10 @@ public final class DownloadProvider extends ContentProvider {
Context context = getContext();
if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
- // don't start downloadservice because it has nothing to do in this case.
- // but does a completion notification need to be sent?
+ // When notification is requested, kick off service to process all
+ // relevant downloads.
if (Downloads.Impl.isNotificationToBeDisplayed(vis)) {
- DownloadNotification notifier = new DownloadNotification(context, mSystemFacade);
- notifier.notificationForCompletedDownload(rowID,
- values.getAsString(Downloads.Impl.COLUMN_TITLE),
- Downloads.Impl.STATUS_SUCCESS,
- Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD,
- lastMod);
+ context.startService(new Intent(context, DownloadService.class));
}
} else {
context.startService(new Intent(context, DownloadService.class));
diff --git a/src/com/android/providers/downloads/DownloadReceiver.java b/src/com/android/providers/downloads/DownloadReceiver.java
index 26ad992e..42f029a3 100644
--- a/src/com/android/providers/downloads/DownloadReceiver.java
+++ b/src/com/android/providers/downloads/DownloadReceiver.java
@@ -16,6 +16,9 @@
package com.android.providers.downloads;
+import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
+import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION;
+
import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
@@ -27,22 +30,34 @@ import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.provider.Downloads;
+import android.text.TextUtils;
import android.util.Log;
+import android.widget.Toast;
import com.google.common.annotations.VisibleForTesting;
-import java.io.File;
-
/**
* Receives system broadcasts (boot, network connectivity)
*/
public class DownloadReceiver extends BroadcastReceiver {
+ private static final String TAG = "DownloadReceiver";
+
+ private static Handler sAsyncHandler;
+
+ static {
+ final HandlerThread thread = new HandlerThread(TAG);
+ thread.start();
+ sAsyncHandler = new Handler(thread.getLooper());
+ }
+
@VisibleForTesting
SystemFacade mSystemFacade = null;
@Override
- public void onReceive(Context context, Intent intent) {
+ public void onReceive(final Context context, final Intent intent) {
if (mSystemFacade == null) {
mSystemFacade = new RealSystemFacade(context);
}
@@ -72,7 +87,20 @@ public class DownloadReceiver extends BroadcastReceiver {
} else if (action.equals(Constants.ACTION_OPEN)
|| action.equals(Constants.ACTION_LIST)
|| action.equals(Constants.ACTION_HIDE)) {
- handleNotificationBroadcast(context, intent);
+
+ final PendingResult result = goAsync();
+ if (result == null) {
+ // TODO: remove this once test is refactored
+ handleNotificationBroadcast(context, intent);
+ } else {
+ sAsyncHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ handleNotificationBroadcast(context, intent);
+ result.finish();
+ }
+ });
+ }
}
}
@@ -80,56 +108,49 @@ public class DownloadReceiver extends BroadcastReceiver {
* Handle any broadcast related to a system notification.
*/
private void handleNotificationBroadcast(Context context, Intent intent) {
- Uri uri = intent.getData();
- String action = intent.getAction();
- if (Constants.LOGVV) {
- if (action.equals(Constants.ACTION_OPEN)) {
- Log.v(Constants.TAG, "Receiver open for " + uri);
- } else if (action.equals(Constants.ACTION_LIST)) {
- Log.v(Constants.TAG, "Receiver list for " + uri);
- } else { // ACTION_HIDE
- Log.v(Constants.TAG, "Receiver hide for " + uri);
- }
+ final String action = intent.getAction();
+ if (Constants.ACTION_LIST.equals(action)) {
+ final long[] ids = intent.getLongArrayExtra(
+ DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
+ sendNotificationClickedIntent(context, ids);
+
+ } else if (Constants.ACTION_OPEN.equals(action)) {
+ final long id = ContentUris.parseId(intent.getData());
+ openDownload(context, id);
+ hideNotification(context, id);
+
+ } else if (Constants.ACTION_HIDE.equals(action)) {
+ final long id = ContentUris.parseId(intent.getData());
+ hideNotification(context, id);
}
+ }
- Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
- if (cursor == null) {
- return;
- }
+ /**
+ * Mark the given {@link DownloadManager#COLUMN_ID} as being acknowledged by
+ * user so it's not renewed later.
+ */
+ private void hideNotification(Context context, long id) {
+ final int status;
+ final int visibility;
+
+ final Uri uri = ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
+ final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
try {
- if (!cursor.moveToFirst()) {
+ if (cursor.moveToFirst()) {
+ status = getInt(cursor, Downloads.Impl.COLUMN_STATUS);
+ visibility = getInt(cursor, Downloads.Impl.COLUMN_VISIBILITY);
+ } else {
+ Log.w(TAG, "Missing details for download " + id);
return;
}
-
- if (action.equals(Constants.ACTION_OPEN)) {
- openDownload(context, cursor);
- hideNotification(context, uri, cursor);
- } else if (action.equals(Constants.ACTION_LIST)) {
- sendNotificationClickedIntent(intent, cursor);
- } else { // ACTION_HIDE
- hideNotification(context, uri, cursor);
- }
} finally {
cursor.close();
}
- }
- /**
- * Hide a system notification for a download.
- * @param uri URI to update the download
- * @param cursor Cursor for reading the download's fields
- */
- private void hideNotification(Context context, Uri uri, Cursor cursor) {
- mSystemFacade.cancelNotification(ContentUris.parseId(uri));
-
- int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
- int status = cursor.getInt(statusColumn);
- int visibilityColumn =
- cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_VISIBILITY);
- int visibility = cursor.getInt(visibilityColumn);
- if (Downloads.Impl.isStatusCompleted(status)
- && visibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
- ContentValues values = new ContentValues();
+ if (Downloads.Impl.isStatusCompleted(status) &&
+ (visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED
+ || visibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)) {
+ final ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_VISIBILITY,
Downloads.Impl.VISIBILITY_VISIBLE);
context.getContentResolver().update(uri, values, null, null);
@@ -137,79 +158,84 @@ public class DownloadReceiver extends BroadcastReceiver {
}
/**
- * Open the download that cursor is currently pointing to, since it's completed notification
- * has been clicked.
+ * Start activity to display the file represented by the given
+ * {@link DownloadManager#COLUMN_ID}.
*/
- private void openDownload(Context context, Cursor cursor) {
- String filename = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl._DATA));
- String mimetype =
- cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE));
- Uri path = Uri.parse(filename);
- // If there is no scheme, then it must be a file
- if (path.getScheme() == null) {
- path = Uri.fromFile(new File(filename));
- }
-
- Intent activityIntent = new Intent(Intent.ACTION_VIEW);
- mimetype = DownloadDrmHelper.getOriginalMimeType(context, filename, mimetype);
- activityIntent.setDataAndType(path, mimetype);
- activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ private void openDownload(Context context, long id) {
+ final Intent intent = OpenHelper.buildViewIntent(context, id);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
- context.startActivity(activityIntent);
+ context.startActivity(intent);
} catch (ActivityNotFoundException ex) {
- Log.d(Constants.TAG, "no activity for " + mimetype, ex);
+ Log.d(Constants.TAG, "no activity for " + intent, ex);
+ Toast.makeText(context, R.string.download_no_application_title, Toast.LENGTH_LONG)
+ .show();
}
}
/**
* Notify the owner of a running download that its notification was clicked.
- * @param intent the broadcast intent sent by the notification manager
- * @param cursor Cursor for reading the download's fields
*/
- private void sendNotificationClickedIntent(Intent intent, Cursor cursor) {
- String pckg = cursor.getString(
- cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE));
- if (pckg == null) {
- return;
+ private void sendNotificationClickedIntent(Context context, long[] ids) {
+ final String packageName;
+ final String clazz;
+ final boolean isPublicApi;
+
+ final Uri uri = ContentUris.withAppendedId(
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, ids[0]);
+ final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
+ try {
+ if (cursor.moveToFirst()) {
+ packageName = getString(cursor, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
+ clazz = getString(cursor, Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
+ isPublicApi = getInt(cursor, Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0;
+ } else {
+ Log.w(TAG, "Missing details for download " + ids[0]);
+ return;
+ }
+ } finally {
+ cursor.close();
}
- String clazz = cursor.getString(
- cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_NOTIFICATION_CLASS));
- boolean isPublicApi =
- cursor.getInt(cursor.getColumnIndex(Downloads.Impl.COLUMN_IS_PUBLIC_API)) != 0;
+ if (TextUtils.isEmpty(packageName)) {
+ Log.w(TAG, "Missing package; skipping broadcast");
+ return;
+ }
Intent appIntent = null;
if (isPublicApi) {
appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED);
- appIntent.setPackage(pckg);
- // send id of the items clicked on.
- if (intent.getBooleanExtra("multiple", false)) {
- // broadcast received saying click occurred on a notification with multiple titles.
- // don't include any ids at all - let the caller query all downloads belonging to it
- // TODO modify the broadcast to include ids of those multiple notifications.
- } else {
- appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
- new long[] {
- cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.Impl._ID))});
- }
+ appIntent.setPackage(packageName);
+ appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, ids);
+
} else { // legacy behavior
- if (clazz == null) {
+ if (TextUtils.isEmpty(clazz)) {
+ Log.w(TAG, "Missing class; skipping broadcast");
return;
}
+
appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED);
- appIntent.setClassName(pckg, clazz);
- if (intent.getBooleanExtra("multiple", true)) {
- appIntent.setData(Downloads.Impl.CONTENT_URI);
+ appIntent.setClassName(packageName, clazz);
+ appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, ids);
+
+ if (ids.length == 1) {
+ appIntent.setData(uri);
} else {
- long downloadId = cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
- appIntent.setData(
- ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, downloadId));
+ appIntent.setData(Downloads.Impl.CONTENT_URI);
}
}
mSystemFacade.sendBroadcast(appIntent);
}
+ private static String getString(Cursor cursor, String col) {
+ return cursor.getString(cursor.getColumnIndexOrThrow(col));
+ }
+
+ private static int getInt(Cursor cursor, String col) {
+ return cursor.getInt(cursor.getColumnIndexOrThrow(col));
+ }
+
private void startService(Context context) {
context.startService(new Intent(context, DownloadService.class));
}
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
index 3b566f8e..b97346b2 100644
--- a/src/com/android/providers/downloads/DownloadService.java
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -38,8 +38,8 @@ import android.os.RemoteException;
import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
-import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.IndentingPrintWriter;
import com.google.android.collect.Maps;
import com.google.common.annotations.VisibleForTesting;
@@ -65,7 +65,7 @@ public class DownloadService extends Service {
private DownloadManagerContentObserver mObserver;
/** Class to handle Notification Manager updates */
- private DownloadNotification mNotifier;
+ private DownloadNotifier mNotifier;
/**
* The Service's view of the list of downloads, mapping download IDs to the corresponding info
@@ -73,6 +73,7 @@ public class DownloadService extends Service {
* downloads based on this data, so that it can deal with situation where the data in the
* content provider changes or disappears.
*/
+ @GuardedBy("mDownloads")
private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap();
/**
@@ -221,8 +222,9 @@ public class DownloadService extends Service {
mMediaScannerConnecting = false;
mMediaScannerConnection = new MediaScannerConnection();
- mNotifier = new DownloadNotification(this, mSystemFacade);
- mSystemFacade.cancelAllNotifications();
+ mNotifier = new DownloadNotifier(this);
+ mNotifier.cancelAll();
+
mStorageManager = StorageManager.getInstance(getApplicationContext());
updateFromProvider();
}
@@ -356,7 +358,7 @@ public class DownloadService extends Service {
}
}
}
- mNotifier.updateNotification(mDownloads.values());
+ mNotifier.updateWith(mDownloads.values());
if (mustScan) {
bindMediaScanner();
} else {
@@ -456,18 +458,6 @@ public class DownloadService extends Service {
Log.v(Constants.TAG, "processing updated download " + info.mId +
", status: " + info.mStatus);
}
-
- boolean lostVisibility =
- oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
- && info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
- && Downloads.Impl.isStatusCompleted(info.mStatus);
- boolean justCompleted =
- !Downloads.Impl.isStatusCompleted(oldStatus)
- && Downloads.Impl.isStatusCompleted(info.mStatus);
- if (lostVisibility || justCompleted) {
- mSystemFacade.cancelNotification(info.mId);
- }
-
info.startIfReady(now, mStorageManager);
}
@@ -476,17 +466,15 @@ public class DownloadService extends Service {
*/
private void deleteDownloadLocked(long id) {
DownloadInfo info = mDownloads.get(id);
- if (info.shouldScanFile()) {
- scanFile(info, false, false);
- }
if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
info.mStatus = Downloads.Impl.STATUS_CANCELED;
}
if (info.mDestination != Downloads.Impl.DESTINATION_EXTERNAL && info.mFileName != null) {
- Slog.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
+ if (Constants.LOGVV) {
+ Log.d(TAG, "deleteDownloadLocked() deleting " + info.mFileName);
+ }
new File(info.mFileName).delete();
}
- mSystemFacade.cancelNotification(info.mId);
mDownloads.remove(info.mId);
}
@@ -559,7 +547,9 @@ public class DownloadService extends Service {
private void deleteFileIfExists(String path) {
try {
if (!TextUtils.isEmpty(path)) {
- Slog.d(TAG, "deleteFileIfExists() deleting " + path);
+ if (Constants.LOGVV) {
+ Log.d(TAG, "deleteFileIfExists() deleting " + path);
+ }
File file = new File(path);
file.delete();
}
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index bd91eaa1..34bc8e34 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -29,11 +29,11 @@ import android.net.http.AndroidHttpClient;
import android.os.FileUtils;
import android.os.PowerManager;
import android.os.Process;
+import android.os.SystemClock;
import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
-import android.util.Slog;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
@@ -101,6 +101,13 @@ public class DownloadThread extends Thread {
public long mBytesNotified = 0;
public long mTimeLastNotification = 0;
+ /** Historical bytes/second speed of this download. */
+ public long mSpeed;
+ /** Time when current sample started. */
+ public long mSpeedSampleStart;
+ /** Bytes transferred since current sample started. */
+ public long mSpeedSampleBytes;
+
public State(DownloadInfo info) {
mMimeType = Intent.normalizeMimeType(info.mMimeType);
mRequestUri = info.mUri;
@@ -131,6 +138,21 @@ public class DownloadThread extends Thread {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ try {
+ runInternal();
+ } finally {
+ DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
+ }
+ }
+
+ private void runInternal() {
+ // Skip when download already marked as finished; this download was
+ // probably started again while racing with UpdateThread.
+ if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId)
+ == Downloads.Impl.STATUS_SUCCESS) {
+ Log.d(TAG, "Download " + mInfo.mId + " already finished; skipping");
+ return;
+ }
State state = new State(mInfo);
AndroidHttpClient client = null;
@@ -211,7 +233,6 @@ public class DownloadThread extends Thread {
notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
state.mGotData, state.mFilename,
state.mNewUri, state.mMimeType, errorMsg);
- DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
netPolicy.unregisterListener(mPolicyListener);
@@ -330,7 +351,9 @@ public class DownloadThread extends Thread {
closeDestination(state);
if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
- Slog.d(TAG, "cleanupDestination() deleting " + state.mFilename);
+ if (Constants.LOGVV) {
+ Log.d(TAG, "cleanupDestination() deleting " + state.mFilename);
+ }
new File(state.mFilename).delete();
state.mFilename = null;
}
@@ -408,7 +431,25 @@ public class DownloadThread extends Thread {
* Report download progress through the database if necessary.
*/
private void reportProgress(State state, InnerState innerState) {
- long now = mSystemFacade.currentTimeMillis();
+ final long now = SystemClock.elapsedRealtime();
+
+ final long sampleDelta = now - state.mSpeedSampleStart;
+ if (sampleDelta > 500) {
+ final long sampleSpeed = ((state.mCurrentBytes - state.mSpeedSampleBytes) * 1000)
+ / sampleDelta;
+
+ if (state.mSpeed == 0) {
+ state.mSpeed = sampleSpeed;
+ } else {
+ state.mSpeed = ((state.mSpeed * 3) + sampleSpeed) / 4;
+ }
+
+ state.mSpeedSampleStart = now;
+ state.mSpeedSampleBytes = state.mCurrentBytes;
+
+ DownloadHandler.getInstance().setCurrentSpeed(mInfo.mId, state.mSpeed);
+ }
+
if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
ContentValues values = new ContentValues();
@@ -847,8 +888,10 @@ public class DownloadThread extends Thread {
long fileLength = f.length();
if (fileLength == 0) {
// The download hadn't actually started, we can restart from scratch
- Slog.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
- + state.mFilename);
+ if (Constants.LOGVV) {
+ Log.d(TAG, "setupDestinationFile() found fileLength=0, deleting "
+ + state.mFilename);
+ }
f.delete();
state.mFilename = null;
if (Constants.LOGV) {
@@ -857,8 +900,10 @@ public class DownloadThread extends Thread {
}
} else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
// This should've been caught upon failure
- Slog.d(TAG, "setupDestinationFile() unable to resume download, deleting "
- + state.mFilename);
+ if (Constants.LOGVV) {
+ Log.d(TAG, "setupDestinationFile() unable to resume download, deleting "
+ + state.mFilename);
+ }
f.delete();
throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
"Trying to resume a download that can't be resumed");
diff --git a/src/com/android/providers/downloads/OpenHelper.java b/src/com/android/providers/downloads/OpenHelper.java
new file mode 100644
index 00000000..7eca95c9
--- /dev/null
+++ b/src/com/android/providers/downloads/OpenHelper.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2012 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.app.DownloadManager.COLUMN_LOCAL_FILENAME;
+import static android.app.DownloadManager.COLUMN_LOCAL_URI;
+import static android.app.DownloadManager.COLUMN_MEDIA_TYPE;
+import static android.app.DownloadManager.COLUMN_URI;
+import static android.provider.Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI;
+
+import android.app.DownloadManager;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Downloads.Impl.RequestHeaders;
+
+public class OpenHelper {
+ /**
+ * Build an {@link Intent} to view the download at current {@link Cursor}
+ * position, handling subtleties around installing packages.
+ */
+ public static Intent buildViewIntent(Context context, long id) {
+ final DownloadManager downManager = (DownloadManager) context.getSystemService(
+ Context.DOWNLOAD_SERVICE);
+ downManager.setAccessAllDownloads(true);
+
+ final Cursor cursor = downManager.query(new DownloadManager.Query().setFilterById(id));
+ try {
+ if (!cursor.moveToFirst()) {
+ throw new IllegalArgumentException("Missing download " + id);
+ }
+
+ final Uri localUri = getCursorUri(cursor, COLUMN_LOCAL_URI);
+ final String filename = getCursorString(cursor, COLUMN_LOCAL_FILENAME);
+ String mimeType = getCursorString(cursor, COLUMN_MEDIA_TYPE);
+ mimeType = DownloadDrmHelper.getOriginalMimeType(context, filename, mimeType);
+
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ if ("application/vnd.android.package-archive".equals(mimeType)) {
+ // PackageInstaller doesn't like content URIs, so open file
+ intent.setDataAndType(localUri, mimeType);
+
+ // Also splice in details about where it came from
+ final Uri remoteUri = getCursorUri(cursor, COLUMN_URI);
+ intent.putExtra(Intent.EXTRA_ORIGINATING_URI, remoteUri);
+ intent.putExtra(Intent.EXTRA_REFERRER, getRefererUri(context, id));
+ intent.putExtra(Intent.EXTRA_ORIGINATING_UID, getOriginatingUid(context, id));
+ } else if ("file".equals(localUri.getScheme())) {
+ intent.setDataAndType(
+ ContentUris.withAppendedId(ALL_DOWNLOADS_CONTENT_URI, id), mimeType);
+ } else {
+ intent.setDataAndType(localUri, mimeType);
+ }
+
+ return intent;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private static Uri getRefererUri(Context context, long id) {
+ final Uri headersUri = Uri.withAppendedPath(
+ ContentUris.withAppendedId(ALL_DOWNLOADS_CONTENT_URI, id),
+ RequestHeaders.URI_SEGMENT);
+ final Cursor headers = context.getContentResolver()
+ .query(headersUri, null, null, null, null);
+ try {
+ while (headers.moveToNext()) {
+ final String header = getCursorString(headers, RequestHeaders.COLUMN_HEADER);
+ if ("Referer".equalsIgnoreCase(header)) {
+ return getCursorUri(headers, RequestHeaders.COLUMN_VALUE);
+ }
+ }
+ } finally {
+ headers.close();
+ }
+ return null;
+ }
+
+ private static int getOriginatingUid(Context context, long id) {
+ final Uri uri = ContentUris.withAppendedId(ALL_DOWNLOADS_CONTENT_URI, id);
+ final Cursor cursor = context.getContentResolver().query(uri, new String[]{Constants.UID},
+ null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ return cursor.getInt(cursor.getColumnIndexOrThrow(Constants.UID));
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ return -1;
+ }
+
+ private static String getCursorString(Cursor cursor, String column) {
+ return cursor.getString(cursor.getColumnIndexOrThrow(column));
+ }
+
+ private static Uri getCursorUri(Cursor cursor, String column) {
+ return Uri.parse(getCursorString(cursor, column));
+ }
+
+ private static long getCursorLong(Cursor cursor, String column) {
+ return cursor.getLong(cursor.getColumnIndexOrThrow(column));
+ }
+}
diff --git a/src/com/android/providers/downloads/RealSystemFacade.java b/src/com/android/providers/downloads/RealSystemFacade.java
index 6580f909..228c7165 100644
--- a/src/com/android/providers/downloads/RealSystemFacade.java
+++ b/src/com/android/providers/downloads/RealSystemFacade.java
@@ -1,26 +1,35 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.providers.downloads;
import android.app.DownloadManager;
-import android.app.Notification;
-import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
-import android.provider.Settings;
-import android.provider.Settings.SettingNotFoundException;
import android.telephony.TelephonyManager;
import android.util.Log;
class RealSystemFacade implements SystemFacade {
private Context mContext;
- private NotificationManager mNotificationManager;
public RealSystemFacade(Context context) {
mContext = context;
- mNotificationManager = (NotificationManager)
- mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
public long currentTimeMillis() {
@@ -85,26 +94,6 @@ class RealSystemFacade implements SystemFacade {
}
@Override
- public void postNotification(long id, Notification notification) {
- /**
- * TODO: The system notification manager takes ints, not longs, as IDs, but the download
- * manager uses IDs take straight from the database, which are longs. This will have to be
- * dealt with at some point.
- */
- mNotificationManager.notify((int) id, notification);
- }
-
- @Override
- public void cancelNotification(long id) {
- mNotificationManager.cancel((int) id);
- }
-
- @Override
- public void cancelAllNotifications() {
- mNotificationManager.cancelAll();
- }
-
- @Override
public void startThread(Thread thread) {
thread.start();
}
diff --git a/src/com/android/providers/downloads/StorageManager.java b/src/com/android/providers/downloads/StorageManager.java
index 4b51921f..915d141b 100644
--- a/src/com/android/providers/downloads/StorageManager.java
+++ b/src/com/android/providers/downloads/StorageManager.java
@@ -30,7 +30,6 @@ import android.os.StatFs;
import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
-import android.util.Slog;
import com.android.internal.R;
@@ -342,9 +341,9 @@ class StorageManager {
if (TextUtils.isEmpty(data)) continue;
File file = new File(data);
- if (true || Constants.LOGV) {
- Slog.d(Constants.TAG, "purging " + file.getAbsolutePath() + " for " +
- file.length() + " bytes");
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "purging " + file.getAbsolutePath() + " for "
+ + file.length() + " bytes");
}
totalFreed += file.length();
file.delete();
@@ -416,7 +415,9 @@ class StorageManager {
try {
final StructStat stat = Libcore.os.stat(path);
if (stat.st_uid == myUid) {
- Slog.d(TAG, "deleting spurious file " + path);
+ if (Constants.LOGVV) {
+ Log.d(TAG, "deleting spurious file " + path);
+ }
file.delete();
}
} catch (ErrnoException e) {
diff --git a/src/com/android/providers/downloads/SystemFacade.java b/src/com/android/providers/downloads/SystemFacade.java
index d1439354..fda97e08 100644
--- a/src/com/android/providers/downloads/SystemFacade.java
+++ b/src/com/android/providers/downloads/SystemFacade.java
@@ -1,12 +1,25 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.providers.downloads;
-import android.app.Notification;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.NetworkInfo;
-
interface SystemFacade {
/**
* @see System#currentTimeMillis()
@@ -50,21 +63,6 @@ interface SystemFacade {
public boolean userOwnsPackage(int uid, String pckg) throws NameNotFoundException;
/**
- * Post a system notification to the NotificationManager.
- */
- public void postNotification(long id, Notification notification);
-
- /**
- * Cancel a system notification.
- */
- public void cancelNotification(long id);
-
- /**
- * Cancel all system notifications.
- */
- public void cancelAllNotifications();
-
- /**
* Start a thread.
*/
public void startThread(Thread thread);