1a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey/*
2a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * Copyright (C) 2012 The Android Open Source Project
3a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey *
4a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * Licensed under the Apache License, Version 2.0 (the "License");
5a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * you may not use this file except in compliance with the License.
6a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * You may obtain a copy of the License at
7a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey *
8a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey *      http://www.apache.org/licenses/LICENSE-2.0
9a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey *
10a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * Unless required by applicable law or agreed to in writing, software
11a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * distributed under the License is distributed on an "AS IS" BASIS,
12a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * See the License for the specific language governing permissions and
14a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * limitations under the License.
15a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey */
16a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
17a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeypackage com.android.providers.downloads;
18a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
19a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport static android.app.DownloadManager.Request.VISIBILITY_VISIBLE;
20a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
21a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION;
222a34a9359dc199e98a45275a22027b5c367933a6Jeff Sharkeyimport static android.provider.Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
231ad10ce731d1b54692d7d5ee32601e965f503fa4Jeff Sharkeyimport static android.provider.Downloads.Impl.STATUS_RUNNING;
243a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey
25703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkeyimport static com.android.providers.downloads.Constants.TAG;
26a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
27a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.app.DownloadManager;
28a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.app.Notification;
299b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkeyimport android.app.NotificationChannel;
30a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.app.NotificationManager;
31a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.app.PendingIntent;
32a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.content.ContentUris;
33a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.content.Context;
34a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.content.Intent;
35a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.content.res.Resources;
363a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkeyimport android.database.Cursor;
37a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.net.Uri;
38703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkeyimport android.os.SystemClock;
39a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.provider.Downloads;
4008fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkeyimport android.service.notification.StatusBarNotification;
41a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.text.TextUtils;
4252b703c5d0c4cff72bafdec0e2229368d3cc20d0Jeff Sharkeyimport android.text.format.DateUtils;
433a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkeyimport android.util.ArrayMap;
443a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkeyimport android.util.IntArray;
45703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkeyimport android.util.Log;
4638648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkeyimport android.util.LongSparseLongArray;
47a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
4808fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkeyimport com.android.internal.util.ArrayUtils;
4908fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkey
50250a1ebc4cf52edf4e55a594f05f4a351ee8e126Elliott Hughesimport java.text.NumberFormat;
51a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
52a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport javax.annotation.concurrent.GuardedBy;
53a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
54a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey/**
553a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey * Update {@link NotificationManager} to reflect current download states.
563a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey * Collapses similar downloads into a single notification, and builds
57a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * {@link PendingIntent} that launch towards {@link DownloadReceiver}.
58a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey */
59a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeypublic class DownloadNotifier {
60a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
61a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static final int TYPE_ACTIVE = 1;
62a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static final int TYPE_WAITING = 2;
63a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static final int TYPE_COMPLETE = 3;
64a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
659b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey    private static final String CHANNEL_ACTIVE = "active";
669b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey    private static final String CHANNEL_WAITING = "waiting";
679b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey    private static final String CHANNEL_COMPLETE = "complete";
689b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey
69a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private final Context mContext;
70a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private final NotificationManager mNotifManager;
71a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
72a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    /**
73a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * Currently active notifications, mapped from clustering tag to timestamp
74a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * when first shown.
75a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     *
763a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey     * @see #buildNotificationTag(Cursor)
77a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     */
78a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    @GuardedBy("mActiveNotifs")
793a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    private final ArrayMap<String, Long> mActiveNotifs = new ArrayMap<>();
80a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
8138648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey    /**
823a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey     * Current speed of active downloads, mapped from download ID to speed in
833a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey     * bytes per second.
8438648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey     */
8538648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey    @GuardedBy("mDownloadSpeed")
8638648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey    private final LongSparseLongArray mDownloadSpeed = new LongSparseLongArray();
8738648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey
88703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey    /**
893a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey     * Last time speed was reproted, mapped from download ID to
90703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey     * {@link SystemClock#elapsedRealtime()}.
91703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey     */
92703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey    @GuardedBy("mDownloadSpeed")
93703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey    private final LongSparseLongArray mDownloadTouch = new LongSparseLongArray();
94703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey
95a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    public DownloadNotifier(Context context) {
96a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        mContext = context;
979b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey        mNotifManager = context.getSystemService(NotificationManager.class);
989b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey
999b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey        // Ensure that all our channels are ready to use
1009b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey        mNotifManager.createNotificationChannel(new NotificationChannel(CHANNEL_ACTIVE,
1019b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                context.getText(R.string.download_running),
102b0a086c29f9fe63ced4524b899c2ca2040fce328Geoffrey Pitsch                NotificationManager.IMPORTANCE_MIN));
1039b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey        mNotifManager.createNotificationChannel(new NotificationChannel(CHANNEL_WAITING,
1049b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                context.getText(R.string.download_queued),
1059b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                NotificationManager.IMPORTANCE_DEFAULT));
1069b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey        mNotifManager.createNotificationChannel(new NotificationChannel(CHANNEL_COMPLETE,
1079b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                context.getText(com.android.internal.R.string.done_label),
1089b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                NotificationManager.IMPORTANCE_DEFAULT));
109a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
110a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
11108fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkey    public void init() {
11208fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkey        synchronized (mActiveNotifs) {
11308fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkey            mActiveNotifs.clear();
11408fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkey            final StatusBarNotification[] notifs = mNotifManager.getActiveNotifications();
11508fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkey            if (!ArrayUtils.isEmpty(notifs)) {
11608fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkey                for (StatusBarNotification notif : notifs) {
11708fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkey                    mActiveNotifs.put(notif.getTag(), notif.getPostTime());
11808fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkey                }
11908fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkey            }
12008fa91badf0b34323c535dde6f8113e16ae94e21Jeff Sharkey        }
121b0bb182a1b52b31ccf1ec5e0be82308ebb4857e2Jeff Sharkey    }
122b0bb182a1b52b31ccf1ec5e0be82308ebb4857e2Jeff Sharkey
123a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    /**
124703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey     * Notify the current speed of an active download, used for calculating
12538648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey     * estimated remaining time.
12638648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey     */
12738648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey    public void notifyDownloadSpeed(long id, long bytesPerSecond) {
12838648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey        synchronized (mDownloadSpeed) {
12938648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey            if (bytesPerSecond != 0) {
13038648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                mDownloadSpeed.put(id, bytesPerSecond);
131703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                mDownloadTouch.put(id, SystemClock.elapsedRealtime());
13238648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey            } else {
13338648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                mDownloadSpeed.delete(id);
134703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                mDownloadTouch.delete(id);
13538648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey            }
13638648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey        }
13738648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey    }
13838648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey
1393a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    private interface UpdateQuery {
1403a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final String[] PROJECTION = new String[] {
1413a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                Downloads.Impl._ID,
1423a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                Downloads.Impl.COLUMN_STATUS,
1433a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                Downloads.Impl.COLUMN_VISIBILITY,
1443a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
1453a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                Downloads.Impl.COLUMN_CURRENT_BYTES,
1463a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                Downloads.Impl.COLUMN_TOTAL_BYTES,
1473a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                Downloads.Impl.COLUMN_DESTINATION,
1483a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                Downloads.Impl.COLUMN_TITLE,
1493a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                Downloads.Impl.COLUMN_DESCRIPTION,
1503a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        };
1513a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey
1523a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final int _ID = 0;
1533a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final int STATUS = 1;
1543a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final int VISIBILITY = 2;
1553a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final int NOTIFICATION_PACKAGE = 3;
1563a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final int CURRENT_BYTES = 4;
1573a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final int TOTAL_BYTES = 5;
1583a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final int DESTINATION = 6;
1593a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final int TITLE = 7;
1603a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final int DESCRIPTION = 8;
161a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
162a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
1633a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    public void update() {
1643a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        try (Cursor cursor = mContext.getContentResolver().query(
1653a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, UpdateQuery.PROJECTION,
1663a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                Downloads.Impl.COLUMN_DELETED + " == '0'", null, null)) {
1673a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            synchronized (mActiveNotifs) {
1683a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                updateWithLocked(cursor);
1693a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            }
1706971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg        }
1716971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg    }
1726971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg
1733a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    private void updateWithLocked(Cursor cursor) {
174a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        final Resources res = mContext.getResources();
175a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
176a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        // Cluster downloads together
1773a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final ArrayMap<String, IntArray> clustered = new ArrayMap<>();
1783a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        while (cursor.moveToNext()) {
1793a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            final String tag = buildNotificationTag(cursor);
180a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (tag != null) {
1813a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                IntArray cluster = clustered.get(tag);
1823a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                if (cluster == null) {
1833a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                    cluster = new IntArray();
1843a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                    clustered.put(tag, cluster);
1853a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                }
1863a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                cluster.add(cursor.getPosition());
187a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
188a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
189a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
190a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        // Build notification for each cluster
1913a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        for (int i = 0; i < clustered.size(); i++) {
1923a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            final String tag = clustered.keyAt(i);
1933a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            final IntArray cluster = clustered.valueAt(i);
194a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            final int type = getNotificationTagType(tag);
1956971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg
1969b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey            final Notification.Builder builder;
1979b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey            if (type == TYPE_ACTIVE) {
1989b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                builder = new Notification.Builder(mContext, CHANNEL_ACTIVE);
1999b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                builder.setSmallIcon(android.R.drawable.stat_sys_download);
2009b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey            } else if (type == TYPE_WAITING) {
2019b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                builder = new Notification.Builder(mContext, CHANNEL_WAITING);
2029b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                builder.setSmallIcon(android.R.drawable.stat_sys_warning);
2039b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey            } else if (type == TYPE_COMPLETE) {
2049b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                builder = new Notification.Builder(mContext, CHANNEL_COMPLETE);
2059b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                builder.setSmallIcon(android.R.drawable.stat_sys_download_done);
2069b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey            } else {
2079b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey                continue;
2089b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey            }
2099b2f2a5f041bf56ab45e5952ae4ddc4b47d0105eJeff Sharkey
21052d35ed20edec2b5086459f1d5d64309c421f3b2Selim Cinek            builder.setColor(res.getColor(
21152d35ed20edec2b5086459f1d5d64309c421f3b2Selim Cinek                    com.android.internal.R.color.system_notification_accent_color));
212a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
213a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            // Use time when cluster was first shown to avoid shuffling
214a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            final long firstShown;
215a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (mActiveNotifs.containsKey(tag)) {
216a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                firstShown = mActiveNotifs.get(tag);
217a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            } else {
218a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                firstShown = System.currentTimeMillis();
219a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                mActiveNotifs.put(tag, firstShown);
220a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
221a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            builder.setWhen(firstShown);
2224b858ed02b773344cfedd66e516c6d365da55974Jeff Sharkey            builder.setOnlyAlertOnce(true);
223a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
224a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            // Build action intents
225a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (type == TYPE_ACTIVE || type == TYPE_WAITING) {
2263a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                final long[] downloadIds = getDownloadIds(cursor, cluster);
2276971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg
228af49093b1554fdc55977e9281505e05865f33813Danny Baumann                // build a synthetic uri for intent identification purposes
229af49093b1554fdc55977e9281505e05865f33813Danny Baumann                final Uri uri = new Uri.Builder().scheme("active-dl").appendPath(tag).build();
230a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final Intent intent = new Intent(Constants.ACTION_LIST,
231af49093b1554fdc55977e9281505e05865f33813Danny Baumann                        uri, mContext, DownloadReceiver.class);
232ae89e55bb059b37922a3745b1b45ac79a71a05beJeff Sharkey                intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
233a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
2346971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                        downloadIds);
235af49093b1554fdc55977e9281505e05865f33813Danny Baumann                builder.setContentIntent(PendingIntent.getBroadcast(mContext,
236af49093b1554fdc55977e9281505e05865f33813Danny Baumann                        0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
2377b7fa39decefca0ff83b1e3758cba11df90f94cdJeff Sharkey                if (type == TYPE_ACTIVE) {
2387b7fa39decefca0ff83b1e3758cba11df90f94cdJeff Sharkey                    builder.setOngoing(true);
2397b7fa39decefca0ff83b1e3758cba11df90f94cdJeff Sharkey                }
240a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
2416971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                // Add a Cancel action
2426971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                final Uri cancelUri = new Uri.Builder().scheme("cancel-dl").appendPath(tag).build();
2436971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                final Intent cancelIntent = new Intent(Constants.ACTION_CANCEL,
2446971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                        cancelUri, mContext, DownloadReceiver.class);
245ae89e55bb059b37922a3745b1b45ac79a71a05beJeff Sharkey                cancelIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
2466971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                cancelIntent.putExtra(DownloadReceiver.EXTRA_CANCELED_DOWNLOAD_IDS, downloadIds);
2476971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                cancelIntent.putExtra(DownloadReceiver.EXTRA_CANCELED_DOWNLOAD_NOTIFICATION_TAG, tag);
2486971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg
2496971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                builder.addAction(
2506971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                    android.R.drawable.ic_menu_close_clear_cancel,
2516971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                    res.getString(R.string.button_cancel_download),
2526971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                    PendingIntent.getBroadcast(mContext,
2536971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg                            0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT));
2546971133998ddc8c8c6b37b2fdaaec1d3ed152e90Oren Blasberg
255a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            } else if (type == TYPE_COMPLETE) {
2563a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                cursor.moveToPosition(cluster.get(0));
2573a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                final long id = cursor.getLong(UpdateQuery._ID);
2583a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                final int status = cursor.getInt(UpdateQuery.STATUS);
2593a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                final int destination = cursor.getInt(UpdateQuery.DESTINATION);
2603a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey
261a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final Uri uri = ContentUris.withAppendedId(
2623a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                        Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id);
26364a9e6cb3628db724e04492cf207757348960eb7Jeff Sharkey                builder.setAutoCancel(true);
264a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
265a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final String action;
2663a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                if (Downloads.Impl.isStatusError(status)) {
267a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    action = Constants.ACTION_LIST;
268a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                } else {
269d635ac295850ba23d528c02b1e5c6eb44b64b22bJeff Sharkey                    action = Constants.ACTION_OPEN;
270a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
271a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
272a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final Intent intent = new Intent(action, uri, mContext, DownloadReceiver.class);
273ae89e55bb059b37922a3745b1b45ac79a71a05beJeff Sharkey                intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
274a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
2753a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                        getDownloadIds(cursor, cluster));
276af49093b1554fdc55977e9281505e05865f33813Danny Baumann                builder.setContentIntent(PendingIntent.getBroadcast(mContext,
277af49093b1554fdc55977e9281505e05865f33813Danny Baumann                        0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
278a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
279a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final Intent hideIntent = new Intent(Constants.ACTION_HIDE,
280a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        uri, mContext, DownloadReceiver.class);
281ae89e55bb059b37922a3745b1b45ac79a71a05beJeff Sharkey                hideIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
282a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                builder.setDeleteIntent(PendingIntent.getBroadcast(mContext, 0, hideIntent, 0));
283a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
284a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
285a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            // Calculate and show progress
286a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            String remainingText = null;
287a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            String percentText = null;
288a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (type == TYPE_ACTIVE) {
289a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                long current = 0;
290a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                long total = 0;
291fec5f50a85e1bfc7bb4fa12d04ffa7526c79fad7Jeff Sharkey                long speed = 0;
29238648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                synchronized (mDownloadSpeed) {
2933a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                    for (int j = 0; j < cluster.size(); j++) {
2943a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                        cursor.moveToPosition(cluster.get(j));
2953a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey
2963a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                        final long id = cursor.getLong(UpdateQuery._ID);
2973a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                        final long currentBytes = cursor.getLong(UpdateQuery.CURRENT_BYTES);
2983a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                        final long totalBytes = cursor.getLong(UpdateQuery.TOTAL_BYTES);
2993a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey
3003a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                        if (totalBytes != -1) {
3013a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                            current += currentBytes;
3023a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                            total += totalBytes;
3033a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                            speed += mDownloadSpeed.get(id);
30438648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                        }
305a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    }
306a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
307a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
308a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                if (total > 0) {
309250a1ebc4cf52edf4e55a594f05f4a351ee8e126Elliott Hughes                    percentText =
310250a1ebc4cf52edf4e55a594f05f4a351ee8e126Elliott Hughes                            NumberFormat.getPercentInstance().format((double) current / total);
311fec5f50a85e1bfc7bb4fa12d04ffa7526c79fad7Jeff Sharkey
312fec5f50a85e1bfc7bb4fa12d04ffa7526c79fad7Jeff Sharkey                    if (speed > 0) {
313fec5f50a85e1bfc7bb4fa12d04ffa7526c79fad7Jeff Sharkey                        final long remainingMillis = ((total - current) * 1000) / speed;
31452b703c5d0c4cff72bafdec0e2229368d3cc20d0Jeff Sharkey                        remainingText = res.getString(R.string.download_remaining,
31552b703c5d0c4cff72bafdec0e2229368d3cc20d0Jeff Sharkey                                DateUtils.formatDuration(remainingMillis));
31652b703c5d0c4cff72bafdec0e2229368d3cc20d0Jeff Sharkey                    }
317a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
318250a1ebc4cf52edf4e55a594f05f4a351ee8e126Elliott Hughes                    final int percent = (int) ((current * 100) / total);
319a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setProgress(100, percent, false);
320a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                } else {
321a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setProgress(100, 0, true);
322a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
323a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
324a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
325a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            // Build titles and description
326a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            final Notification notif;
327a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (cluster.size() == 1) {
3283a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                cursor.moveToPosition(cluster.get(0));
3293a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                builder.setContentTitle(getDownloadTitle(res, cursor));
330a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
331a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                if (type == TYPE_ACTIVE) {
3323a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                    final String description = cursor.getString(UpdateQuery.DESCRIPTION);
3333a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                    if (!TextUtils.isEmpty(description)) {
3343a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                        builder.setContentText(description);
335a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    } else {
336a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        builder.setContentText(remainingText);
337a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    }
338a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentInfo(percentText);
339a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
340a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                } else if (type == TYPE_WAITING) {
341a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentText(
342a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                            res.getString(R.string.notification_need_wifi_for_size));
343a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
344a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                } else if (type == TYPE_COMPLETE) {
3453a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                    final int status = cursor.getInt(UpdateQuery.STATUS);
3463a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                    if (Downloads.Impl.isStatusError(status)) {
347a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        builder.setContentText(res.getText(R.string.notification_download_failed));
3483a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                    } else if (Downloads.Impl.isStatusSuccess(status)) {
349a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        builder.setContentText(
350a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                                res.getText(R.string.notification_download_complete));
351a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    }
352a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
353a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
354a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                notif = builder.build();
355a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
356a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            } else {
357a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final Notification.InboxStyle inboxStyle = new Notification.InboxStyle(builder);
358a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
3593a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                for (int j = 0; j < cluster.size(); j++) {
3603a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                    cursor.moveToPosition(cluster.get(j));
3613a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                    inboxStyle.addLine(getDownloadTitle(res, cursor));
362a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
363a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
364a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                if (type == TYPE_ACTIVE) {
365a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentTitle(res.getQuantityString(
366a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                            R.plurals.notif_summary_active, cluster.size(), cluster.size()));
367a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentText(remainingText);
368a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentInfo(percentText);
369a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    inboxStyle.setSummaryText(remainingText);
370a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
371a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                } else if (type == TYPE_WAITING) {
372a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentTitle(res.getQuantityString(
373a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                            R.plurals.notif_summary_waiting, cluster.size(), cluster.size()));
374a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentText(
375a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                            res.getString(R.string.notification_need_wifi_for_size));
376a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    inboxStyle.setSummaryText(
377a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                            res.getString(R.string.notification_need_wifi_for_size));
378a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
379a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
380a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                notif = inboxStyle.build();
381a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
382a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
383a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            mNotifManager.notify(tag, 0, notif);
384a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
385a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
386a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        // Remove stale tags that weren't renewed
3873a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        for (int i = 0; i < mActiveNotifs.size();) {
3883a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            final String tag = mActiveNotifs.keyAt(i);
3893a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            if (clustered.containsKey(tag)) {
3903a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                i++;
3913a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            } else {
392a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                mNotifManager.cancel(tag, 0);
3933a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                mActiveNotifs.removeAt(i);
394a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
395a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
396a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
397a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
3983a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    private static CharSequence getDownloadTitle(Resources res, Cursor cursor) {
3993a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final String title = cursor.getString(UpdateQuery.TITLE);
4003a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        if (!TextUtils.isEmpty(title)) {
4013a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            return title;
402a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        } else {
403a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            return res.getString(R.string.download_unknown_title);
404a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
405a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
406a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
4073a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    private long[] getDownloadIds(Cursor cursor, IntArray cluster) {
4083a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final long[] ids = new long[cluster.size()];
4093a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        for (int i = 0; i < cluster.size(); i++) {
4103a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            cursor.moveToPosition(cluster.get(i));
4113a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            ids[i] = cursor.getLong(UpdateQuery._ID);
412a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
413a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        return ids;
414a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
415a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
416703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey    public void dumpSpeeds() {
417703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey        synchronized (mDownloadSpeed) {
418703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey            for (int i = 0; i < mDownloadSpeed.size(); i++) {
419703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                final long id = mDownloadSpeed.keyAt(i);
420703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                final long delta = SystemClock.elapsedRealtime() - mDownloadTouch.get(id);
421703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                Log.d(TAG, "Download " + id + " speed " + mDownloadSpeed.valueAt(i) + "bps, "
422703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                        + delta + "ms ago");
423703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey            }
424703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey        }
425703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey    }
426703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey
427a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    /**
4283a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey     * Build tag used for collapsing several downloads into a single
429a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * {@link Notification}.
430a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     */
4313a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    private static String buildNotificationTag(Cursor cursor) {
4323a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final long id = cursor.getLong(UpdateQuery._ID);
4333a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final int status = cursor.getInt(UpdateQuery.STATUS);
4343a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final int visibility = cursor.getInt(UpdateQuery.VISIBILITY);
4353a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        final String notifPackage = cursor.getString(UpdateQuery.NOTIFICATION_PACKAGE);
4363a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey
4372a34a9359dc199e98a45275a22027b5c367933a6Jeff Sharkey        if (isQueuedAndVisible(status, visibility)) {
4383a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            return TYPE_WAITING + ":" + notifPackage;
4393a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        } else if (isActiveAndVisible(status, visibility)) {
4403a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            return TYPE_ACTIVE + ":" + notifPackage;
4413a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        } else if (isCompleteAndVisible(status, visibility)) {
442a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            // Complete downloads always have unique notifs
4433a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey            return TYPE_COMPLETE + ":" + id;
444a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        } else {
445a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            return null;
446a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
447a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
448a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
449a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    /**
450a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * Return the cluster type of the given tag, as created by
4513a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey     * {@link #buildNotificationTag(Cursor)}.
452a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     */
453a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static int getNotificationTagType(String tag) {
454a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        return Integer.parseInt(tag.substring(0, tag.indexOf(':')));
455a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
456a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
4572a34a9359dc199e98a45275a22027b5c367933a6Jeff Sharkey    private static boolean isQueuedAndVisible(int status, int visibility) {
4582a34a9359dc199e98a45275a22027b5c367933a6Jeff Sharkey        return status == STATUS_QUEUED_FOR_WIFI &&
4592a34a9359dc199e98a45275a22027b5c367933a6Jeff Sharkey                (visibility == VISIBILITY_VISIBLE
4602a34a9359dc199e98a45275a22027b5c367933a6Jeff Sharkey                || visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
4612a34a9359dc199e98a45275a22027b5c367933a6Jeff Sharkey    }
4622a34a9359dc199e98a45275a22027b5c367933a6Jeff Sharkey
4633a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    private static boolean isActiveAndVisible(int status, int visibility) {
4643a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        return status == STATUS_RUNNING &&
4653a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                (visibility == VISIBILITY_VISIBLE
4663a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                || visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
467a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
468a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
4693a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey    private static boolean isCompleteAndVisible(int status, int visibility) {
4703a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey        return Downloads.Impl.isStatusCompleted(status) &&
4713a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                (visibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED
4723a5f5eafb34eaa4963c801882148e8f61514a61bJeff Sharkey                || visibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
473a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
474a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey}
475