diff options
Diffstat (limited to 'src')
5 files changed, 426 insertions, 396 deletions
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); } |