BluetoothOppNotification.java revision dec631a77ab9cc89c4d3867b80cfe300e7cf83e9
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 notification object 277 // TODO: split description into two rows with filename in second row 278 Notification.Builder b = new Notification.Builder(mContext); 279 b.setContentTitle(item.description); 280 b.setContentInfo( 281 BluetoothOppUtility.formatProgressText(item.totalTotal, item.totalCurrent)); 282 b.setProgress(item.totalTotal, item.totalCurrent, item.totalTotal == -1); 283 b.setWhen(item.timeStamp); 284 if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) { 285 b.setSmallIcon(android.R.drawable.stat_sys_upload); 286 } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) { 287 b.setSmallIcon(android.R.drawable.stat_sys_download); 288 } else { 289 if (V) Log.v(TAG, "mDirection ERROR!"); 290 } 291 b.setOngoing(true); 292 293 Intent intent = new Intent(Constants.ACTION_LIST); 294 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 295 intent.setData(Uri.parse(BluetoothShare.CONTENT_URI + "/" + item.id)); 296 297 b.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0)); 298 mNotificationMgr.notify(item.id, b.getNotification()); 299 300 mActiveNotificationId = item.id; 301 } 302 } 303 304 private void updateCompletedNotification() { 305 String title; 306 String caption; 307 long timeStamp = 0; 308 int outboundSuccNumber = 0; 309 int outboundFailNumber = 0; 310 int outboundNum; 311 int inboundNum; 312 int inboundSuccNumber = 0; 313 int inboundFailNumber = 0; 314 Intent intent; 315 316 // If there is active transfer, no need to update complete transfer 317 // notification 318 if (!mUpdateCompleteNotification) { 319 if (V) Log.v(TAG, "No need to update complete notification"); 320 return; 321 } 322 323 // After merge complete notifications to 2 notifications, there is no 324 // chance to update the active notifications to complete notifications 325 // as before. So need cancel the active notification after the active 326 // transfer becomes complete. 327 if (mNotificationMgr != null && mActiveNotificationId != 0) { 328 mNotificationMgr.cancel(mActiveNotificationId); 329 if (V) Log.v(TAG, "ongoing transfer notification was removed"); 330 } 331 332 // Creating outbound notification 333 Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, 334 WHERE_COMPLETED_OUTBOUND, null, BluetoothShare.TIMESTAMP + " DESC"); 335 if (cursor == null) { 336 return; 337 } 338 339 final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP); 340 final int statusIndex = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS); 341 342 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 343 if (cursor.isFirst()) { 344 // Display the time for the latest transfer 345 timeStamp = cursor.getLong(timestampIndex); 346 } 347 int status = cursor.getInt(statusIndex); 348 349 if (BluetoothShare.isStatusError(status)) { 350 outboundFailNumber++; 351 } else { 352 outboundSuccNumber++; 353 } 354 } 355 if (V) Log.v(TAG, "outbound: succ-" + outboundSuccNumber + " fail-" + outboundFailNumber); 356 cursor.close(); 357 358 outboundNum = outboundSuccNumber + outboundFailNumber; 359 // create the outbound notification 360 if (outboundNum > 0) { 361 Notification outNoti = new Notification(); 362 outNoti.icon = android.R.drawable.stat_sys_upload_done; 363 title = mContext.getString(R.string.outbound_noti_title); 364 caption = mContext.getString(R.string.noti_caption, outboundSuccNumber, 365 outboundFailNumber); 366 intent = new Intent(Constants.ACTION_OPEN_OUTBOUND_TRANSFER); 367 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 368 outNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast( 369 mContext, 0, intent, 0)); 370 intent = new Intent(Constants.ACTION_COMPLETE_HIDE); 371 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 372 outNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 373 outNoti.when = timeStamp; 374 mNotificationMgr.notify(NOTIFICATION_ID_OUTBOUND, outNoti); 375 } else { 376 if (mNotificationMgr != null) { 377 mNotificationMgr.cancel(NOTIFICATION_ID_OUTBOUND); 378 if (V) Log.v(TAG, "outbound notification was removed."); 379 } 380 } 381 382 // Creating inbound notification 383 cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, 384 WHERE_COMPLETED_INBOUND, null, BluetoothShare.TIMESTAMP + " DESC"); 385 if (cursor == null) { 386 return; 387 } 388 389 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 390 if (cursor.isFirst()) { 391 // Display the time for the latest transfer 392 timeStamp = cursor.getLong(timestampIndex); 393 } 394 int status = cursor.getInt(statusIndex); 395 396 if (BluetoothShare.isStatusError(status)) { 397 inboundFailNumber++; 398 } else { 399 inboundSuccNumber++; 400 } 401 } 402 if (V) Log.v(TAG, "inbound: succ-" + inboundSuccNumber + " fail-" + inboundFailNumber); 403 cursor.close(); 404 405 inboundNum = inboundSuccNumber + inboundFailNumber; 406 // create the inbound notification 407 if (inboundNum > 0) { 408 Notification inNoti = new Notification(); 409 inNoti.icon = android.R.drawable.stat_sys_download_done; 410 title = mContext.getString(R.string.inbound_noti_title); 411 caption = mContext.getString(R.string.noti_caption, inboundSuccNumber, 412 inboundFailNumber); 413 intent = new Intent(Constants.ACTION_OPEN_INBOUND_TRANSFER); 414 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 415 inNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast( 416 mContext, 0, intent, 0)); 417 intent = new Intent(Constants.ACTION_COMPLETE_HIDE); 418 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 419 inNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 420 inNoti.when = timeStamp; 421 mNotificationMgr.notify(NOTIFICATION_ID_INBOUND, inNoti); 422 } else { 423 if (mNotificationMgr != null) { 424 mNotificationMgr.cancel(NOTIFICATION_ID_INBOUND); 425 if (V) Log.v(TAG, "inbound notification was removed."); 426 } 427 } 428 } 429 430 private void updateIncomingFileConfirmNotification() { 431 Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, 432 WHERE_CONFIRM_PENDING, null, BluetoothShare._ID); 433 434 if (cursor == null) { 435 return; 436 } 437 438 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 439 CharSequence title = 440 mContext.getText(R.string.incoming_file_confirm_Notification_title); 441 CharSequence caption = mContext 442 .getText(R.string.incoming_file_confirm_Notification_caption); 443 int id = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)); 444 long timeStamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)); 445 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id); 446 447 Notification n = new Notification(); 448 n.icon = R.drawable.bt_incomming_file_notification; 449 n.flags |= Notification.FLAG_ONLY_ALERT_ONCE; 450 n.flags |= Notification.FLAG_ONGOING_EVENT; 451 n.defaults = Notification.DEFAULT_SOUND; 452 n.tickerText = title; 453 454 Intent intent = new Intent(Constants.ACTION_INCOMING_FILE_CONFIRM); 455 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 456 intent.setData(contentUri); 457 458 n.when = timeStamp; 459 n.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(mContext, 0, 460 intent, 0)); 461 462 intent = new Intent(Constants.ACTION_HIDE); 463 intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); 464 intent.setData(contentUri); 465 n.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); 466 467 mNotificationMgr.notify(id, n); 468 } 469 cursor.close(); 470 } 471} 472