/* * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * - Neither the name of the Motorola, Inc. nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.android.bluetooth.opp; import com.android.bluetooth.R; import android.content.Context; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.util.Log; import android.widget.RemoteViews; import android.os.Handler; import android.os.Message; import android.os.Process; import java.util.HashMap; /** * This class handles the updating of the Notification Manager for the cases * where there is an ongoing transfer, incoming transfer need confirm and * complete (successful or failed) transfer. */ class BluetoothOppNotification { private static final String TAG = "BluetoothOppNotification"; private static final boolean V = Constants.VERBOSE; static final String status = "(" + BluetoothShare.STATUS + " == '192'" + ")"; static final String visible = "(" + BluetoothShare.VISIBILITY + " IS NULL OR " + BluetoothShare.VISIBILITY + " == '" + BluetoothShare.VISIBILITY_VISIBLE + "'" + ")"; static final String confirm = "(" + BluetoothShare.USER_CONFIRMATION + " == '" + BluetoothShare.USER_CONFIRMATION_CONFIRMED + "' OR " + BluetoothShare.USER_CONFIRMATION + " == '" + BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED + "'" + ")"; static final String WHERE_RUNNING = status + " AND " + visible + " AND " + confirm; static final String WHERE_COMPLETED = BluetoothShare.STATUS + " >= '200' AND " + visible; private static final String WHERE_COMPLETED_OUTBOUND = WHERE_COMPLETED + " AND " + "(" + BluetoothShare.DIRECTION + " == " + BluetoothShare.DIRECTION_OUTBOUND + ")"; private static final String WHERE_COMPLETED_INBOUND = WHERE_COMPLETED + " AND " + "(" + BluetoothShare.DIRECTION + " == " + BluetoothShare.DIRECTION_INBOUND + ")"; static final String WHERE_CONFIRM_PENDING = BluetoothShare.USER_CONFIRMATION + " == '" + BluetoothShare.USER_CONFIRMATION_PENDING + "'" + " AND " + visible; public NotificationManager mNotificationMgr; private Context mContext; private HashMap<String, NotificationItem> mNotifications; private NotificationUpdateThread mUpdateNotificationThread; private int mPendingUpdate = 0; private static final int NOTIFICATION_ID_OUTBOUND = -1000005; private static final int NOTIFICATION_ID_INBOUND = -1000006; private boolean mUpdateCompleteNotification = true; private int mActiveNotificationId = 0; /** * This inner class is used to describe some properties for one transfer. */ static class NotificationItem { int id; // This first field _id in db; int direction; // to indicate sending or receiving int totalCurrent = 0; // current transfer bytes int totalTotal = 0; // total bytes for current transfer int timeStamp = 0; // Database time stamp. Used for sorting ongoing transfers. String description; // the text above progress bar } /** * Constructor * * @param ctx The context to use to obtain access to the Notification * Service */ BluetoothOppNotification(Context ctx) { mContext = ctx; mNotificationMgr = (NotificationManager)mContext .getSystemService(Context.NOTIFICATION_SERVICE); mNotifications = new HashMap<String, NotificationItem>(); } /** * Update the notification ui. */ public void updateNotification() { synchronized (BluetoothOppNotification.this) { mPendingUpdate++; if (mPendingUpdate > 1) { if (V) Log.v(TAG, "update too frequent, put in queue"); return; } if (!mHandler.hasMessages(NOTIFY)) { if (V) Log.v(TAG, "send message"); mHandler.sendMessage(mHandler.obtainMessage(NOTIFY)); } } } private static final int NOTIFY = 0; // Use 1 second timer to limit notification frequency. // 1. On the first notification, create the update thread. // Buffer other updates. // 2. Update thread will clear mPendingUpdate. // 3. Handler sends a delayed message to self // 4. Handler checks if there are any more updates after 1 second. // 5. If there is an update, update it else stop. private Handler mHandler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case NOTIFY: synchronized (BluetoothOppNotification.this) { if (mPendingUpdate > 0 && mUpdateNotificationThread == null) { if (V) Log.v(TAG, "new notify threadi!"); mUpdateNotificationThread = new NotificationUpdateThread(); mUpdateNotificationThread.start(); if (V) Log.v(TAG, "send delay message"); mHandler.sendMessageDelayed(mHandler.obtainMessage(NOTIFY), 1000); } else if (mPendingUpdate > 0) { if (V) Log.v(TAG, "previous thread is not finished yet"); mHandler.sendMessageDelayed(mHandler.obtainMessage(NOTIFY), 1000); } break; } } } }; private class NotificationUpdateThread extends Thread { public NotificationUpdateThread() { super("Notification Update Thread"); } @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); synchronized (BluetoothOppNotification.this) { if (mUpdateNotificationThread != this) { throw new IllegalStateException( "multiple UpdateThreads in BluetoothOppNotification"); } mPendingUpdate = 0; } updateActiveNotification(); updateCompletedNotification(); updateIncomingFileConfirmNotification(); synchronized (BluetoothOppNotification.this) { mUpdateNotificationThread = null; } } } private void updateActiveNotification() { // Active transfers Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, WHERE_RUNNING, null, BluetoothShare._ID); if (cursor == null) { return; } // If there is active transfers, then no need to update completed transfer // notifications if (cursor.getCount() > 0) { mUpdateCompleteNotification = false; } else { mUpdateCompleteNotification = true; } if (V) Log.v(TAG, "mUpdateCompleteNotification = " + mUpdateCompleteNotification); // Collate the notifications final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP); final int directionIndex = cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION); final int idIndex = cursor.getColumnIndexOrThrow(BluetoothShare._ID); final int totalBytesIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES); final int currentBytesIndex = cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES); final int dataIndex = cursor.getColumnIndexOrThrow(BluetoothShare._DATA); final int filenameHintIndex = cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT); mNotifications.clear(); for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { int timeStamp = cursor.getInt(timestampIndex); int dir = cursor.getInt(directionIndex); int id = cursor.getInt(idIndex); int total = cursor.getInt(totalBytesIndex); int current = cursor.getInt(currentBytesIndex); String fileName = cursor.getString(dataIndex); if (fileName == null) { fileName = cursor.getString(filenameHintIndex); } if (fileName == null) { fileName = mContext.getString(R.string.unknown_file); } String batchID = Long.toString(timeStamp); // sending objects in one batch has same timeStamp if (mNotifications.containsKey(batchID)) { // NOTE: currently no such case // Batch sending case } else { NotificationItem item = new NotificationItem(); item.timeStamp = timeStamp; item.id = id; item.direction = dir; if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) { item.description = mContext.getString(R.string.notification_sending, fileName); } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) { item.description = mContext .getString(R.string.notification_receiving, fileName); } else { if (V) Log.v(TAG, "mDirection ERROR!"); } item.totalCurrent = current; item.totalTotal = total; mNotifications.put(batchID, item); if (V) Log.v(TAG, "ID=" + item.id + "; batchID=" + batchID + "; totoalCurrent" + item.totalCurrent + "; totalTotal=" + item.totalTotal); } } cursor.close(); // Add the notifications for (NotificationItem item : mNotifications.values()) { // Build the notification object // TODO: split description into two rows with filename in second row Notification.Builder b = new Notification.Builder(mContext); b.setContentTitle(item.description); b.setContentInfo( BluetoothOppUtility.formatProgressText(item.totalTotal, item.totalCurrent)); b.setProgress(item.totalTotal, item.totalCurrent, item.totalTotal == -1); b.setWhen(item.timeStamp); if (item.direction == BluetoothShare.DIRECTION_OUTBOUND) { b.setSmallIcon(android.R.drawable.stat_sys_upload); } else if (item.direction == BluetoothShare.DIRECTION_INBOUND) { b.setSmallIcon(android.R.drawable.stat_sys_download); } else { if (V) Log.v(TAG, "mDirection ERROR!"); } b.setOngoing(true); Intent intent = new Intent(Constants.ACTION_LIST); intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); intent.setData(Uri.parse(BluetoothShare.CONTENT_URI + "/" + item.id)); b.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0)); mNotificationMgr.notify(item.id, b.getNotification()); mActiveNotificationId = item.id; } } private void updateCompletedNotification() { String title; String caption; long timeStamp = 0; int outboundSuccNumber = 0; int outboundFailNumber = 0; int outboundNum; int inboundNum; int inboundSuccNumber = 0; int inboundFailNumber = 0; Intent intent; // If there is active transfer, no need to update complete transfer // notification if (!mUpdateCompleteNotification) { if (V) Log.v(TAG, "No need to update complete notification"); return; } // After merge complete notifications to 2 notifications, there is no // chance to update the active notifications to complete notifications // as before. So need cancel the active notification after the active // transfer becomes complete. if (mNotificationMgr != null && mActiveNotificationId != 0) { mNotificationMgr.cancel(mActiveNotificationId); if (V) Log.v(TAG, "ongoing transfer notification was removed"); } // Creating outbound notification Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, WHERE_COMPLETED_OUTBOUND, null, BluetoothShare.TIMESTAMP + " DESC"); if (cursor == null) { return; } final int timestampIndex = cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP); final int statusIndex = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS); for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { if (cursor.isFirst()) { // Display the time for the latest transfer timeStamp = cursor.getLong(timestampIndex); } int status = cursor.getInt(statusIndex); if (BluetoothShare.isStatusError(status)) { outboundFailNumber++; } else { outboundSuccNumber++; } } if (V) Log.v(TAG, "outbound: succ-" + outboundSuccNumber + " fail-" + outboundFailNumber); cursor.close(); outboundNum = outboundSuccNumber + outboundFailNumber; // create the outbound notification if (outboundNum > 0) { Notification outNoti = new Notification(); outNoti.icon = android.R.drawable.stat_sys_upload_done; title = mContext.getString(R.string.outbound_noti_title); caption = mContext.getString(R.string.noti_caption, outboundSuccNumber, outboundFailNumber); intent = new Intent(Constants.ACTION_OPEN_OUTBOUND_TRANSFER); intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); outNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast( mContext, 0, intent, 0)); intent = new Intent(Constants.ACTION_COMPLETE_HIDE); intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); outNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); outNoti.when = timeStamp; mNotificationMgr.notify(NOTIFICATION_ID_OUTBOUND, outNoti); } else { if (mNotificationMgr != null) { mNotificationMgr.cancel(NOTIFICATION_ID_OUTBOUND); if (V) Log.v(TAG, "outbound notification was removed."); } } // Creating inbound notification cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, WHERE_COMPLETED_INBOUND, null, BluetoothShare.TIMESTAMP + " DESC"); if (cursor == null) { return; } for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { if (cursor.isFirst()) { // Display the time for the latest transfer timeStamp = cursor.getLong(timestampIndex); } int status = cursor.getInt(statusIndex); if (BluetoothShare.isStatusError(status)) { inboundFailNumber++; } else { inboundSuccNumber++; } } if (V) Log.v(TAG, "inbound: succ-" + inboundSuccNumber + " fail-" + inboundFailNumber); cursor.close(); inboundNum = inboundSuccNumber + inboundFailNumber; // create the inbound notification if (inboundNum > 0) { Notification inNoti = new Notification(); inNoti.icon = android.R.drawable.stat_sys_download_done; title = mContext.getString(R.string.inbound_noti_title); caption = mContext.getString(R.string.noti_caption, inboundSuccNumber, inboundFailNumber); intent = new Intent(Constants.ACTION_OPEN_INBOUND_TRANSFER); intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); inNoti.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast( mContext, 0, intent, 0)); intent = new Intent(Constants.ACTION_COMPLETE_HIDE); intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); inNoti.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); inNoti.when = timeStamp; mNotificationMgr.notify(NOTIFICATION_ID_INBOUND, inNoti); } else { if (mNotificationMgr != null) { mNotificationMgr.cancel(NOTIFICATION_ID_INBOUND); if (V) Log.v(TAG, "inbound notification was removed."); } } } private void updateIncomingFileConfirmNotification() { Cursor cursor = mContext.getContentResolver().query(BluetoothShare.CONTENT_URI, null, WHERE_CONFIRM_PENDING, null, BluetoothShare._ID); if (cursor == null) { return; } for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { CharSequence title = mContext.getText(R.string.incoming_file_confirm_Notification_title); CharSequence caption = mContext .getText(R.string.incoming_file_confirm_Notification_caption); int id = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)); long timeStamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)); Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id); Notification n = new Notification(); n.icon = R.drawable.bt_incomming_file_notification; n.flags |= Notification.FLAG_ONLY_ALERT_ONCE; n.flags |= Notification.FLAG_ONGOING_EVENT; n.defaults = Notification.DEFAULT_SOUND; n.tickerText = title; Intent intent = new Intent(Constants.ACTION_INCOMING_FILE_CONFIRM); intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); intent.setData(contentUri); n.when = timeStamp; n.setLatestEventInfo(mContext, title, caption, PendingIntent.getBroadcast(mContext, 0, intent, 0)); intent = new Intent(Constants.ACTION_HIDE); intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); intent.setData(contentUri); n.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); mNotificationMgr.notify(id, n); } cursor.close(); } }