Home | History | Annotate | Download | only in opp
      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