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