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