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