BluetoothOppNotification.java revision 7a4cc5c2f6ec06e8dc6f930680ce9aceff6df359
1/*
2 * Copyright (c) 2008-2009, Motorola, Inc.
3 *
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
8 *
9 * - Redistributions of source code must retain the above copyright notice,
10 * this list of conditions and the following disclaimer.
11 *
12 * - Redistributions in binary form must reproduce the above copyright notice,
13 * this list of conditions and the following disclaimer in the documentation
14 * and/or other materials provided with the distribution.
15 *
16 * - Neither the name of the Motorola, Inc. nor the names of its contributors
17 * may be used to endorse or promote products derived from this software
18 * without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30 * POSSIBILITY OF SUCH DAMAGE.
31 */
32
33package com.android.bluetooth.opp;
34
35import com.android.bluetooth.R;
36
37import android.content.Context;
38import android.app.Notification;
39import android.app.NotificationManager;
40import android.app.PendingIntent;
41import android.content.Intent;
42import android.database.Cursor;
43import android.net.Uri;
44import android.util.Log;
45import android.os.Handler;
46import android.os.Message;
47import android.os.Process;
48import java.util.HashMap;
49
50/**
51 * This class handles the updating of the Notification Manager for the cases
52 * where there is an ongoing transfer, incoming transfer need confirm and
53 * complete (successful or failed) transfer.
54 */
55class BluetoothOppNotification {
56    private static final String TAG = "BluetoothOppNotification";
57    private static final boolean V = Constants.VERBOSE;
58
59    static final String status = "(" + BluetoothShare.STATUS + " == '192'" + ")";
60
61    static final String visible = "(" + BluetoothShare.VISIBILITY + " IS NULL OR "
62            + BluetoothShare.VISIBILITY + " == '" + BluetoothShare.VISIBILITY_VISIBLE + "'" + ")";
63
64    static final String confirm = "(" + BluetoothShare.USER_CONFIRMATION + " == '"
65            + BluetoothShare.USER_CONFIRMATION_CONFIRMED + "' OR "
66            + BluetoothShare.USER_CONFIRMATION + " == '"
67            + BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED  + "' OR "
68            + BluetoothShare.USER_CONFIRMATION + " == '"
69            + BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED + "'" + ")";
70
71    static final String not_through_handover = "(" + BluetoothShare.USER_CONFIRMATION + " != '"
72            + BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED + "'" + ")";
73
74    static final String WHERE_RUNNING = status + " AND " + visible + " AND " + confirm;
75
76    static final String WHERE_COMPLETED = BluetoothShare.STATUS + " >= '200' AND " + visible +
77            " AND " + not_through_handover; // Don't show handover-initiated transfers
78
79    private static final String WHERE_COMPLETED_OUTBOUND = WHERE_COMPLETED + " AND " + "("
80            + BluetoothShare.DIRECTION + " == " + BluetoothShare.DIRECTION_OUTBOUND + ")";
81
82    private static final String WHERE_COMPLETED_INBOUND = WHERE_COMPLETED + " AND " + "("
83            + BluetoothShare.DIRECTION + " == " + BluetoothShare.DIRECTION_INBOUND + ")";
84
85    static final String WHERE_CONFIRM_PENDING = BluetoothShare.USER_CONFIRMATION + " == '"
86            + BluetoothShare.USER_CONFIRMATION_PENDING + "'" + " AND " + visible;
87
88    public NotificationManager mNotificationMgr;
89
90    private Context mContext;
91
92    private HashMap<String, NotificationItem> mNotifications;
93
94    private NotificationUpdateThread mUpdateNotificationThread;
95
96    private int mPendingUpdate = 0;
97
98    private static final int NOTIFICATION_ID_OUTBOUND = -1000005;
99
100    private static final int NOTIFICATION_ID_INBOUND = -1000006;
101
102    private boolean mUpdateCompleteNotification = true;
103
104    private int mActiveNotificationId = 0;
105
106    /**
107     * This inner class is used to describe some properties for one transfer.
108     */
109    static class NotificationItem {
110        int id; // This first field _id in db;
111
112        int direction; // to indicate sending or receiving
113
114        int totalCurrent = 0; // current transfer bytes
115
116        int totalTotal = 0; // total bytes for current transfer
117
118        int timeStamp = 0; // Database time stamp. Used for sorting ongoing transfers.
119
120        String description; // the text above progress bar
121
122        boolean handoverInitiated = false; // transfer initiated by connection handover (eg NFC)
123
124        String destination; // destination associated with this transfer
125    }
126
127    /**
128     * Constructor
129     *
130     * @param ctx The context to use to obtain access to the Notification
131     *            Service
132     */
133    BluetoothOppNotification(Context ctx) {
134        mContext = ctx;
135        mNotificationMgr = (NotificationManager)mContext
136                .getSystemService(Context.NOTIFICATION_SERVICE);
137        mNotifications = new HashMap<String, NotificationItem>();
138    }
139
140    /**
141     * Update the notification ui.
142     */
143    public void updateNotification() {
144        synchronized (BluetoothOppNotification.this) {
145            mPendingUpdate++;
146            if (mPendingUpdate > 1) {
147                if (V) Log.v(TAG, "update too frequent, put in queue");
148                return;
149            }
150            if (!mHandler.hasMessages(NOTIFY)) {
151                if (V) Log.v(TAG, "send message");
152                mHandler.sendMessage(mHandler.obtainMessage(NOTIFY));
153            }
154        }
155    }
156
157    private static final int NOTIFY = 0;
158    // Use 1 second timer to limit notification frequency.
159    // 1. On the first notification, create the update thread.
160    //    Buffer other updates.
161    // 2. Update thread will clear mPendingUpdate.
162    // 3. Handler sends a delayed message to self
163    // 4. Handler checks if there are any more updates after 1 second.
164    // 5. If there is an update, update it else stop.
165    private Handler mHandler = new Handler() {
166        public void handleMessage(Message msg) {
167            switch (msg.what) {
168                case NOTIFY:
169                    synchronized (BluetoothOppNotification.this) {
170                        if (mPendingUpdate > 0 && mUpdateNotificationThread == null) {
171                            if (V) Log.v(TAG, "new notify threadi!");
172                            mUpdateNotificationThread = new NotificationUpdateThread();
173                            mUpdateNotificationThread.start();
174                            if (V) Log.v(TAG, "send delay message");
175                            mHandler.sendMessageDelayed(mHandler.obtainMessage(NOTIFY), 1000);
176                        } else if (mPendingUpdate > 0) {
177                            if (V) Log.v(TAG, "previous thread is not finished yet");
178                            mHandler.sendMessageDelayed(mHandler.obtainMessage(NOTIFY), 1000);
179                        }
180                        break;
181                    }
182              }
183         }
184    };
185
186    private class NotificationUpdateThread extends Thread {
187
188        public NotificationUpdateThread() {
189            super("Notification Update Thread");
190        }
191
192        @Override
193        public void run() {
194            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
195            synchronized (BluetoothOppNotification.this) {
196                if (mUpdateNotificationThread != this) {
197                    throw new IllegalStateException(
198                            "multiple UpdateThreads in BluetoothOppNotification");
199                }
200                mPendingUpdate = 0;
201            }
202            updateActiveNotification();
203            updateCompletedNotification();
204            updateIncomingFileConfirmNotification();
205            synchronized (BluetoothOppNotification.this) {
206                mUpdateNotificationThread = null;
207            }
208        }
209    }
210
211    private void updateActiveNotification() {
212        // Active transfers
213        Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
214                WHERE_RUNNING, null, BluetoothShare._ID);
215        if (cursor == null) {
216            return;
217        }
218
219        // If there is active transfers, then no need to update completed transfer
220        // notifications
221        if (cursor.getCount() > 0) {
222            mUpdateCompleteNotification = false;
223        } else {
224            mUpdateCompleteNotification = true;
225        }
226        if (V) Log.v(TAG, "mUpdateCompleteNotification = " + mUpdateCompleteNotification);
227
228        // Collate the notifications
229        final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP);
230        final int directionIndex = cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION);
231        final int idIndex = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
232        final int totalBytesIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES);
233        final int currentBytesIndex = cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES);
234        final int dataIndex = cursor.getColumnIndexOrThrow(BluetoothShare._DATA);
235        final int filenameHintIndex = cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT);
236        final int confirmIndex = cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION);
237        final int destinationIndex = cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION);
238
239        mNotifications.clear();
240        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
241            int timeStamp = cursor.getInt(timestampIndex);
242            int dir = cursor.getInt(directionIndex);
243            int id = cursor.getInt(idIndex);
244            int total = cursor.getInt(totalBytesIndex);
245            int current = cursor.getInt(currentBytesIndex);
246            int confirmation = cursor.getInt(confirmIndex);
247
248            String destination = cursor.getString(destinationIndex);
249            String fileName = cursor.getString(dataIndex);
250            if (fileName == null) {
251                fileName = cursor.getString(filenameHintIndex);
252            }
253            if (fileName == null) {
254                fileName = mContext.getString(R.string.unknown_file);
255            }
256
257            String batchID = Long.toString(timeStamp);
258
259            // sending objects in one batch has same timeStamp
260            if (mNotifications.containsKey(batchID)) {
261                // NOTE: currently no such case
262                // Batch sending case
263            } else {
264                NotificationItem item = new NotificationItem();
265                item.timeStamp = timeStamp;
266                item.id = id;
267                item.direction = dir;
268                if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) {
269                    item.description = mContext.getString(R.string.notification_sending, fileName);
270                } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) {
271                    item.description = mContext
272                            .getString(R.string.notification_receiving, fileName);
273                } else {
274                    if (V) Log.v(TAG, "mDirection ERROR!");
275                }
276                item.totalCurrent = current;
277                item.totalTotal = total;
278                item.handoverInitiated =
279                        confirmation == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED;
280                item.destination = destination;
281                mNotifications.put(batchID, item);
282
283                if (V) Log.v(TAG, "ID=" + item.id + "; batchID=" + batchID + "; totoalCurrent"
284                            + item.totalCurrent + "; totalTotal=" + item.totalTotal);
285            }
286        }
287        cursor.close();
288
289        // Add the notifications
290        for (NotificationItem item : mNotifications.values()) {
291            if (item.handoverInitiated) {
292                float progress = 0;
293                if (item.totalTotal == -1) {
294                    progress = -1;
295                } else {
296                    progress = (float)item.totalCurrent / item.totalTotal;
297                }
298
299                // Let NFC service deal with notifications for this transfer
300                Intent intent = new Intent(Constants.ACTION_BT_OPP_TRANSFER_PROGRESS);
301                if (item.direction == BluetoothShare.DIRECTION_INBOUND) {
302                    intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_DIRECTION,
303                            Constants.DIRECTION_BLUETOOTH_INCOMING);
304                } else {
305                    intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_DIRECTION,
306                            Constants.DIRECTION_BLUETOOTH_OUTGOING);
307                }
308                intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_ID, item.id);
309                intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_PROGRESS, progress);
310                intent.putExtra(Constants.EXTRA_BT_OPP_ADDRESS, item.destination);
311                mContext.sendBroadcast(intent, Constants.HANDOVER_STATUS_PERMISSION);
312                continue;
313            }
314            // Build the notification object
315            // TODO: split description into two rows with filename in second row
316            Notification.Builder b = new Notification.Builder(mContext);
317            b.setContentTitle(item.description);
318            b.setContentInfo(
319                    BluetoothOppUtility.formatProgressText(item.totalTotal, item.totalCurrent));
320            b.setProgress(item.totalTotal, item.totalCurrent, item.totalTotal == -1);
321            b.setWhen(item.timeStamp);
322            if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) {
323                b.setSmallIcon(android.R.drawable.stat_sys_upload);
324            } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) {
325                b.setSmallIcon(android.R.drawable.stat_sys_download);
326            } else {
327                if (V) Log.v(TAG, "mDirection ERROR!");
328            }
329            b.setOngoing(true);
330
331            Intent intent = new Intent(Constants.ACTION_LIST);
332            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
333            intent.setData(Uri.parse(BluetoothShare.CONTENT_URI + "/" + item.id));
334
335            b.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0));
336            mNotificationMgr.notify(item.id, b.getNotification());
337
338            mActiveNotificationId = item.id;
339        }
340    }
341
342    private void updateCompletedNotification() {
343        String title;
344        String caption;
345        long timeStamp = 0;
346        int outboundSuccNumber = 0;
347        int outboundFailNumber = 0;
348        int outboundNum;
349        int inboundNum;
350        int inboundSuccNumber = 0;
351        int inboundFailNumber = 0;
352        Intent intent;
353
354        // If there is active transfer, no need to update complete transfer
355        // notification
356        if (!mUpdateCompleteNotification) {
357            if (V) Log.v(TAG, "No need to update complete notification");
358            return;
359        }
360
361        // After merge complete notifications to 2 notifications, there is no
362        // chance to update the active notifications to complete notifications
363        // as before. So need cancel the active notification after the active
364        // transfer becomes complete.
365        if (mNotificationMgr != null && mActiveNotificationId != 0) {
366            mNotificationMgr.cancel(mActiveNotificationId);
367            if (V) Log.v(TAG, "ongoing transfer notification was removed");
368        }
369
370        // Creating outbound notification
371        Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
372                WHERE_COMPLETED_OUTBOUND, null, BluetoothShare.TIMESTAMP + " DESC");
373        if (cursor == null) {
374            return;
375        }
376
377        final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP);
378        final int statusIndex = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS);
379
380        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
381            if (cursor.isFirst()) {
382                // Display the time for the latest transfer
383                timeStamp = cursor.getLong(timestampIndex);
384            }
385            int status = cursor.getInt(statusIndex);
386
387            if (BluetoothShare.isStatusError(status)) {
388                outboundFailNumber++;
389            } else {
390                outboundSuccNumber++;
391            }
392        }
393        if (V) Log.v(TAG, "outbound: succ-" + outboundSuccNumber + "  fail-" + outboundFailNumber);
394        cursor.close();
395
396        outboundNum = outboundSuccNumber + outboundFailNumber;
397        // create the outbound notification
398        if (outboundNum > 0) {
399            Notification outNoti = new Notification();
400            outNoti.icon = android.R.drawable.stat_sys_upload_done;
401            title = mContext.getString(R.string.outbound_noti_title);
402            caption = mContext.getString(R.string.noti_caption, outboundSuccNumber,
403                    outboundFailNumber);
404            intent = new Intent(Constants.ACTION_OPEN_OUTBOUND_TRANSFER);
405            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
406            outNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(
407                    mContext, 0, intent, 0));
408            intent = new Intent(Constants.ACTION_COMPLETE_HIDE);
409            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
410            outNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
411            outNoti.when = timeStamp;
412            mNotificationMgr.notify(NOTIFICATION_ID_OUTBOUND, outNoti);
413        } else {
414            if (mNotificationMgr != null) {
415                mNotificationMgr.cancel(NOTIFICATION_ID_OUTBOUND);
416                if (V) Log.v(TAG, "outbound notification was removed.");
417            }
418        }
419
420        // Creating inbound notification
421        cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
422                WHERE_COMPLETED_INBOUND, null, BluetoothShare.TIMESTAMP + " DESC");
423        if (cursor == null) {
424            return;
425        }
426
427        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
428            if (cursor.isFirst()) {
429                // Display the time for the latest transfer
430                timeStamp = cursor.getLong(timestampIndex);
431            }
432            int status = cursor.getInt(statusIndex);
433
434            if (BluetoothShare.isStatusError(status)) {
435                inboundFailNumber++;
436            } else {
437                inboundSuccNumber++;
438            }
439        }
440        if (V) Log.v(TAG, "inbound: succ-" + inboundSuccNumber + "  fail-" + inboundFailNumber);
441        cursor.close();
442
443        inboundNum = inboundSuccNumber + inboundFailNumber;
444        // create the inbound notification
445        if (inboundNum > 0) {
446            Notification inNoti = new Notification();
447            inNoti.icon = android.R.drawable.stat_sys_download_done;
448            title = mContext.getString(R.string.inbound_noti_title);
449            caption = mContext.getString(R.string.noti_caption, inboundSuccNumber,
450                    inboundFailNumber);
451            intent = new Intent(Constants.ACTION_OPEN_INBOUND_TRANSFER);
452            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
453            inNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(
454                    mContext, 0, intent, 0));
455            intent = new Intent(Constants.ACTION_COMPLETE_HIDE);
456            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
457            inNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
458            inNoti.when = timeStamp;
459            mNotificationMgr.notify(NOTIFICATION_ID_INBOUND, inNoti);
460        } else {
461            if (mNotificationMgr != null) {
462                mNotificationMgr.cancel(NOTIFICATION_ID_INBOUND);
463                if (V) Log.v(TAG, "inbound notification was removed.");
464            }
465        }
466    }
467
468    private void updateIncomingFileConfirmNotification() {
469        Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
470                WHERE_CONFIRM_PENDING, null, BluetoothShare._ID);
471
472        if (cursor == null) {
473            return;
474        }
475
476        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
477            CharSequence title =
478                    mContext.getText(R.string.incoming_file_confirm_Notification_title);
479            CharSequence caption = mContext
480                    .getText(R.string.incoming_file_confirm_Notification_caption);
481            int id = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
482            long timeStamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
483            Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id);
484
485            Notification n = new Notification();
486            n.icon = R.drawable.bt_incomming_file_notification;
487            n.flags |= Notification.FLAG_ONLY_ALERT_ONCE;
488            n.flags |= Notification.FLAG_ONGOING_EVENT;
489            n.defaults = Notification.DEFAULT_SOUND;
490            n.tickerText = title;
491
492            Intent intent = new Intent(Constants.ACTION_INCOMING_FILE_CONFIRM);
493            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
494            intent.setData(contentUri);
495
496            n.when = timeStamp;
497            n.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(mContext, 0,
498                    intent, 0));
499
500            intent = new Intent(Constants.ACTION_HIDE);
501            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
502            intent.setData(contentUri);
503            n.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
504
505            mNotificationMgr.notify(id, n);
506        }
507        cursor.close();
508    }
509}
510