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