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