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