BluetoothOppNotification.java revision 509a6cd43906cce5f9bc78a83bd2058a4e5249e7
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        // create the outbound notification
350        if (outboundNum > 0) {
351            mOutNoti = new Notification();
352            mOutNoti.icon = android.R.drawable.stat_sys_upload_done;
353            title = mContext.getString(R.string.outbound_noti_title);
354            caption = mContext.getString(R.string.noti_caption, outboundSuccNumber,
355                    outboundFailNumber);
356            intent = new Intent(Constants.ACTION_OPEN_OUTBOUND_TRANSFER);
357            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
358            mOutNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(
359                    mContext, 0, intent, 0));
360            mOutNoti.when = timeStamp;
361            mNotificationMgr.notify(NOTIFICATION_ID_OUTBOUND, mOutNoti);
362        } else {
363            if (mNotificationMgr != null && mOutNoti != null) {
364                mNotificationMgr.cancel(NOTIFICATION_ID_OUTBOUND);
365                mOutNoti = null;
366                if (V) Log.v(TAG, "outbound notification was removed.");
367            }
368        }
369
370        // Creating inbound notification
371        cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
372                WHERE_COMPLETED_INBOUND, null, BluetoothShare.TIMESTAMP + " DESC");
373        if (cursor == null) {
374            return;
375        }
376
377        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
378            if (cursor.isFirst()) {
379                // Display the time for the latest transfer
380                timeStamp = cursor.getLong(timestampIndex);
381            }
382            int status = cursor.getInt(statusIndex);
383
384            if (BluetoothShare.isStatusError(status)) {
385                inboundFailNumber++;
386            } else {
387                inboundSuccNumber++;
388            }
389        }
390        if (V) Log.v(TAG, "inbound: succ-" + inboundSuccNumber + "  fail-" + inboundFailNumber);
391        cursor.close();
392
393        inboundNum = inboundSuccNumber + inboundFailNumber;
394        // create the inbound notification
395        if (inboundNum > 0) {
396            mInNoti = new Notification();
397            mInNoti.icon = android.R.drawable.stat_sys_download_done;
398            title = mContext.getString(R.string.inbound_noti_title);
399            caption = mContext.getString(R.string.noti_caption, inboundSuccNumber,
400                    inboundFailNumber);
401            intent = new Intent(Constants.ACTION_OPEN_INBOUND_TRANSFER);
402            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
403            mInNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(
404                    mContext, 0, intent, 0));
405            mInNoti.when = timeStamp;
406            mNotificationMgr.notify(NOTIFICATION_ID_INBOUND, mInNoti);
407        } else {
408            if (mNotificationMgr != null && mInNoti != null) {
409                mNotificationMgr.cancel(NOTIFICATION_ID_INBOUND);
410                mInNoti = null;
411                if (V) Log.v(TAG, "inbound notification was removed.");
412            }
413        }
414    }
415
416    private void updateIncomingFileConfirmNotification() {
417        Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null,
418                WHERE_CONFIRM_PENDING, null, BluetoothShare._ID);
419
420        if (cursor == null) {
421            return;
422        }
423
424        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
425            String title = mContext.getString(R.string.incoming_file_confirm_Notification_title);
426            String caption = mContext
427                    .getString(R.string.incoming_file_confirm_Notification_caption);
428            int id = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
429            long timeStamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
430            Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id);
431
432            Notification n = new Notification();
433            n.icon = R.drawable.bt_incomming_file_notification;
434            n.flags |= Notification.FLAG_ONLY_ALERT_ONCE;
435            n.defaults = Notification.DEFAULT_SOUND;
436            n.tickerText = title;
437            Intent intent = new Intent(Constants.ACTION_INCOMING_FILE_CONFIRM);
438            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
439            intent.setData(contentUri);
440
441            n.when = timeStamp;
442            n.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(mContext, 0,
443                    intent, 0));
444
445            intent = new Intent(Constants.ACTION_HIDE);
446            intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
447            intent.setData(contentUri);
448            n.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
449
450            mNotificationMgr.notify(id, n);
451        }
452        cursor.close();
453    }
454}
455