• 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 android.app.Notification;
36 import android.app.NotificationChannel;
37 import android.app.NotificationManager;
38 import android.app.PendingIntent;
39 import android.content.ContentResolver;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.database.Cursor;
43 import android.net.Uri;
44 import android.os.Handler;
45 import android.os.Message;
46 import android.os.Process;
47 import android.text.format.Formatter;
48 import android.util.Log;
49 
50 import com.android.bluetooth.R;
51 
52 import java.util.HashMap;
53 
54 /**
55  * This class handles the updating of the Notification Manager for the cases
56  * where there is an ongoing transfer, incoming transfer need confirm and
57  * complete (successful or failed) transfer.
58  */
59 class BluetoothOppNotification {
60     private static final String TAG = "BluetoothOppNotification";
61     private static final boolean V = Constants.VERBOSE;
62 
63     static final String STATUS = "(" + BluetoothShare.STATUS + " == '192'" + ")";
64 
65     static final String VISIBLE =
66             "(" + BluetoothShare.VISIBILITY + " IS NULL OR " + BluetoothShare.VISIBILITY + " == '"
67                     + BluetoothShare.VISIBILITY_VISIBLE + "'" + ")";
68 
69     static final String CONFIRM = "(" + BluetoothShare.USER_CONFIRMATION + " == '"
70             + BluetoothShare.USER_CONFIRMATION_CONFIRMED + "' OR "
71             + BluetoothShare.USER_CONFIRMATION + " == '"
72             + BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED + "' OR "
73             + BluetoothShare.USER_CONFIRMATION + " == '"
74             + BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED + "'" + ")";
75 
76     static final String NOT_THROUGH_HANDOVER = "(" + BluetoothShare.USER_CONFIRMATION + " != '"
77             + BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED + "'" + ")";
78 
79     static final String WHERE_RUNNING = STATUS + " AND " + VISIBLE + " AND " + CONFIRM;
80 
81     static final String WHERE_COMPLETED =
82             BluetoothShare.STATUS + " >= '200' AND " + VISIBLE + " AND " + NOT_THROUGH_HANDOVER;
83     // Don't show handover-initiated transfers
84 
85     private static final String WHERE_COMPLETED_OUTBOUND =
86             WHERE_COMPLETED + " AND " + "(" + BluetoothShare.DIRECTION + " == "
87                     + BluetoothShare.DIRECTION_OUTBOUND + ")";
88 
89     private static final String WHERE_COMPLETED_INBOUND =
90             WHERE_COMPLETED + " AND " + "(" + BluetoothShare.DIRECTION + " == "
91                     + BluetoothShare.DIRECTION_INBOUND + ")";
92 
93     static final String WHERE_CONFIRM_PENDING =
94             BluetoothShare.USER_CONFIRMATION + " == '" + BluetoothShare.USER_CONFIRMATION_PENDING
95                     + "'" + " AND " + VISIBLE;
96 
97     public NotificationManager mNotificationMgr;
98 
99     private NotificationChannel mNotificationChannel;
100     private static final String OPP_NOTIFICATION_CHANNEL = "opp_notification_channel";
101 
102     private Context mContext;
103 
104     private HashMap<String, NotificationItem> mNotifications;
105 
106     private NotificationUpdateThread mUpdateNotificationThread;
107 
108     private int mPendingUpdate = 0;
109 
110     public static final int NOTIFICATION_ID_PROGRESS = -1000004;
111 
112     private static final int NOTIFICATION_ID_OUTBOUND_COMPLETE = -1000005;
113 
114     private static final int NOTIFICATION_ID_INBOUND_COMPLETE = -1000006;
115 
116     private boolean mUpdateCompleteNotification = true;
117 
118     private ContentResolver mContentResolver = null;
119 
120     /**
121      * This inner class is used to describe some properties for one transfer.
122      */
123     static class NotificationItem {
124         public int id; // This first field _id in db;
125 
126         public int direction; // to indicate sending or receiving
127 
128         public long totalCurrent = 0; // current transfer bytes
129 
130         public long totalTotal = 0; // total bytes for current transfer
131 
132         public long timeStamp = 0; // Database time stamp. Used for sorting ongoing transfers.
133 
134         public String description; // the text above progress bar
135 
136         public boolean handoverInitiated = false;
137         // transfer initiated by connection handover (eg NFC)
138 
139         public String destination; // destination associated with this transfer
140     }
141 
142     /**
143      * Constructor
144      *
145      * @param ctx The context to use to obtain access to the Notification
146      *            Service
147      */
BluetoothOppNotification(Context ctx)148     BluetoothOppNotification(Context ctx) {
149         mContext = ctx;
150         mNotificationMgr =
151                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
152         mNotificationChannel = new NotificationChannel(OPP_NOTIFICATION_CHANNEL,
153                 mContext.getString(R.string.opp_notification_group),
154                 NotificationManager.IMPORTANCE_HIGH);
155 
156         mNotificationMgr.createNotificationChannel(mNotificationChannel);
157         mNotifications = new HashMap<String, NotificationItem>();
158         // Get Content Resolver object one time
159         mContentResolver = mContext.getContentResolver();
160     }
161 
162     /**
163      * Update the notification ui.
164      */
updateNotification()165     public void updateNotification() {
166         synchronized (BluetoothOppNotification.this) {
167             mPendingUpdate++;
168             if (mPendingUpdate > 1) {
169                 if (V) {
170                     Log.v(TAG, "update too frequent, put in queue");
171                 }
172                 return;
173             }
174             if (!mHandler.hasMessages(NOTIFY)) {
175                 if (V) {
176                     Log.v(TAG, "send message");
177                 }
178                 mHandler.sendMessage(mHandler.obtainMessage(NOTIFY));
179             }
180         }
181     }
182 
183     private static final int NOTIFY = 0;
184     // Use 1 second timer to limit notification frequency.
185     // 1. On the first notification, create the update thread.
186     //    Buffer other updates.
187     // 2. Update thread will clear mPendingUpdate.
188     // 3. Handler sends a delayed message to self
189     // 4. Handler checks if there are any more updates after 1 second.
190     // 5. If there is an update, update it else stop.
191     private Handler mHandler = new Handler() {
192         @Override
193         public void handleMessage(Message msg) {
194             switch (msg.what) {
195                 case NOTIFY:
196                     synchronized (BluetoothOppNotification.this) {
197                         if (mPendingUpdate > 0 && mUpdateNotificationThread == null) {
198                             if (V) {
199                                 Log.v(TAG, "new notify threadi!");
200                             }
201                             mUpdateNotificationThread = new NotificationUpdateThread();
202                             mUpdateNotificationThread.start();
203                             if (V) {
204                                 Log.v(TAG, "send delay message");
205                             }
206                             mHandler.sendMessageDelayed(mHandler.obtainMessage(NOTIFY), 1000);
207                         } else if (mPendingUpdate > 0) {
208                             if (V) {
209                                 Log.v(TAG, "previous thread is not finished yet");
210                             }
211                             mHandler.sendMessageDelayed(mHandler.obtainMessage(NOTIFY), 1000);
212                         }
213                         break;
214                     }
215             }
216         }
217     };
218 
219     private class NotificationUpdateThread extends Thread {
220 
NotificationUpdateThread()221         NotificationUpdateThread() {
222             super("Notification Update Thread");
223         }
224 
225         @Override
run()226         public void run() {
227             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
228             synchronized (BluetoothOppNotification.this) {
229                 if (mUpdateNotificationThread != this) {
230                     throw new IllegalStateException(
231                             "multiple UpdateThreads in BluetoothOppNotification");
232                 }
233                 mPendingUpdate = 0;
234             }
235             updateActiveNotification();
236             updateCompletedNotification();
237             updateIncomingFileConfirmNotification();
238             synchronized (BluetoothOppNotification.this) {
239                 mUpdateNotificationThread = null;
240             }
241         }
242     }
243 
updateActiveNotification()244     private void updateActiveNotification() {
245         // Active transfers
246         Cursor cursor =
247                 mContentResolver.query(BluetoothShare.CONTENT_URI, null, WHERE_RUNNING, null,
248                         BluetoothShare._ID);
249         if (cursor == null) {
250             return;
251         }
252 
253         // If there is active transfers, then no need to update completed transfer
254         // notifications
255         if (cursor.getCount() > 0) {
256             mUpdateCompleteNotification = false;
257         } else {
258             mUpdateCompleteNotification = true;
259         }
260         if (V) {
261             Log.v(TAG, "mUpdateCompleteNotification = " + mUpdateCompleteNotification);
262         }
263 
264         // Collate the notifications
265         final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP);
266         final int directionIndex = cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION);
267         final int idIndex = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
268         final int totalBytesIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES);
269         final int currentBytesIndex = cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES);
270         final int dataIndex = cursor.getColumnIndexOrThrow(BluetoothShare._DATA);
271         final int filenameHintIndex = cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT);
272         final int confirmIndex = cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION);
273         final int destinationIndex = cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION);
274 
275         mNotifications.clear();
276         for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
277             long timeStamp = cursor.getLong(timestampIndex);
278             int dir = cursor.getInt(directionIndex);
279             int id = cursor.getInt(idIndex);
280             long total = cursor.getLong(totalBytesIndex);
281             long current = cursor.getLong(currentBytesIndex);
282             int confirmation = cursor.getInt(confirmIndex);
283 
284             String destination = cursor.getString(destinationIndex);
285             String fileName = cursor.getString(dataIndex);
286             if (fileName == null) {
287                 fileName = cursor.getString(filenameHintIndex);
288             }
289             if (fileName == null) {
290                 fileName = mContext.getString(R.string.unknown_file);
291             }
292 
293             String batchID = Long.toString(timeStamp);
294 
295             // sending objects in one batch has same timeStamp
296             if (mNotifications.containsKey(batchID)) {
297                 // NOTE: currently no such case
298                 // Batch sending case
299             } else {
300                 NotificationItem item = new NotificationItem();
301                 item.timeStamp = timeStamp;
302                 item.id = id;
303                 item.direction = dir;
304                 if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) {
305                     item.description = mContext.getString(R.string.notification_sending, fileName);
306                 } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) {
307                     item.description =
308                             mContext.getString(R.string.notification_receiving, fileName);
309                 } else {
310                     if (V) {
311                         Log.v(TAG, "mDirection ERROR!");
312                     }
313                 }
314                 item.totalCurrent = current;
315                 item.totalTotal = total;
316                 item.handoverInitiated =
317                         confirmation == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED;
318                 item.destination = destination;
319                 mNotifications.put(batchID, item);
320 
321                 if (V) {
322                     Log.v(TAG, "ID=" + item.id + "; batchID=" + batchID + "; totoalCurrent"
323                             + item.totalCurrent + "; totalTotal=" + item.totalTotal);
324                 }
325             }
326         }
327         cursor.close();
328 
329         // Add the notifications
330         for (NotificationItem item : mNotifications.values()) {
331             if (item.handoverInitiated) {
332                 float progress = 0;
333                 if (item.totalTotal == -1) {
334                     progress = -1;
335                 } else {
336                     progress = (float) item.totalCurrent / item.totalTotal;
337                 }
338 
339                 // Let NFC service deal with notifications for this transfer
340                 Intent intent = new Intent(Constants.ACTION_BT_OPP_TRANSFER_PROGRESS);
341                 if (item.direction == BluetoothShare.DIRECTION_INBOUND) {
342                     intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_DIRECTION,
343                             Constants.DIRECTION_BLUETOOTH_INCOMING);
344                 } else {
345                     intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_DIRECTION,
346                             Constants.DIRECTION_BLUETOOTH_OUTGOING);
347                 }
348                 intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_ID, item.id);
349                 intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_PROGRESS, progress);
350                 intent.putExtra(Constants.EXTRA_BT_OPP_ADDRESS, item.destination);
351                 mContext.sendBroadcast(intent, Constants.HANDOVER_STATUS_PERMISSION);
352                 continue;
353             }
354             // Build the notification object
355             // TODO: split description into two rows with filename in second row
356             Notification.Builder b = new Notification.Builder(mContext, OPP_NOTIFICATION_CHANNEL);
357             b.setOnlyAlertOnce(true);
358             b.setColor(mContext.getResources()
359                     .getColor(com.android.internal.R.color.system_notification_accent_color,
360                             mContext.getTheme()));
361             b.setContentTitle(item.description);
362             b.setSubText(
363                     BluetoothOppUtility.formatProgressText(item.totalTotal, item.totalCurrent));
364             if (item.totalTotal != 0) {
365                 if (V) {
366                     Log.v(TAG, "mCurrentBytes: " + item.totalCurrent + " mTotalBytes: "
367                             + item.totalTotal + " (" + (int) ((item.totalCurrent * 100)
368                             / item.totalTotal) + " %)");
369                 }
370                 b.setProgress(100, (int) ((item.totalCurrent * 100) / item.totalTotal),
371                         item.totalTotal == -1);
372             } else {
373                 b.setProgress(100, 100, item.totalTotal == -1);
374             }
375             b.setWhen(item.timeStamp);
376             if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) {
377                 b.setSmallIcon(android.R.drawable.stat_sys_upload);
378             } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) {
379                 b.setSmallIcon(android.R.drawable.stat_sys_download);
380             } else {
381                 if (V) {
382                     Log.v(TAG, "mDirection ERROR!");
383                 }
384             }
385             b.setOngoing(true);
386             b.setLocalOnly(true);
387 
388             Intent intent = new Intent(Constants.ACTION_LIST);
389             intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
390             intent.setDataAndNormalize(Uri.parse(BluetoothShare.CONTENT_URI + "/" + item.id));
391 
392             b.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0));
393             mNotificationMgr.notify(NOTIFICATION_ID_PROGRESS, b.build());
394         }
395     }
396 
updateCompletedNotification()397     private void updateCompletedNotification() {
398         long timeStamp = 0;
399         int outboundSuccNumber = 0;
400         int outboundFailNumber = 0;
401         int outboundNum;
402         int inboundNum;
403         int inboundSuccNumber = 0;
404         int inboundFailNumber = 0;
405 
406         // Creating outbound notification
407         Cursor cursor =
408                 mContentResolver.query(BluetoothShare.CONTENT_URI, null, WHERE_COMPLETED_OUTBOUND,
409                         null, BluetoothShare.TIMESTAMP + " DESC");
410         if (cursor == null) {
411             return;
412         }
413 
414         final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP);
415         final int statusIndex = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS);
416 
417         for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
418             if (cursor.isFirst()) {
419                 // Display the time for the latest transfer
420                 timeStamp = cursor.getLong(timestampIndex);
421             }
422             int status = cursor.getInt(statusIndex);
423 
424             if (BluetoothShare.isStatusError(status)) {
425                 outboundFailNumber++;
426             } else {
427                 outboundSuccNumber++;
428             }
429         }
430         if (V) {
431             Log.v(TAG, "outbound: succ-" + outboundSuccNumber + "  fail-" + outboundFailNumber);
432         }
433         cursor.close();
434 
435         outboundNum = outboundSuccNumber + outboundFailNumber;
436         // create the outbound notification
437         if (outboundNum > 0) {
438             String unsuccessCaption = mContext.getResources()
439                     .getQuantityString(R.plurals.noti_caption_unsuccessful, outboundFailNumber,
440                             outboundFailNumber);
441             String caption = mContext.getResources()
442                     .getQuantityString(R.plurals.noti_caption_success, outboundSuccNumber,
443                             outboundSuccNumber, unsuccessCaption);
444             Intent contentIntent = new Intent(Constants.ACTION_OPEN_OUTBOUND_TRANSFER).setClassName(
445                     Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
446             Intent deleteIntent = new Intent(Constants.ACTION_COMPLETE_HIDE).setClassName(
447                     Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
448             Notification outNoti =
449                     new Notification.Builder(mContext, OPP_NOTIFICATION_CHANNEL).setOnlyAlertOnce(
450                             true)
451                             .setContentTitle(mContext.getString(R.string.outbound_noti_title))
452                             .setContentText(caption)
453                             .setSmallIcon(android.R.drawable.stat_sys_upload_done)
454                             .setColor(mContext.getResources()
455                                     .getColor(
456                                             com.android.internal.R.color
457                                                     .system_notification_accent_color,
458                                             mContext.getTheme()))
459                             .setContentIntent(
460                                     PendingIntent.getBroadcast(mContext, 0, contentIntent, 0))
461                             .setDeleteIntent(
462                                     PendingIntent.getBroadcast(mContext, 0, deleteIntent, 0))
463                             .setWhen(timeStamp)
464                             .setLocalOnly(true)
465                             .build();
466             mNotificationMgr.notify(NOTIFICATION_ID_OUTBOUND_COMPLETE, outNoti);
467         } else {
468             if (mNotificationMgr != null) {
469                 mNotificationMgr.cancel(NOTIFICATION_ID_OUTBOUND_COMPLETE);
470                 if (V) {
471                     Log.v(TAG, "outbound notification was removed.");
472                 }
473             }
474         }
475 
476         // Creating inbound notification
477         cursor = mContentResolver.query(BluetoothShare.CONTENT_URI, null, WHERE_COMPLETED_INBOUND,
478                 null, BluetoothShare.TIMESTAMP + " DESC");
479         if (cursor == null) {
480             return;
481         }
482 
483         for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
484             if (cursor.isFirst()) {
485                 // Display the time for the latest transfer
486                 timeStamp = cursor.getLong(timestampIndex);
487             }
488             int status = cursor.getInt(statusIndex);
489 
490             if (BluetoothShare.isStatusError(status)) {
491                 inboundFailNumber++;
492             } else {
493                 inboundSuccNumber++;
494             }
495         }
496         if (V) {
497             Log.v(TAG, "inbound: succ-" + inboundSuccNumber + "  fail-" + inboundFailNumber);
498         }
499         cursor.close();
500 
501         inboundNum = inboundSuccNumber + inboundFailNumber;
502         // create the inbound notification
503         if (inboundNum > 0) {
504             String unsuccessCaption = mContext.getResources()
505                     .getQuantityString(R.plurals.noti_caption_unsuccessful, inboundFailNumber,
506                             inboundFailNumber);
507             String caption = mContext.getResources()
508                     .getQuantityString(R.plurals.noti_caption_success, inboundSuccNumber,
509                             inboundSuccNumber, unsuccessCaption);
510             Intent contentIntent = new Intent(Constants.ACTION_OPEN_INBOUND_TRANSFER).setClassName(
511                     Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
512             Intent deleteIntent = new Intent(Constants.ACTION_COMPLETE_HIDE).setClassName(
513                     Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
514             Notification inNoti =
515                     new Notification.Builder(mContext, OPP_NOTIFICATION_CHANNEL).setOnlyAlertOnce(
516                             true)
517                             .setContentTitle(mContext.getString(R.string.inbound_noti_title))
518                             .setContentText(caption)
519                             .setSmallIcon(android.R.drawable.stat_sys_download_done)
520                             .setColor(mContext.getResources()
521                                     .getColor(
522                                             com.android.internal.R.color
523                                                     .system_notification_accent_color,
524                                             mContext.getTheme()))
525                             .setContentIntent(
526                                     PendingIntent.getBroadcast(mContext, 0, contentIntent, 0))
527                             .setDeleteIntent(
528                                     PendingIntent.getBroadcast(mContext, 0, deleteIntent, 0))
529                             .setWhen(timeStamp)
530                             .setLocalOnly(true)
531                             .build();
532             mNotificationMgr.notify(NOTIFICATION_ID_INBOUND_COMPLETE, inNoti);
533         } else {
534             if (mNotificationMgr != null) {
535                 mNotificationMgr.cancel(NOTIFICATION_ID_INBOUND_COMPLETE);
536                 if (V) {
537                     Log.v(TAG, "inbound notification was removed.");
538                 }
539             }
540         }
541     }
542 
updateIncomingFileConfirmNotification()543     private void updateIncomingFileConfirmNotification() {
544         Cursor cursor =
545                 mContentResolver.query(BluetoothShare.CONTENT_URI, null, WHERE_CONFIRM_PENDING,
546                         null, BluetoothShare._ID);
547 
548         if (cursor == null) {
549             return;
550         }
551 
552         for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
553             BluetoothOppTransferInfo info = new BluetoothOppTransferInfo();
554             BluetoothOppUtility.fillRecord(mContext, cursor, info);
555             Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + info.mID);
556             Intent baseIntent = new Intent().setDataAndNormalize(contentUri)
557                     .setClassName(Constants.THIS_PACKAGE_NAME,
558                             BluetoothOppReceiver.class.getName());
559             Notification.Action actionDecline =
560                     new Notification.Action.Builder(R.drawable.ic_decline,
561                             mContext.getText(R.string.incoming_file_confirm_cancel),
562                             PendingIntent.getBroadcast(mContext, 0,
563                                     new Intent(baseIntent).setAction(Constants.ACTION_DECLINE),
564                                     0)).build();
565             Notification.Action actionAccept = new Notification.Action.Builder(R.drawable.ic_accept,
566                     mContext.getText(R.string.incoming_file_confirm_ok),
567                     PendingIntent.getBroadcast(mContext, 0,
568                             new Intent(baseIntent).setAction(Constants.ACTION_ACCEPT), 0)).build();
569             Notification n =
570                     new Notification.Builder(mContext, OPP_NOTIFICATION_CHANNEL).setOnlyAlertOnce(
571                             true)
572                             .setOngoing(true)
573                             .setWhen(info.mTimeStamp)
574                             .addAction(actionDecline)
575                             .addAction(actionAccept)
576                             .setContentIntent(PendingIntent.getBroadcast(mContext, 0,
577                                     new Intent(baseIntent).setAction(
578                                             Constants.ACTION_INCOMING_FILE_CONFIRM), 0))
579                             .setDeleteIntent(PendingIntent.getBroadcast(mContext, 0,
580                                     new Intent(baseIntent).setAction(Constants.ACTION_HIDE), 0))
581                             .setColor(mContext.getResources()
582                                     .getColor(
583                                             com.android.internal.R.color
584                                                     .system_notification_accent_color,
585                                             mContext.getTheme()))
586                             .setContentTitle(mContext.getText(
587                                     R.string.incoming_file_confirm_Notification_title))
588                             .setContentText(info.mFileName)
589                             .setStyle(new Notification.BigTextStyle().bigText(mContext.getString(
590                                     R.string.incoming_file_confirm_Notification_content,
591                                     info.mDeviceName, info.mFileName)))
592                             .setContentInfo(Formatter.formatFileSize(mContext, info.mTotalBytes))
593                             .setSmallIcon(R.drawable.bt_incomming_file_notification)
594                             .setLocalOnly(true)
595                             .build();
596             mNotificationMgr.notify(NOTIFICATION_ID_PROGRESS, n);
597         }
598         cursor.close();
599     }
600 
cancelNotifications()601     void cancelNotifications() {
602         if (V) {
603             Log.v(TAG, "cancelNotifications ");
604         }
605         mHandler.removeCallbacksAndMessages(null);
606         mNotificationMgr.cancelAll();
607     }
608 }
609