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