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