summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--res/values/strings.xml34
-rw-r--r--src/com/android/providers/downloads/DownloadNotification.java288
-rw-r--r--src/com/android/providers/downloads/DownloadNotifier.java306
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java11
-rw-r--r--src/com/android/providers/downloads/DownloadReceiver.java194
-rw-r--r--src/com/android/providers/downloads/DownloadService.java23
-rw-r--r--tests/src/com/android/providers/downloads/FakeSystemFacade.java5
-rw-r--r--tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java13
8 files changed, 455 insertions, 419 deletions
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 56352951..c607e35f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -116,23 +116,6 @@
for a download that doesn't have a title associated with it. -->
<string name="download_unknown_title">&lt;Untitled&gt;</string>
- <!-- When there are multiple simultaneous outstanding downloads from a
- single application, they are displayed as a single notification,
- and the expanded notification view displays the first two download
- names separated with this string, i.e. "[title], [title]"
- or "[title], [title] and [n] more". This is the comma + space
- that separates the first two titles, and it's used both when there
- are exactly two and more than two titles. -->
- <string name="notification_filename_separator">", "</string>
-
- <!-- When there are three or more simultaneous outstanding downloads from a
- single application, they are displayed as a single notification,
- and the expanded notification view uses this string to indicate
- downloads beyond the first two, i.e. "[title], [title] and [n] more".
- This is the " and [n] more" part, including the leading space, and it's
- used regardless of the number of additional downloads. -->
- <string name="notification_filename_extras">" and <xliff:g id="number" example="27">%d</xliff:g> more"</string>
-
<!-- When a download completes, a notification is displayed, and this
string is used to indicate that the download successfully completed.
Note that such a download could have been initiated by a variety of
@@ -210,4 +193,21 @@
<!-- Short representation of download progress percentage. [CHAR LIMIT=8] -->
<string name="download_percent"><xliff:g id="number">%d</xliff:g><xliff:g id="percent">%%</xliff:g></string>
+ <!-- Title summarizing active downloads. [CHAR LIMIT=32] -->
+ <plurals name="notif_summary_active">
+ <item quantity="one">1 file downloading</item>
+ <item quantity="other"><xliff:g id="number">%d</xliff:g> files downloading</item>
+ </plurals>
+
+ <!-- Title summarizing waiting downloads. [CHAR LIMIT=32] -->
+ <plurals name="notif_summary_waiting">
+ <item quantity="one">1 file waiting</item>
+ <item quantity="other"><xliff:g id="number">%d</xliff:g> files waiting</item>
+ </plurals>
+
+ <!-- Text for a toast appearing when a user clicks on a completed download, informing the user
+ that there is no application on the device that can open the file that was downloaded
+ [CHAR LIMIT=200] -->
+ <string name="download_no_application_title">Can\'t open file</string>
+
</resources>
diff --git a/src/com/android/providers/downloads/DownloadNotification.java b/src/com/android/providers/downloads/DownloadNotification.java
deleted file mode 100644
index f5778e79..00000000
--- a/src/com/android/providers/downloads/DownloadNotification.java
+++ /dev/null
@@ -1,288 +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.NotificationManager;
-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 {
-
- private Context mContext;
- private NotificationManager mNotifManager;
- private HashMap<String, NotificationItem> mNotifications;
-
- /** 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;
- mNotifManager = (NotificationManager) mContext.getSystemService(
- Context.NOTIFICATION_SERVICE);
- 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));
-
- mNotifManager.notify(item.mId, builder.build());
- }
- }
-
- 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));
-
- mNotifManager.notify((int) id, builder.build());
- }
-
- 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..a1805e5e
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadNotifier.java
@@ -0,0 +1,306 @@
+/*
+ * 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 com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+
+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);
+ }
+
+ /**
+ * 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) {
+ long current = 0;
+ long total = 0;
+ for (DownloadInfo info : cluster) {
+ if (info.mTotalBytes != -1) {
+ current += info.mCurrentBytes;
+ total += info.mTotalBytes;
+ }
+ }
+
+ if (total > 0) {
+ final int percent = (int) ((current * 100) / total);
+ // TODO: calculate remaining time based on recent bandwidth
+ percentText = res.getString(R.string.download_percent, percent);
+
+ 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 7469508d..cbc963ce 100644
--- a/src/com/android/providers/downloads/DownloadReceiver.java
+++ b/src/com/android/providers/downloads/DownloadReceiver.java
@@ -16,8 +16,10 @@
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.app.NotificationManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
@@ -28,9 +30,12 @@ import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
-import android.provider.BaseColumns;
+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;
@@ -38,11 +43,21 @@ import com.google.common.annotations.VisibleForTesting;
* 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,58 +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) {
- final NotificationManager notifManager = (NotificationManager) context.getSystemService(
- Context.NOTIFICATION_SERVICE);
- notifManager.cancel((int) 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);
@@ -139,69 +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) {
- final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BaseColumns._ID));
+ private void openDownload(Context context, long id) {
final Intent intent = OpenHelper.buildViewIntent(context, id);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(intent);
} catch (ActivityNotFoundException 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 8380830a..0a16a7d1 100644
--- a/src/com/android/providers/downloads/DownloadService.java
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -19,7 +19,6 @@ package com.android.providers.downloads;
import static com.android.providers.downloads.Constants.TAG;
import android.app.AlarmManager;
-import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
@@ -65,8 +64,7 @@ public class DownloadService extends Service {
private DownloadManagerContentObserver mObserver;
/** Class to handle Notification Manager updates */
- private DownloadNotification mNotifier;
- private NotificationManager mNotifManager;
+ private DownloadNotifier mNotifier;
/**
* The Service's view of the list of downloads, mapping download IDs to the corresponding info
@@ -222,9 +220,7 @@ public class DownloadService extends Service {
mMediaScannerConnecting = false;
mMediaScannerConnection = new MediaScannerConnection();
- mNotifier = new DownloadNotification(this, mSystemFacade);
- mNotifManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- mNotifManager.cancelAll();
+ mNotifier = new DownloadNotifier(this);
mStorageManager = StorageManager.getInstance(getApplicationContext());
updateFromProvider();
@@ -359,7 +355,7 @@ public class DownloadService extends Service {
}
}
}
- mNotifier.updateNotification(mDownloads.values());
+ mNotifier.updateWith(mDownloads.values());
if (mustScan) {
bindMediaScanner();
} else {
@@ -459,18 +455,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) {
- mNotifManager.cancel((int) info.mId);
- }
-
info.startIfReady(now, mStorageManager);
}
@@ -488,7 +472,6 @@ public class DownloadService extends Service {
}
new File(info.mFileName).delete();
}
- mNotifManager.cancel((int) info.mId);
mDownloads.remove(info.mId);
}
diff --git a/tests/src/com/android/providers/downloads/FakeSystemFacade.java b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
index 6898efdb..481b5cba 100644
--- a/tests/src/com/android/providers/downloads/FakeSystemFacade.java
+++ b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
@@ -4,6 +4,7 @@ import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
import java.util.ArrayList;
import java.util.LinkedList;
@@ -36,7 +37,9 @@ public class FakeSystemFacade implements SystemFacade {
if (mActiveNetworkType == null) {
return null;
} else {
- return new NetworkInfo(mActiveNetworkType, 0, null, null);
+ final NetworkInfo info = new NetworkInfo(mActiveNetworkType, 0, null, null);
+ info.setDetailedState(DetailedState.CONNECTED, null, null);
+ return info;
}
}
diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
index 34a69df9..2661a1f2 100644
--- a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
@@ -17,6 +17,7 @@
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;
@@ -449,6 +450,8 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
receiver.mSystemFacade = mSystemFacade;
Intent intent = new Intent(Constants.ACTION_LIST);
intent.setData(Uri.parse(Downloads.Impl.CONTENT_URI + "/" + download.mId));
+ intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
+ new long[] { download.mId });
receiver.onReceive(mContext, intent);
assertEquals(1, mSystemFacade.mBroadcastsSent.size());
@@ -523,7 +526,7 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
runService();
- verify(mNotifManager, never()).notify(anyInt(), isA(Notification.class));
+ verify(mNotifManager, never()).notify(anyString(), anyInt(), isA(Notification.class));
// TODO: verify that it never cancels
}
@@ -536,8 +539,8 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
runService();
// TODO: verify different notif types with tags
- verify(mNotifManager, atLeastOnce()).notify(anyInt(), isA(Notification.class));
- verify(mNotifManager, times(1)).cancel(anyInt());
+ verify(mNotifManager, atLeastOnce()).notify(anyString(), anyInt(), isA(Notification.class));
+ verify(mNotifManager, times(1)).cancel(anyString(), anyInt());
}
public void testNotificationVisibleComplete() throws Exception {
@@ -549,8 +552,8 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
runService();
// TODO: verify different notif types with tags
- verify(mNotifManager, atLeastOnce()).notify(anyInt(), isA(Notification.class));
- verify(mNotifManager, times(1)).cancel(anyInt());
+ verify(mNotifManager, atLeastOnce()).notify(anyString(), anyInt(), isA(Notification.class));
+ verify(mNotifManager, times(1)).cancel(anyString(), anyInt());
}
public void testRetryAfter() throws Exception {