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