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