diff options
Diffstat (limited to 'src')
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); |