/* * 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 android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Handler; import android.os.Message; import android.os.Process; import android.text.format.Formatter; import android.util.Log; import com.android.bluetooth.R; 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 + "' OR " + BluetoothShare.USER_CONFIRMATION + " == '" + BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED + "'" + ")"; static final String NOT_THROUGH_HANDOVER = "(" + BluetoothShare.USER_CONFIRMATION + " != '" + BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED + "'" + ")"; static final String WHERE_RUNNING = STATUS + " AND " + VISIBLE + " AND " + CONFIRM; static final String WHERE_COMPLETED = BluetoothShare.STATUS + " >= '200' AND " + VISIBLE + " AND " + NOT_THROUGH_HANDOVER; // Don't show handover-initiated transfers 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 NotificationChannel mNotificationChannel; private static final String OPP_NOTIFICATION_CHANNEL = "opp_notification_channel"; private Context mContext; private HashMap mNotifications; private NotificationUpdateThread mUpdateNotificationThread; private int mPendingUpdate = 0; public static final int NOTIFICATION_ID_PROGRESS = -1000004; private static final int NOTIFICATION_ID_OUTBOUND_COMPLETE = -1000005; private static final int NOTIFICATION_ID_INBOUND_COMPLETE = -1000006; private boolean mUpdateCompleteNotification = true; private ContentResolver mContentResolver = null; /** * This inner class is used to describe some properties for one transfer. */ static class NotificationItem { public int id; // This first field _id in db; public int direction; // to indicate sending or receiving public long totalCurrent = 0; // current transfer bytes public long totalTotal = 0; // total bytes for current transfer public long timeStamp = 0; // Database time stamp. Used for sorting ongoing transfers. public String description; // the text above progress bar public boolean handoverInitiated = false; // transfer initiated by connection handover (eg NFC) public String destination; // destination associated with this transfer } /** * 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); mNotificationChannel = new NotificationChannel(OPP_NOTIFICATION_CHANNEL, mContext.getString(R.string.opp_notification_group), NotificationManager.IMPORTANCE_HIGH); mNotificationMgr.createNotificationChannel(mNotificationChannel); mNotifications = new HashMap(); // Get Content Resolver object one time mContentResolver = mContext.getContentResolver(); } /** * 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() { @Override 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 { 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 = mContentResolver.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); final int confirmIndex = cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION); final int destinationIndex = cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION); mNotifications.clear(); for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { long timeStamp = cursor.getLong(timestampIndex); int dir = cursor.getInt(directionIndex); int id = cursor.getInt(idIndex); long total = cursor.getLong(totalBytesIndex); long current = cursor.getLong(currentBytesIndex); int confirmation = cursor.getInt(confirmIndex); String destination = cursor.getString(destinationIndex); 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; item.handoverInitiated = confirmation == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED; item.destination = destination; 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()) { if (item.handoverInitiated) { float progress = 0; if (item.totalTotal == -1) { progress = -1; } else { progress = (float) item.totalCurrent / item.totalTotal; } // Let NFC service deal with notifications for this transfer Intent intent = new Intent(Constants.ACTION_BT_OPP_TRANSFER_PROGRESS); if (item.direction == BluetoothShare.DIRECTION_INBOUND) { intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_DIRECTION, Constants.DIRECTION_BLUETOOTH_INCOMING); } else { intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_DIRECTION, Constants.DIRECTION_BLUETOOTH_OUTGOING); } intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_ID, item.id); intent.putExtra(Constants.EXTRA_BT_OPP_TRANSFER_PROGRESS, progress); intent.putExtra(Constants.EXTRA_BT_OPP_ADDRESS, item.destination); mContext.sendBroadcast(intent, Constants.HANDOVER_STATUS_PERMISSION); continue; } // Build the notification object // TODO: split description into two rows with filename in second row Notification.Builder b = new Notification.Builder(mContext, OPP_NOTIFICATION_CHANNEL); b.setOnlyAlertOnce(true); b.setColor(mContext.getResources() .getColor(com.android.internal.R.color.system_notification_accent_color, mContext.getTheme())); b.setContentTitle(item.description); b.setSubText( BluetoothOppUtility.formatProgressText(item.totalTotal, item.totalCurrent)); if (item.totalTotal != 0) { if (V) { Log.v(TAG, "mCurrentBytes: " + item.totalCurrent + " mTotalBytes: " + item.totalTotal + " (" + (int) ((item.totalCurrent * 100) / item.totalTotal) + " %)"); } b.setProgress(100, (int) ((item.totalCurrent * 100) / item.totalTotal), item.totalTotal == -1); } else { b.setProgress(100, 100, 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); b.setLocalOnly(true); Intent intent = new Intent(Constants.ACTION_LIST); intent.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); intent.setDataAndNormalize(Uri.parse(BluetoothShare.CONTENT_URI + "/" + item.id)); b.setContentIntent(PendingIntent.getBroadcast(mContext, 0, intent, 0)); mNotificationMgr.notify(NOTIFICATION_ID_PROGRESS, b.build()); } } private void updateCompletedNotification() { long timeStamp = 0; int outboundSuccNumber = 0; int outboundFailNumber = 0; int outboundNum; int inboundNum; int inboundSuccNumber = 0; int inboundFailNumber = 0; // Creating outbound notification Cursor cursor = mContentResolver.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) { String unsuccessCaption = mContext.getResources() .getQuantityString(R.plurals.noti_caption_unsuccessful, outboundFailNumber, outboundFailNumber); String caption = mContext.getResources() .getQuantityString(R.plurals.noti_caption_success, outboundSuccNumber, outboundSuccNumber, unsuccessCaption); Intent contentIntent = new Intent(Constants.ACTION_OPEN_OUTBOUND_TRANSFER).setClassName( Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); Intent deleteIntent = new Intent(Constants.ACTION_COMPLETE_HIDE).setClassName( Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); Notification outNoti = new Notification.Builder(mContext, OPP_NOTIFICATION_CHANNEL).setOnlyAlertOnce( true) .setContentTitle(mContext.getString(R.string.outbound_noti_title)) .setContentText(caption) .setSmallIcon(android.R.drawable.stat_sys_upload_done) .setColor(mContext.getResources() .getColor( com.android.internal.R.color .system_notification_accent_color, mContext.getTheme())) .setContentIntent( PendingIntent.getBroadcast(mContext, 0, contentIntent, 0)) .setDeleteIntent( PendingIntent.getBroadcast(mContext, 0, deleteIntent, 0)) .setWhen(timeStamp) .setLocalOnly(true) .build(); mNotificationMgr.notify(NOTIFICATION_ID_OUTBOUND_COMPLETE, outNoti); } else { if (mNotificationMgr != null) { mNotificationMgr.cancel(NOTIFICATION_ID_OUTBOUND_COMPLETE); if (V) { Log.v(TAG, "outbound notification was removed."); } } } // Creating inbound notification cursor = mContentResolver.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) { String unsuccessCaption = mContext.getResources() .getQuantityString(R.plurals.noti_caption_unsuccessful, inboundFailNumber, inboundFailNumber); String caption = mContext.getResources() .getQuantityString(R.plurals.noti_caption_success, inboundSuccNumber, inboundSuccNumber, unsuccessCaption); Intent contentIntent = new Intent(Constants.ACTION_OPEN_INBOUND_TRANSFER).setClassName( Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); Intent deleteIntent = new Intent(Constants.ACTION_COMPLETE_HIDE).setClassName( Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); Notification inNoti = new Notification.Builder(mContext, OPP_NOTIFICATION_CHANNEL).setOnlyAlertOnce( true) .setContentTitle(mContext.getString(R.string.inbound_noti_title)) .setContentText(caption) .setSmallIcon(android.R.drawable.stat_sys_download_done) .setColor(mContext.getResources() .getColor( com.android.internal.R.color .system_notification_accent_color, mContext.getTheme())) .setContentIntent( PendingIntent.getBroadcast(mContext, 0, contentIntent, 0)) .setDeleteIntent( PendingIntent.getBroadcast(mContext, 0, deleteIntent, 0)) .setWhen(timeStamp) .setLocalOnly(true) .build(); mNotificationMgr.notify(NOTIFICATION_ID_INBOUND_COMPLETE, inNoti); } else { if (mNotificationMgr != null) { mNotificationMgr.cancel(NOTIFICATION_ID_INBOUND_COMPLETE); if (V) { Log.v(TAG, "inbound notification was removed."); } } } } private void updateIncomingFileConfirmNotification() { Cursor cursor = mContentResolver.query(BluetoothShare.CONTENT_URI, null, WHERE_CONFIRM_PENDING, null, BluetoothShare._ID); if (cursor == null) { return; } for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { BluetoothOppTransferInfo info = new BluetoothOppTransferInfo(); BluetoothOppUtility.fillRecord(mContext, cursor, info); Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + info.mID); Intent baseIntent = new Intent().setDataAndNormalize(contentUri) .setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName()); Notification.Action actionDecline = new Notification.Action.Builder(Icon.createWithResource(mContext, R.drawable.ic_decline), mContext.getText(R.string.incoming_file_confirm_cancel), PendingIntent.getBroadcast(mContext, 0, new Intent(baseIntent).setAction(Constants.ACTION_DECLINE), 0)).build(); Notification.Action actionAccept = new Notification.Action.Builder( Icon.createWithResource(mContext,R.drawable.ic_accept), mContext.getText(R.string.incoming_file_confirm_ok), PendingIntent.getBroadcast(mContext, 0, new Intent(baseIntent).setAction(Constants.ACTION_ACCEPT), 0)).build(); Notification n = new Notification.Builder(mContext, OPP_NOTIFICATION_CHANNEL).setOnlyAlertOnce( true) .setOngoing(true) .setWhen(info.mTimeStamp) .addAction(actionDecline) .addAction(actionAccept) .setContentIntent(PendingIntent.getBroadcast(mContext, 0, new Intent(baseIntent).setAction( Constants.ACTION_INCOMING_FILE_CONFIRM), 0)) .setDeleteIntent(PendingIntent.getBroadcast(mContext, 0, new Intent(baseIntent).setAction(Constants.ACTION_HIDE), 0)) .setColor(mContext.getResources() .getColor( com.android.internal.R.color .system_notification_accent_color, mContext.getTheme())) .setContentTitle(mContext.getText( R.string.incoming_file_confirm_Notification_title)) .setContentText(info.mFileName) .setStyle(new Notification.BigTextStyle().bigText(mContext.getString( R.string.incoming_file_confirm_Notification_content, info.mDeviceName, info.mFileName))) .setSubText(Formatter.formatFileSize(mContext, info.mTotalBytes)) .setSmallIcon(R.drawable.bt_incomming_file_notification) .setLocalOnly(true) .build(); mNotificationMgr.notify(NOTIFICATION_ID_PROGRESS, n); } cursor.close(); } void cancelNotifications() { if (V) { Log.v(TAG, "cancelNotifications "); } mHandler.removeCallbacksAndMessages(null); mNotificationMgr.cancelAll(); } }