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 com.android.bluetooth.R; 36 37 import android.content.Context; 38 import android.app.Notification; 39 import android.app.NotificationManager; 40 import android.app.PendingIntent; 41 import android.content.Intent; 42 import android.database.Cursor; 43 import android.net.Uri; 44 import android.util.Log; 45 import android.os.Handler; 46 import android.os.Message; 47 import android.os.Process; 48 import java.util.HashMap; 49 50 /** 51 * This class handles the updating of the Notification Manager for the cases 52 * where there is an ongoing transfer, incoming transfer need confirm and 53 * complete (successful or failed) transfer. 54 */ 55 class BluetoothOppNotification { 56 private static final String TAG = "BluetoothOppNotification"; 57 private static final boolean V = Constants.VERBOSE; 58 59 static final String status = "(" + BluetoothShare.STATUS + " == '192'" + ")"; 60 61 static final String visible = "(" + BluetoothShare.VISIBILITY + " IS NULL OR " 62 + BluetoothShare.VISIBILITY + " == '" + BluetoothShare.VISIBILITY_VISIBLE + "'" + ")"; 63 64 static final String confirm = "(" + BluetoothShare.USER_CONFIRMATION + " == '" 65 + BluetoothShare.USER_CONFIRMATION_CONFIRMED + "' OR " 66 + BluetoothShare.USER_CONFIRMATION + " == '" 67 + BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED + "' OR " 68 + BluetoothShare.USER_CONFIRMATION + " == '" 69 + BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED + "'" + ")"; 70 71 static final String not_through_handover = "(" + BluetoothShare.USER_CONFIRMATION + " != '" 72 + BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED + "'" + ")"; 73 74 static final String WHERE_RUNNING = status + " AND " + visible + " AND " + confirm; 75 76 static final String WHERE_COMPLETED = BluetoothShare.STATUS + " >= '200' AND " + visible + 77 " AND " + not_through_handover; // Don't show handover-initiated transfers 78 79 private static final String WHERE_COMPLETED_OUTBOUND = WHERE_COMPLETED + " AND " + "(" 80 + BluetoothShare.DIRECTION + " == " + BluetoothShare.DIRECTION_OUTBOUND + ")"; 81 82 private static final String WHERE_COMPLETED_INBOUND = WHERE_COMPLETED + " AND " + "(" 83 + BluetoothShare.DIRECTION + " == " + BluetoothShare.DIRECTION_INBOUND + ")"; 84 85 static final String WHERE_CONFIRM_PENDING = BluetoothShare.USER_CONFIRMATION + " == '" 86 + BluetoothShare.USER_CONFIRMATION_PENDING + "'" + " AND " + visible; 87 88 public NotificationManager mNotificationMgr; 89 90 private Context mContext; 91 92 private HashMap<String, NotificationItem> mNotifications; 93 94 private NotificationUpdateThread mUpdateNotificationThread; 95 96 private int mPendingUpdate = 0; 97 98 private static final int NOTIFICATION_ID_OUTBOUND = -1000005; 99 100 private static final int NOTIFICATION_ID_INBOUND = -1000006; 101 102 private boolean mUpdateCompleteNotification = true; 103 104 private int mActiveNotificationId = 0; 105 106 /** 107 * This inner class is used to describe some properties for one transfer. 108 */ 109 static class NotificationItem { 110 int id; // This first field _id in db; 111 112 int direction; // to indicate sending or receiving 113 114 int totalCurrent = 0; // current transfer bytes 115 116 int totalTotal = 0; // total bytes for current transfer 117 118 long timeStamp = 0; // Database time stamp. Used for sorting ongoing transfers. 119 120 String description; // the text above progress bar 121 122 boolean handoverInitiated = false; // transfer initiated by connection handover (eg NFC) 123 124 String destination; // destination associated with this transfer 125 } 126 127 /** 128 * Constructor 129 * 130 * @param ctx The context to use to obtain access to the Notification 131 * Service 132 */ BluetoothOppNotification(Context ctx)133 BluetoothOppNotification(Context ctx) { 134 mContext = ctx; 135 mNotificationMgr = (NotificationManager)mContext 136 .getSystemService(Context.NOTIFICATION_SERVICE); 137 mNotifications = new HashMap<String, NotificationItem>(); 138 } 139 140 /** 141 * Update the notification ui. 142 */ updateNotification()143 public void updateNotification() { 144 synchronized (BluetoothOppNotification.this) { 145 mPendingUpdate++; 146 if (mPendingUpdate > 1) { 147 if (V) Log.v(TAG, "update too frequent, put in queue"); 148 return; 149 } 150 if (!mHandler.hasMessages(NOTIFY)) { 151 if (V) Log.v(TAG, "send message"); 152 mHandler.sendMessage(mHandler.obtainMessage(NOTIFY)); 153 } 154 } 155 } 156 157 private static final int NOTIFY = 0; 158 // Use 1 second timer to limit notification frequency. 159 // 1. On the first notification, create the update thread. 160 // Buffer other updates. 161 // 2. Update thread will clear mPendingUpdate. 162 // 3. Handler sends a delayed message to self 163 // 4. Handler checks if there are any more updates after 1 second. 164 // 5. If there is an update, update it else stop. 165 private Handler mHandler = new Handler() { 166 public void handleMessage(Message msg) { 167 switch (msg.what) { 168 case NOTIFY: 169 synchronized (BluetoothOppNotification.this) { 170 if (mPendingUpdate > 0 && mUpdateNotificationThread == null) { 171 if (V) Log.v(TAG, "new notify threadi!"); 172 mUpdateNotificationThread = new NotificationUpdateThread(); 173 mUpdateNotificationThread.start(); 174 if (V) Log.v(TAG, "send delay message"); 175 mHandler.sendMessageDelayed(mHandler.obtainMessage(NOTIFY), 1000); 176 } else if (mPendingUpdate > 0) { 177 if (V) Log.v(TAG, "previous thread is not finished yet"); 178 mHandler.sendMessageDelayed(mHandler.obtainMessage(NOTIFY), 1000); 179 } 180 break; 181 } 182 } 183 } 184 }; 185 186 private class NotificationUpdateThread extends Thread { 187 NotificationUpdateThread()188 public NotificationUpdateThread() { 189 super("Notification Update Thread"); 190 } 191 192 @Override run()193 public void run() { 194 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 195 synchronized (BluetoothOppNotification.this) { 196 if (mUpdateNotificationThread != this) { 197 throw new IllegalStateException( 198 "multiple UpdateThreads in BluetoothOppNotification"); 199 } 200 mPendingUpdate = 0; 201 } 202 updateActiveNotification(); 203 updateCompletedNotification(); 204 updateIncomingFileConfirmNotification(); 205 synchronized (BluetoothOppNotification.this) { 206 mUpdateNotificationThread = null; 207 } 208 } 209 } 210 updateActiveNotification()211 private void updateActiveNotification() { 212 // Active transfers 213 Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, 214 WHERE_RUNNING, null, BluetoothShare._ID); 215 if (cursor == null) { 216 return; 217 } 218 219 // If there is active transfers, then no need to update completed transfer 220 // notifications 221 if (cursor.getCount() > 0) { 222 mUpdateCompleteNotification = false; 223 } else { 224 mUpdateCompleteNotification = true; 225 } 226 if (V) Log.v(TAG, "mUpdateCompleteNotification = " + mUpdateCompleteNotification); 227 228 // Collate the notifications 229 final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP); 230 final int directionIndex = cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION); 231 final int idIndex = cursor.getColumnIndexOrThrow(BluetoothShare._ID); 232 final int totalBytesIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES); 233 final int currentBytesIndex = cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES); 234 final int dataIndex = cursor.getColumnIndexOrThrow(BluetoothShare._DATA); 235 final int filenameHintIndex = cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT); 236 final int confirmIndex = cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION); 237 final int destinationIndex = cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION); 238 239 mNotifications.clear(); 240 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 241 long timeStamp = cursor.getLong(timestampIndex); 242 int dir = cursor.getInt(directionIndex); 243 int id = cursor.getInt(idIndex); 244 int total = cursor.getInt(totalBytesIndex); 245 int current = cursor.getInt(currentBytesIndex); 246 int confirmation = cursor.getInt(confirmIndex); 247 248 String destination = cursor.getString(destinationIndex); 249 String fileName = cursor.getString(dataIndex); 250 if (fileName == null) { 251 fileName = cursor.getString(filenameHintIndex); 252 } 253 if (fileName == null) { 254 fileName = mContext.getString(R.string.unknown_file); 255 } 256 257 String batchID = Long.toString(timeStamp); 258 259 // sending objects in one batch has same timeStamp 260 if (mNotifications.containsKey(batchID)) { 261 // NOTE: currently no such case 262 // Batch sending case 263 } else { 264 NotificationItem item = new NotificationItem(); 265 item.timeStamp = timeStamp; 266 item.id = id; 267 item.direction = dir; 268 if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) { 269 item.description = mContext.getString(R.string.notification_sending, fileName); 270 } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) { 271 item.description = mContext 272 .getString(R.string.notification_receiving, fileName); 273 } else { 274 if (V) Log.v(TAG, "mDirection ERROR!"); 275 } 276 item.totalCurrent = current; 277 item.totalTotal = total; 278 item.handoverInitiated = 279 confirmation == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED; 280 item.destination = destination; 281 mNotifications.put(batchID, item); 282 283 if (V) Log.v(TAG, "ID=" + item.id + "; batchID=" + batchID + "; totoalCurrent" 284 + item.totalCurrent + "; totalTotal=" + item.totalTotal); 285 } 286 } 287 cursor.close(); 288 289 // Add the notifications 290 for (NotificationItem item : mNotifications.values()) { 291 if (item.handoverInitiated) { 292 float progress = 0; 293 if (item.totalTotal == -1) { 294 progress = -1; 295 } else { 296 progress = (float)item.totalCurrent / item.totalTotal; 297 } 298 299 // Let NFC service deal with notifications for this transfer 300 Intent intent = new Intent(Constants.ACTION_BT_OPP_TRANSFER_PROGRESS); 301 if (item.direction == BluetoothShare.DIRECTION_INBOUND) { 302 intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_DIRECTION, 303 Constants.DIRECTION_BLUETOOTH_INCOMING); 304 } else { 305 intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_DIRECTION, 306 Constants.DIRECTION_BLUETOOTH_OUTGOING); 307 } 308 intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_ID, item.id); 309 intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_PROGRESS, progress); 310 intent.putExtra(Constants.EXTRA_BT_OPP_ADDRESS, item.destination); 311 mContext.sendBroadcast(intent, Constants.HANDOVER_STATUS_PERMISSION); 312 continue; 313 } 314 // Build the notification object 315 // TODO: split description into two rows with filename in second row 316 Notification.Builder b = new Notification.Builder(mContext); 317 b.setColor(mContext.getResources().getColor( 318 com.android.internal.R.color.system_notification_accent_color)); 319 b.setContentTitle(item.description); 320 b.setContentInfo( 321 BluetoothOppUtility.formatProgressText(item.totalTotal, item.totalCurrent)); 322 b.setProgress(item.totalTotal, item.totalCurrent, item.totalTotal == -1); 323 b.setWhen(item.timeStamp); 324 if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) { 325 b.setSmallIcon(android.R.drawable.stat_sys_upload); 326 } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) { 327 b.setSmallIcon(android.R.drawable.stat_sys_download); 328 } else { 329 if (V) Log.v(TAG, "mDirection ERROR!"); 330 } 331 b.setOngoing(true); 332 333 Intent intent = new Intent(Constants.ACTION_LIST); 334 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 335 intent.setDataAndNormalize(Uri.parse(BluetoothShare.CONTENT_URI + "/" + item.id)); 336 337 b.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0)); 338 mNotificationMgr.notify(item.id, b.getNotification()); 339 340 mActiveNotificationId = item.id; 341 } 342 } 343 updateCompletedNotification()344 private void updateCompletedNotification() { 345 String title; 346 String caption; 347 long timeStamp = 0; 348 int outboundSuccNumber = 0; 349 int outboundFailNumber = 0; 350 int outboundNum; 351 int inboundNum; 352 int inboundSuccNumber = 0; 353 int inboundFailNumber = 0; 354 Intent intent; 355 356 // If there is active transfer, no need to update complete transfer 357 // notification 358 if (!mUpdateCompleteNotification) { 359 if (V) Log.v(TAG, "No need to update complete notification"); 360 return; 361 } 362 363 // After merge complete notifications to 2 notifications, there is no 364 // chance to update the active notifications to complete notifications 365 // as before. So need cancel the active notification after the active 366 // transfer becomes complete. 367 if (mNotificationMgr != null && mActiveNotificationId != 0) { 368 mNotificationMgr.cancel(mActiveNotificationId); 369 if (V) Log.v(TAG, "ongoing transfer notification was removed"); 370 } 371 372 // Creating outbound notification 373 Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, 374 WHERE_COMPLETED_OUTBOUND, null, BluetoothShare.TIMESTAMP + " DESC"); 375 if (cursor == null) { 376 return; 377 } 378 379 final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP); 380 final int statusIndex = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS); 381 382 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 383 if (cursor.isFirst()) { 384 // Display the time for the latest transfer 385 timeStamp = cursor.getLong(timestampIndex); 386 } 387 int status = cursor.getInt(statusIndex); 388 389 if (BluetoothShare.isStatusError(status)) { 390 outboundFailNumber++; 391 } else { 392 outboundSuccNumber++; 393 } 394 } 395 if (V) Log.v(TAG, "outbound: succ-" + outboundSuccNumber + " fail-" + outboundFailNumber); 396 cursor.close(); 397 398 outboundNum = outboundSuccNumber + outboundFailNumber; 399 // create the outbound notification 400 if (outboundNum > 0) { 401 Notification outNoti = new Notification(); 402 outNoti.icon = android.R.drawable.stat_sys_upload_done; 403 title = mContext.getString(R.string.outbound_noti_title); 404 caption = mContext.getString(R.string.noti_caption, outboundSuccNumber, 405 outboundFailNumber); 406 intent = new Intent(Constants.ACTION_OPEN_OUTBOUND_TRANSFER); 407 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 408 outNoti.color = mContext.getResources().getColor( 409 com.android.internal.R.color.system_notification_accent_color); 410 outNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast( 411 mContext, 0, intent, 0)); 412 intent = new Intent(Constants.ACTION_COMPLETE_HIDE); 413 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 414 outNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 415 outNoti.when = timeStamp; 416 mNotificationMgr.notify(NOTIFICATION_ID_OUTBOUND, outNoti); 417 } else { 418 if (mNotificationMgr != null) { 419 mNotificationMgr.cancel(NOTIFICATION_ID_OUTBOUND); 420 if (V) Log.v(TAG, "outbound notification was removed."); 421 } 422 } 423 424 // Creating inbound notification 425 cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, 426 WHERE_COMPLETED_INBOUND, null, BluetoothShare.TIMESTAMP + " DESC"); 427 if (cursor == null) { 428 return; 429 } 430 431 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 432 if (cursor.isFirst()) { 433 // Display the time for the latest transfer 434 timeStamp = cursor.getLong(timestampIndex); 435 } 436 int status = cursor.getInt(statusIndex); 437 438 if (BluetoothShare.isStatusError(status)) { 439 inboundFailNumber++; 440 } else { 441 inboundSuccNumber++; 442 } 443 } 444 if (V) Log.v(TAG, "inbound: succ-" + inboundSuccNumber + " fail-" + inboundFailNumber); 445 cursor.close(); 446 447 inboundNum = inboundSuccNumber + inboundFailNumber; 448 // create the inbound notification 449 if (inboundNum > 0) { 450 Notification inNoti = new Notification(); 451 inNoti.icon = android.R.drawable.stat_sys_download_done; 452 title = mContext.getString(R.string.inbound_noti_title); 453 caption = mContext.getString(R.string.noti_caption, inboundSuccNumber, 454 inboundFailNumber); 455 intent = new Intent(Constants.ACTION_OPEN_INBOUND_TRANSFER); 456 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 457 inNoti.color = mContext.getResources().getColor( 458 com.android.internal.R.color.system_notification_accent_color); 459 inNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast( 460 mContext, 0, intent, 0)); 461 intent = new Intent(Constants.ACTION_COMPLETE_HIDE); 462 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 463 inNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 464 inNoti.when = timeStamp; 465 mNotificationMgr.notify(NOTIFICATION_ID_INBOUND, inNoti); 466 } else { 467 if (mNotificationMgr != null) { 468 mNotificationMgr.cancel(NOTIFICATION_ID_INBOUND); 469 if (V) Log.v(TAG, "inbound notification was removed."); 470 } 471 } 472 } 473 updateIncomingFileConfirmNotification()474 private void updateIncomingFileConfirmNotification() { 475 Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, 476 WHERE_CONFIRM_PENDING, null, BluetoothShare._ID); 477 478 if (cursor == null) { 479 return; 480 } 481 482 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 483 CharSequence title = 484 mContext.getText(R.string.incoming_file_confirm_Notification_title); 485 CharSequence caption = mContext 486 .getText(R.string.incoming_file_confirm_Notification_caption); 487 int id = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)); 488 long timeStamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)); 489 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id); 490 491 Notification n = new Notification(); 492 n.icon = R.drawable.bt_incomming_file_notification; 493 n.flags |= Notification.FLAG_ONLY_ALERT_ONCE; 494 n.flags |= Notification.FLAG_ONGOING_EVENT; 495 n.defaults = Notification.DEFAULT_SOUND; 496 n.tickerText = title; 497 498 Intent intent = new Intent(Constants.ACTION_INCOMING_FILE_CONFIRM); 499 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 500 intent.setDataAndNormalize(contentUri); 501 502 n.when = timeStamp; 503 n.color = mContext.getResources().getColor( 504 com.android.internal.R.color.system_notification_accent_color); 505 n.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(mContext, 0, 506 intent, 0)); 507 508 intent = new Intent(Constants.ACTION_HIDE); 509 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 510 intent.setDataAndNormalize(contentUri); 511 n.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 512 513 mNotificationMgr.notify(id, n); 514 } 515 cursor.close(); 516 } 517 } 518