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