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