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