BluetoothOppNotification.java revision dec631a77ab9cc89c4d3867b80cfe300e7cf83e9
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 notification object
277            // TODO: split description into two rows with filename in second row
278            Notification.Builder b = new Notification.Builder(mContext);
279            b.setContentTitle(item.description);
280            b.setContentInfo(
281                    BluetoothOppUtility.formatProgressText(item.totalTotal, item.totalCurrent));
282            b.setProgress(item.totalTotal, item.totalCurrent, item.totalTotal == -1);
283            b.setWhen(item.timeStamp);
284            if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) {
285                b.setSmallIcon(android.R.drawable.stat_sys_upload);
286            } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) {
287                b.setSmallIcon(android.R.drawable.stat_sys_download);
288            } else {
289                if (V) Log.v(TAG, "mDirection ERROR!");
290            }
291            b.setOngoing(true);
292
293            Intent intent = new Intent(Constants.ACTION_LIST);
294            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
295            intent.setData(Uri.parse(BluetoothShare.CONTENT_URI + "/" + item.id));
296
297            b.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0));
298            mNotificationMgr.notify(item.id, b.getNotification());
299
300            mActiveNotificationId = item.id;
301        }
302    }
303
304    private void updateCompletedNotification() {
305        String title;
306        String caption;
307        long timeStamp = 0;
308        int outboundSuccNumber = 0;
309        int outboundFailNumber = 0;
310        int outboundNum;
311        int inboundNum;
312        int inboundSuccNumber = 0;
313        int inboundFailNumber = 0;
314        Intent intent;
315
316        // If there is active transfer, no need to update complete transfer
317        // notification
318        if (!mUpdateCompleteNotification) {
319            if (V) Log.v(TAG, "No need to update complete notification");
320            return;
321        }
322
323        // After merge complete notifications to 2 notifications, there is no
324        // chance to update the active notifications to complete notifications
325        // as before. So need cancel the active notification after the active
326        // transfer becomes complete.
327        if (mNotificationMgr != null && mActiveNotificationId != 0) {
328            mNotificationMgr.cancel(mActiveNotificationId);
329            if (V) Log.v(TAG, "ongoing transfer notification was removed");
330        }
331
332        // Creating outbound notification
333        Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
334                WHERE_COMPLETED_OUTBOUND, null, BluetoothShare.TIMESTAMP + " DESC");
335        if (cursor == null) {
336            return;
337        }
338
339        final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP);
340        final int statusIndex = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS);
341
342        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
343            if (cursor.isFirst()) {
344                // Display the time for the latest transfer
345                timeStamp = cursor.getLong(timestampIndex);
346            }
347            int status = cursor.getInt(statusIndex);
348
349            if (BluetoothShare.isStatusError(status)) {
350                outboundFailNumber++;
351            } else {
352                outboundSuccNumber++;
353            }
354        }
355        if (V) Log.v(TAG, "outbound: succ-" + outboundSuccNumber + "  fail-" + outboundFailNumber);
356        cursor.close();
357
358        outboundNum = outboundSuccNumber + outboundFailNumber;
359        // create the outbound notification
360        if (outboundNum > 0) {
361            Notification outNoti = new Notification();
362            outNoti.icon = android.R.drawable.stat_sys_upload_done;
363            title = mContext.getString(R.string.outbound_noti_title);
364            caption = mContext.getString(R.string.noti_caption, outboundSuccNumber,
365                    outboundFailNumber);
366            intent = new Intent(Constants.ACTION_OPEN_OUTBOUND_TRANSFER);
367            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
368            outNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(
369                    mContext, 0, intent, 0));
370            intent = new Intent(Constants.ACTION_COMPLETE_HIDE);
371            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
372            outNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
373            outNoti.when = timeStamp;
374            mNotificationMgr.notify(NOTIFICATION_ID_OUTBOUND, outNoti);
375        } else {
376            if (mNotificationMgr != null) {
377                mNotificationMgr.cancel(NOTIFICATION_ID_OUTBOUND);
378                if (V) Log.v(TAG, "outbound notification was removed.");
379            }
380        }
381
382        // Creating inbound notification
383        cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
384                WHERE_COMPLETED_INBOUND, null, BluetoothShare.TIMESTAMP + " DESC");
385        if (cursor == null) {
386            return;
387        }
388
389        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
390            if (cursor.isFirst()) {
391                // Display the time for the latest transfer
392                timeStamp = cursor.getLong(timestampIndex);
393            }
394            int status = cursor.getInt(statusIndex);
395
396            if (BluetoothShare.isStatusError(status)) {
397                inboundFailNumber++;
398            } else {
399                inboundSuccNumber++;
400            }
401        }
402        if (V) Log.v(TAG, "inbound: succ-" + inboundSuccNumber + "  fail-" + inboundFailNumber);
403        cursor.close();
404
405        inboundNum = inboundSuccNumber + inboundFailNumber;
406        // create the inbound notification
407        if (inboundNum > 0) {
408            Notification inNoti = new Notification();
409            inNoti.icon = android.R.drawable.stat_sys_download_done;
410            title = mContext.getString(R.string.inbound_noti_title);
411            caption = mContext.getString(R.string.noti_caption, inboundSuccNumber,
412                    inboundFailNumber);
413            intent = new Intent(Constants.ACTION_OPEN_INBOUND_TRANSFER);
414            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
415            inNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(
416                    mContext, 0, intent, 0));
417            intent = new Intent(Constants.ACTION_COMPLETE_HIDE);
418            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
419            inNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
420            inNoti.when = timeStamp;
421            mNotificationMgr.notify(NOTIFICATION_ID_INBOUND, inNoti);
422        } else {
423            if (mNotificationMgr != null) {
424                mNotificationMgr.cancel(NOTIFICATION_ID_INBOUND);
425                if (V) Log.v(TAG, "inbound notification was removed.");
426            }
427        }
428    }
429
430    private void updateIncomingFileConfirmNotification() {
431        Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
432                WHERE_CONFIRM_PENDING, null, BluetoothShare._ID);
433
434        if (cursor == null) {
435            return;
436        }
437
438        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
439            CharSequence title =
440                    mContext.getText(R.string.incoming_file_confirm_Notification_title);
441            CharSequence caption = mContext
442                    .getText(R.string.incoming_file_confirm_Notification_caption);
443            int id = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
444            long timeStamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
445            Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id);
446
447            Notification n = new Notification();
448            n.icon = R.drawable.bt_incomming_file_notification;
449            n.flags |= Notification.FLAG_ONLY_ALERT_ONCE;
450            n.flags |= Notification.FLAG_ONGOING_EVENT;
451            n.defaults = Notification.DEFAULT_SOUND;
452            n.tickerText = title;
453
454            Intent intent = new Intent(Constants.ACTION_INCOMING_FILE_CONFIRM);
455            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
456            intent.setData(contentUri);
457
458            n.when = timeStamp;
459            n.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(mContext, 0,
460                    intent, 0));
461
462            intent = new Intent(Constants.ACTION_HIDE);
463            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
464            intent.setData(contentUri);
465            n.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
466
467            mNotificationMgr.notify(id, n);
468        }
469        cursor.close();
470    }
471}
472