BluetoothOppNotification.java revision b0b662a98d8b7c099ee706d0e08bd239adcdffc7
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.widget.RemoteViews;
46import android.os.Handler;
47import android.os.Message;
48import android.os.Process;
49import java.util.HashMap;
50
51/**
52 * This class handles the updating of the Notification Manager for the cases
53 * where there is an ongoing transfer, incoming transfer need confirm and
54 * complete (successful or failed) transfer.
55 */
56class BluetoothOppNotification {
57    private static final String TAG = "BluetoothOppNotification";
58    private static final boolean V = Constants.VERBOSE;
59
60    static final String status = "(" + BluetoothShare.STATUS + " == '192'" + ")";
61
62    static final String visible = "(" + BluetoothShare.VISIBILITY + " IS NULL OR "
63            + BluetoothShare.VISIBILITY + " == '" + BluetoothShare.VISIBILITY_VISIBLE + "'" + ")";
64
65    static final String confirm = "(" + BluetoothShare.USER_CONFIRMATION + " == '"
66            + BluetoothShare.USER_CONFIRMATION_CONFIRMED + "' OR "
67            + BluetoothShare.USER_CONFIRMATION + " == '"
68            + BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED + "'" + ")";
69
70    static final String WHERE_RUNNING = status + " AND " + visible + " AND " + confirm;
71
72    static final String WHERE_COMPLETED = BluetoothShare.STATUS + " >= '200' AND " + visible;
73
74    private static final String WHERE_COMPLETED_OUTBOUND = WHERE_COMPLETED + " AND " + "("
75            + BluetoothShare.DIRECTION + " == " + BluetoothShare.DIRECTION_OUTBOUND + ")";
76
77    private static final String WHERE_COMPLETED_INBOUND = WHERE_COMPLETED + " AND " + "("
78            + BluetoothShare.DIRECTION + " == " + BluetoothShare.DIRECTION_INBOUND + ")";
79
80    static final String WHERE_CONFIRM_PENDING = BluetoothShare.USER_CONFIRMATION + " == '"
81            + BluetoothShare.USER_CONFIRMATION_PENDING + "'" + " AND " + visible;
82
83    public NotificationManager mNotificationMgr;
84
85    private Context mContext;
86
87    private HashMap<String, NotificationItem> mNotifications;
88
89    private NotificationUpdateThread mUpdateNotificationThread;
90
91    private int mPendingUpdate = 0;
92
93    private static final int NOTIFICATION_ID_OUTBOUND = -1000005;
94
95    private static final int NOTIFICATION_ID_INBOUND = -1000006;
96
97    private boolean mUpdateCompleteNotification = true;
98
99    private int mActiveNotificationId = 0;
100
101    /**
102     * This inner class is used to describe some properties for one transfer.
103     */
104    static class NotificationItem {
105        int id; // This first field _id in db;
106
107        int direction; // to indicate sending or receiving
108
109        int totalCurrent = 0; // current transfer bytes
110
111        int totalTotal = 0; // total bytes for current transfer
112
113        int timeStamp = 0; // Database time stamp. Used for sorting ongoing transfers.
114
115        String description; // the text above progress bar
116    }
117
118    /**
119     * Constructor
120     *
121     * @param ctx The context to use to obtain access to the Notification
122     *            Service
123     */
124    BluetoothOppNotification(Context ctx) {
125        mContext = ctx;
126        mNotificationMgr = (NotificationManager)mContext
127                .getSystemService(Context.NOTIFICATION_SERVICE);
128        mNotifications = new HashMap<String, NotificationItem>();
129    }
130
131    /**
132     * Update the notification ui.
133     */
134    public void updateNotification() {
135        synchronized (BluetoothOppNotification.this) {
136            mPendingUpdate++;
137            if (mPendingUpdate > 1) {
138                if (V) Log.v(TAG, "update too frequent, put in queue");
139                return;
140            }
141            if (!mHandler.hasMessages(NOTIFY)) {
142                if (V) Log.v(TAG, "send message");
143                mHandler.sendMessage(mHandler.obtainMessage(NOTIFY));
144            }
145        }
146    }
147
148    private static final int NOTIFY = 0;
149    // Use 1 second timer to limit notification frequency.
150    // 1. On the first notification, create the update thread.
151    //    Buffer other updates.
152    // 2. Update thread will clear mPendingUpdate.
153    // 3. Handler sends a delayed message to self
154    // 4. Handler checks if there are any more updates after 1 second.
155    // 5. If there is an update, update it else stop.
156    private Handler mHandler = new Handler() {
157        public void handleMessage(Message msg) {
158            switch (msg.what) {
159                case NOTIFY:
160                    synchronized (BluetoothOppNotification.this) {
161                        if (mPendingUpdate > 0 && mUpdateNotificationThread == null) {
162                            if (V) Log.v(TAG, "new notify threadi!");
163                            mUpdateNotificationThread = new NotificationUpdateThread();
164                            mUpdateNotificationThread.start();
165                            if (V) Log.v(TAG, "send delay message");
166                            mHandler.sendMessageDelayed(mHandler.obtainMessage(NOTIFY), 1000);
167                        } else if (mPendingUpdate > 0) {
168                            if (V) Log.v(TAG, "previous thread is not finished yet");
169                            mHandler.sendMessageDelayed(mHandler.obtainMessage(NOTIFY), 1000);
170                        }
171                        break;
172                    }
173              }
174         }
175    };
176
177    private class NotificationUpdateThread extends Thread {
178
179        public NotificationUpdateThread() {
180            super("Notification Update Thread");
181        }
182
183        @Override
184        public void run() {
185            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
186            synchronized (BluetoothOppNotification.this) {
187                if (mUpdateNotificationThread != this) {
188                    throw new IllegalStateException(
189                            "multiple UpdateThreads in BluetoothOppNotification");
190                }
191                mPendingUpdate = 0;
192            }
193            updateActiveNotification();
194            updateCompletedNotification();
195            updateIncomingFileConfirmNotification();
196            synchronized (BluetoothOppNotification.this) {
197                mUpdateNotificationThread = null;
198            }
199        }
200    }
201
202    private void updateActiveNotification() {
203        // Active transfers
204        Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
205                WHERE_RUNNING, null, BluetoothShare._ID);
206        if (cursor == null) {
207            return;
208        }
209
210        // If there is active transfers, then no need to update completed transfer
211        // notifications
212        if (cursor.getCount() > 0) {
213            mUpdateCompleteNotification = false;
214        } else {
215            mUpdateCompleteNotification = true;
216        }
217        if (V) Log.v(TAG, "mUpdateCompleteNotification = " + mUpdateCompleteNotification);
218
219        // Collate the notifications
220        final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP);
221        final int directionIndex = cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION);
222        final int idIndex = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
223        final int totalBytesIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES);
224        final int currentBytesIndex = cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES);
225        final int dataIndex = cursor.getColumnIndexOrThrow(BluetoothShare._DATA);
226        final int filenameHintIndex = cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT);
227
228        mNotifications.clear();
229        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
230            int timeStamp = cursor.getInt(timestampIndex);
231            int dir = cursor.getInt(directionIndex);
232            int id = cursor.getInt(idIndex);
233            int total = cursor.getInt(totalBytesIndex);
234            int current = cursor.getInt(currentBytesIndex);
235
236            String fileName = cursor.getString(dataIndex);
237            if (fileName == null) {
238                fileName = cursor.getString(filenameHintIndex);
239            }
240            if (fileName == null) {
241                fileName = mContext.getString(R.string.unknown_file);
242            }
243
244            String batchID = Long.toString(timeStamp);
245
246            // sending objects in one batch has same timeStamp
247            if (mNotifications.containsKey(batchID)) {
248                // NOTE: currently no such case
249                // Batch sending case
250            } else {
251                NotificationItem item = new NotificationItem();
252                item.timeStamp = timeStamp;
253                item.id = id;
254                item.direction = dir;
255                if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) {
256                    item.description = mContext.getString(R.string.notification_sending, fileName);
257                } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) {
258                    item.description = mContext
259                            .getString(R.string.notification_receiving, fileName);
260                } else {
261                    if (V) Log.v(TAG, "mDirection ERROR!");
262                }
263                item.totalCurrent = current;
264                item.totalTotal = total;
265
266                mNotifications.put(batchID, item);
267
268                if (V) Log.v(TAG, "ID=" + item.id + "; batchID=" + batchID + "; totoalCurrent"
269                            + item.totalCurrent + "; totalTotal=" + item.totalTotal);
270            }
271        }
272        cursor.close();
273
274        // Add the notifications
275        for (NotificationItem item : mNotifications.values()) {
276            // Build the RemoteView object
277            RemoteViews expandedView = new RemoteViews(Constants.THIS_PACKAGE_NAME,
278                    R.layout.status_bar_ongoing_event_progress_bar);
279
280            expandedView.setTextViewText(R.id.description, item.description);
281
282            expandedView.setProgressBar(R.id.progress_bar, item.totalTotal, item.totalCurrent,
283                    item.totalTotal == -1);
284
285            expandedView.setTextViewText(R.id.progress_text, BluetoothOppUtility
286                    .formatProgressText(item.totalTotal, item.totalCurrent));
287
288            // Build the notification object
289            Notification n = new Notification();
290            n.when = item.timeStamp;
291            if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) {
292                n.icon = android.R.drawable.stat_sys_upload;
293                expandedView.setImageViewResource(R.id.appIcon, android.R.drawable.stat_sys_upload);
294            } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) {
295                n.icon = android.R.drawable.stat_sys_download;
296                expandedView.setImageViewResource(R.id.appIcon,
297                        android.R.drawable.stat_sys_download);
298            } else {
299                if (V) Log.v(TAG, "mDirection ERROR!");
300            }
301
302            n.flags |= Notification.FLAG_ONGOING_EVENT;
303            n.contentView = expandedView;
304
305            Intent intent = new Intent(Constants.ACTION_LIST);
306            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
307            intent.setData(Uri.parse(BluetoothShare.CONTENT_URI + "/" + item.id));
308
309            n.contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
310            mNotificationMgr.notify(item.id, n);
311
312            mActiveNotificationId = item.id;
313        }
314    }
315
316    private void updateCompletedNotification() {
317        String title;
318        String caption;
319        long timeStamp = 0;
320        int outboundSuccNumber = 0;
321        int outboundFailNumber = 0;
322        int outboundNum;
323        int inboundNum;
324        int inboundSuccNumber = 0;
325        int inboundFailNumber = 0;
326        Intent intent;
327
328        // If there is active transfer, no need to update complete transfer
329        // notification
330        if (!mUpdateCompleteNotification) {
331            if (V) Log.v(TAG, "No need to update complete notification");
332            return;
333        }
334
335        // After merge complete notifications to 2 notifications, there is no
336        // chance to update the active notifications to complete notifications
337        // as before. So need cancel the active notification after the active
338        // transfer becomes complete.
339        if (mNotificationMgr != null && mActiveNotificationId != 0) {
340            mNotificationMgr.cancel(mActiveNotificationId);
341            if (V) Log.v(TAG, "ongoing transfer notification was removed");
342        }
343
344        // Creating outbound notification
345        Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
346                WHERE_COMPLETED_OUTBOUND, null, BluetoothShare.TIMESTAMP + " DESC");
347        if (cursor == null) {
348            return;
349        }
350
351        final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP);
352        final int statusIndex = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS);
353
354        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
355            if (cursor.isFirst()) {
356                // Display the time for the latest transfer
357                timeStamp = cursor.getLong(timestampIndex);
358            }
359            int status = cursor.getInt(statusIndex);
360
361            if (BluetoothShare.isStatusError(status)) {
362                outboundFailNumber++;
363            } else {
364                outboundSuccNumber++;
365            }
366        }
367        if (V) Log.v(TAG, "outbound: succ-" + outboundSuccNumber + "  fail-" + outboundFailNumber);
368        cursor.close();
369
370        outboundNum = outboundSuccNumber + outboundFailNumber;
371        // create the outbound notification
372        if (outboundNum > 0) {
373            Notification outNoti = new Notification();
374            outNoti.icon = android.R.drawable.stat_sys_upload_done;
375            title = mContext.getString(R.string.outbound_noti_title);
376            caption = mContext.getString(R.string.noti_caption, outboundSuccNumber,
377                    outboundFailNumber);
378            intent = new Intent(Constants.ACTION_OPEN_OUTBOUND_TRANSFER);
379            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
380            outNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(
381                    mContext, 0, intent, 0));
382            intent = new Intent(Constants.ACTION_COMPLETE_HIDE);
383            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
384            outNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
385            outNoti.when = timeStamp;
386            mNotificationMgr.notify(NOTIFICATION_ID_OUTBOUND, outNoti);
387        } else {
388            if (mNotificationMgr != null) {
389                mNotificationMgr.cancel(NOTIFICATION_ID_OUTBOUND);
390                if (V) Log.v(TAG, "outbound notification was removed.");
391            }
392        }
393
394        // Creating inbound notification
395        cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
396                WHERE_COMPLETED_INBOUND, null, BluetoothShare.TIMESTAMP + " DESC");
397        if (cursor == null) {
398            return;
399        }
400
401        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
402            if (cursor.isFirst()) {
403                // Display the time for the latest transfer
404                timeStamp = cursor.getLong(timestampIndex);
405            }
406            int status = cursor.getInt(statusIndex);
407
408            if (BluetoothShare.isStatusError(status)) {
409                inboundFailNumber++;
410            } else {
411                inboundSuccNumber++;
412            }
413        }
414        if (V) Log.v(TAG, "inbound: succ-" + inboundSuccNumber + "  fail-" + inboundFailNumber);
415        cursor.close();
416
417        inboundNum = inboundSuccNumber + inboundFailNumber;
418        // create the inbound notification
419        if (inboundNum > 0) {
420            Notification inNoti = new Notification();
421            inNoti.icon = android.R.drawable.stat_sys_download_done;
422            title = mContext.getString(R.string.inbound_noti_title);
423            caption = mContext.getString(R.string.noti_caption, inboundSuccNumber,
424                    inboundFailNumber);
425            intent = new Intent(Constants.ACTION_OPEN_INBOUND_TRANSFER);
426            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
427            inNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(
428                    mContext, 0, intent, 0));
429            intent = new Intent(Constants.ACTION_COMPLETE_HIDE);
430            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
431            inNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
432            inNoti.when = timeStamp;
433            mNotificationMgr.notify(NOTIFICATION_ID_INBOUND, inNoti);
434        } else {
435            if (mNotificationMgr != null) {
436                mNotificationMgr.cancel(NOTIFICATION_ID_INBOUND);
437                if (V) Log.v(TAG, "inbound notification was removed.");
438            }
439        }
440    }
441
442    private void updateIncomingFileConfirmNotification() {
443        Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
444                WHERE_CONFIRM_PENDING, null, BluetoothShare._ID);
445
446        if (cursor == null) {
447            return;
448        }
449
450        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
451            CharSequence title =
452                    mContext.getText(R.string.incoming_file_confirm_Notification_title);
453            CharSequence caption = mContext
454                    .getText(R.string.incoming_file_confirm_Notification_caption);
455            int id = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
456            long timeStamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
457            Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id);
458
459            Notification n = new Notification();
460            n.icon = R.drawable.bt_incomming_file_notification;
461            n.flags |= Notification.FLAG_ONLY_ALERT_ONCE;
462            n.flags |= Notification.FLAG_ONGOING_EVENT;
463            n.defaults = Notification.DEFAULT_SOUND;
464            n.tickerText = title;
465
466            Intent intent = new Intent(Constants.ACTION_INCOMING_FILE_CONFIRM);
467            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
468            intent.setData(contentUri);
469
470            n.when = timeStamp;
471            n.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(mContext, 0,
472                    intent, 0));
473
474            intent = new Intent(Constants.ACTION_HIDE);
475            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
476            intent.setData(contentUri);
477            n.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
478
479            mNotificationMgr.notify(id, n);
480        }
481        cursor.close();
482    }
483}
484