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 */ 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 */ 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 188 public NotificationUpdateThread() { 189 super("Notification Update Thread"); 190 } 191 192 @Override 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 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.setContentTitle(item.description); 318 b.setContentInfo( 319 BluetoothOppUtility.formatProgressText(item.totalTotal, item.totalCurrent)); 320 b.setProgress(item.totalTotal, item.totalCurrent, item.totalTotal == -1); 321 b.setWhen(item.timeStamp); 322 if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) { 323 b.setSmallIcon(android.R.drawable.stat_sys_upload); 324 } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) { 325 b.setSmallIcon(android.R.drawable.stat_sys_download); 326 } else { 327 if (V) Log.v(TAG, "mDirection ERROR!"); 328 } 329 b.setOngoing(true); 330 331 Intent intent = new Intent(Constants.ACTION_LIST); 332 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 333 intent.setDataAndNormalize(Uri.parse(BluetoothShare.CONTENT_URI + "/" + item.id)); 334 335 b.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0)); 336 mNotificationMgr.notify(item.id, b.getNotification()); 337 338 mActiveNotificationId = item.id; 339 } 340 } 341 342 private void updateCompletedNotification() { 343 String title; 344 String caption; 345 long timeStamp = 0; 346 int outboundSuccNumber = 0; 347 int outboundFailNumber = 0; 348 int outboundNum; 349 int inboundNum; 350 int inboundSuccNumber = 0; 351 int inboundFailNumber = 0; 352 Intent intent; 353 354 // If there is active transfer, no need to update complete transfer 355 // notification 356 if (!mUpdateCompleteNotification) { 357 if (V) Log.v(TAG, "No need to update complete notification"); 358 return; 359 } 360 361 // After merge complete notifications to 2 notifications, there is no 362 // chance to update the active notifications to complete notifications 363 // as before. So need cancel the active notification after the active 364 // transfer becomes complete. 365 if (mNotificationMgr != null && mActiveNotificationId != 0) { 366 mNotificationMgr.cancel(mActiveNotificationId); 367 if (V) Log.v(TAG, "ongoing transfer notification was removed"); 368 } 369 370 // Creating outbound notification 371 Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, 372 WHERE_COMPLETED_OUTBOUND, null, BluetoothShare.TIMESTAMP + " DESC"); 373 if (cursor == null) { 374 return; 375 } 376 377 final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP); 378 final int statusIndex = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS); 379 380 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 381 if (cursor.isFirst()) { 382 // Display the time for the latest transfer 383 timeStamp = cursor.getLong(timestampIndex); 384 } 385 int status = cursor.getInt(statusIndex); 386 387 if (BluetoothShare.isStatusError(status)) { 388 outboundFailNumber++; 389 } else { 390 outboundSuccNumber++; 391 } 392 } 393 if (V) Log.v(TAG, "outbound: succ-" + outboundSuccNumber + " fail-" + outboundFailNumber); 394 cursor.close(); 395 396 outboundNum = outboundSuccNumber + outboundFailNumber; 397 // create the outbound notification 398 if (outboundNum > 0) { 399 Notification outNoti = new Notification(); 400 outNoti.icon = android.R.drawable.stat_sys_upload_done; 401 title = mContext.getString(R.string.outbound_noti_title); 402 caption = mContext.getString(R.string.noti_caption, outboundSuccNumber, 403 outboundFailNumber); 404 intent = new Intent(Constants.ACTION_OPEN_OUTBOUND_TRANSFER); 405 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 406 outNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast( 407 mContext, 0, intent, 0)); 408 intent = new Intent(Constants.ACTION_COMPLETE_HIDE); 409 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 410 outNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 411 outNoti.when = timeStamp; 412 mNotificationMgr.notify(NOTIFICATION_ID_OUTBOUND, outNoti); 413 } else { 414 if (mNotificationMgr != null) { 415 mNotificationMgr.cancel(NOTIFICATION_ID_OUTBOUND); 416 if (V) Log.v(TAG, "outbound notification was removed."); 417 } 418 } 419 420 // Creating inbound notification 421 cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, 422 WHERE_COMPLETED_INBOUND, null, BluetoothShare.TIMESTAMP + " DESC"); 423 if (cursor == null) { 424 return; 425 } 426 427 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 428 if (cursor.isFirst()) { 429 // Display the time for the latest transfer 430 timeStamp = cursor.getLong(timestampIndex); 431 } 432 int status = cursor.getInt(statusIndex); 433 434 if (BluetoothShare.isStatusError(status)) { 435 inboundFailNumber++; 436 } else { 437 inboundSuccNumber++; 438 } 439 } 440 if (V) Log.v(TAG, "inbound: succ-" + inboundSuccNumber + " fail-" + inboundFailNumber); 441 cursor.close(); 442 443 inboundNum = inboundSuccNumber + inboundFailNumber; 444 // create the inbound notification 445 if (inboundNum > 0) { 446 Notification inNoti = new Notification(); 447 inNoti.icon = android.R.drawable.stat_sys_download_done; 448 title = mContext.getString(R.string.inbound_noti_title); 449 caption = mContext.getString(R.string.noti_caption, inboundSuccNumber, 450 inboundFailNumber); 451 intent = new Intent(Constants.ACTION_OPEN_INBOUND_TRANSFER); 452 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 453 inNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast( 454 mContext, 0, intent, 0)); 455 intent = new Intent(Constants.ACTION_COMPLETE_HIDE); 456 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 457 inNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 458 inNoti.when = timeStamp; 459 mNotificationMgr.notify(NOTIFICATION_ID_INBOUND, inNoti); 460 } else { 461 if (mNotificationMgr != null) { 462 mNotificationMgr.cancel(NOTIFICATION_ID_INBOUND); 463 if (V) Log.v(TAG, "inbound notification was removed."); 464 } 465 } 466 } 467 468 private void updateIncomingFileConfirmNotification() { 469 Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, 470 WHERE_CONFIRM_PENDING, null, BluetoothShare._ID); 471 472 if (cursor == null) { 473 return; 474 } 475 476 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 477 CharSequence title = 478 mContext.getText(R.string.incoming_file_confirm_Notification_title); 479 CharSequence caption = mContext 480 .getText(R.string.incoming_file_confirm_Notification_caption); 481 int id = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)); 482 long timeStamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)); 483 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id); 484 485 Notification n = new Notification(); 486 n.icon = R.drawable.bt_incomming_file_notification; 487 n.flags |= Notification.FLAG_ONLY_ALERT_ONCE; 488 n.flags |= Notification.FLAG_ONGOING_EVENT; 489 n.defaults = Notification.DEFAULT_SOUND; 490 n.tickerText = title; 491 492 Intent intent = new Intent(Constants.ACTION_INCOMING_FILE_CONFIRM); 493 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 494 intent.setDataAndNormalize(contentUri); 495 496 n.when = timeStamp; 497 n.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(mContext, 0, 498 intent, 0)); 499 500 intent = new Intent(Constants.ACTION_HIDE); 501 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 502 intent.setDataAndNormalize(contentUri); 503 n.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 504 505 mNotificationMgr.notify(id, n); 506 } 507 cursor.close(); 508 } 509 } 510