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