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