1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.messenger; 18 19 import android.app.NotificationManager; 20 import android.app.PendingIntent; 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothMapClient; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.net.Uri; 29 import android.os.Parcel; 30 import android.os.Parcelable; 31 import android.support.annotation.Nullable; 32 import android.support.v4.app.NotificationCompat; 33 import android.util.Log; 34 import android.widget.Toast; 35 36 import java.util.HashMap; 37 import java.util.Iterator; 38 import java.util.LinkedList; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Objects; 42 import java.util.function.Predicate; 43 44 /** 45 * Component that monitors for incoming messages and posts/updates notifications. 46 * <p> 47 * It also handles notifications requests e.g. sending auto-replies and message play-out. 48 * <p> 49 * It will receive broadcasts for new incoming messages as long as the MapClient is connected in 50 * {@link MessengerService}. 51 */ 52 class MapMessageMonitor { 53 private static final String TAG = "Messenger.MsgMonitor"; 54 private static final boolean DBG = MessengerService.DBG; 55 56 private final Context mContext; 57 private final BluetoothMapReceiver mBluetoothMapReceiver; 58 private final NotificationManager mNotificationManager; 59 private final Map<MessageKey, MapMessage> mMessages = new HashMap<>(); 60 private final Map<SenderKey, NotificationInfo> mNotificationInfos = new HashMap<>(); 61 private final TTSHelper mTTSHelper; 62 63 MapMessageMonitor(Context context) { 64 mContext = context; 65 mBluetoothMapReceiver = new BluetoothMapReceiver(); 66 mNotificationManager = 67 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 68 mTTSHelper = new TTSHelper(mContext); 69 } 70 71 private void handleNewMessage(Intent intent) { 72 if (DBG) { 73 Log.d(TAG, "Handling new message"); 74 } 75 try { 76 MapMessage message = MapMessage.parseFrom(intent); 77 if (MessengerService.VDBG) { 78 Log.v(TAG, "Parsed message: " + message); 79 } 80 MessageKey messageKey = new MessageKey(message); 81 boolean repeatMessage = mMessages.containsKey(messageKey); 82 mMessages.put(messageKey, message); 83 if (!repeatMessage) { 84 updateNotificationInfo(message, messageKey); 85 } 86 } catch (IllegalArgumentException e) { 87 Log.e(TAG, "Dropping invalid MAP message", e); 88 } 89 } 90 91 private void updateNotificationInfo(MapMessage message, MessageKey messageKey) { 92 SenderKey senderKey = new SenderKey(message); 93 NotificationInfo notificationInfo = mNotificationInfos.get(senderKey); 94 if (notificationInfo == null) { 95 notificationInfo = 96 new NotificationInfo(message.getSenderName(), message.getSenderContactUri()); 97 mNotificationInfos.put(senderKey, notificationInfo); 98 } 99 notificationInfo.mMessageKeys.add(messageKey); 100 updateNotificationFor(senderKey, notificationInfo, false /* ttsPlaying */); 101 } 102 103 private void updateNotificationFor(SenderKey senderKey, 104 NotificationInfo notificationInfo, boolean ttsPlaying) { 105 NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext); 106 // TODO(sriniv): Use right icon when switching to correct layout. b/33280056. 107 builder.setSmallIcon(android.R.drawable.btn_plus); 108 builder.setContentTitle(notificationInfo.mSenderName); 109 builder.setContentText(mContext.getResources().getQuantityString( 110 R.plurals.notification_new_message, notificationInfo.mMessageKeys.size(), 111 notificationInfo.mMessageKeys.size())); 112 113 Intent deleteIntent = new Intent(mContext, MessengerService.class) 114 .setAction(MessengerService.ACTION_CLEAR_NOTIFICATION_STATE) 115 .putExtra(MessengerService.EXTRA_SENDER_KEY, senderKey); 116 builder.setDeleteIntent( 117 PendingIntent.getService(mContext, notificationInfo.mNotificationId, deleteIntent, 118 PendingIntent.FLAG_UPDATE_CURRENT)); 119 120 String messageActions[] = { 121 MessengerService.ACTION_AUTO_REPLY, 122 MessengerService.ACTION_PLAY_MESSAGES 123 }; 124 // TODO(sriniv): Actual spec does not have any of these strings. Remove later. b/33280056. 125 // is implemented for notifications. 126 String actionTexts[] = { "Reply", "Play" }; 127 if (ttsPlaying) { 128 messageActions[1] = MessengerService.ACTION_STOP_PLAYOUT; 129 actionTexts[1] = "Stop"; 130 } 131 for (int i = 0; i < messageActions.length; i++) { 132 Intent intent = new Intent(mContext, MessengerService.class) 133 .setAction(messageActions[i]) 134 .putExtra(MessengerService.EXTRA_SENDER_KEY, senderKey); 135 PendingIntent pendingIntent = PendingIntent.getService(mContext, 136 notificationInfo.mNotificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); 137 builder.addAction(android.R.drawable.ic_media_play, actionTexts[i], pendingIntent); 138 } 139 mNotificationManager.notify(notificationInfo.mNotificationId, builder.build()); 140 } 141 142 void clearNotificationState(SenderKey senderKey) { 143 if (DBG) { 144 Log.d(TAG, "Clearing notification state for: " + senderKey); 145 } 146 mNotificationInfos.remove(senderKey); 147 } 148 149 void playMessages(SenderKey senderKey) { 150 NotificationInfo notificationInfo = mNotificationInfos.get(senderKey); 151 if (notificationInfo == null) { 152 Log.e(TAG, "Unknown senderKey! " + senderKey); 153 return; 154 } 155 156 StringBuilder ttsMessage = new StringBuilder(); 157 ttsMessage.append(notificationInfo.mSenderName) 158 .append(" ").append(mContext.getString(R.string.tts_says_verb)); 159 for (MessageKey messageKey : notificationInfo.mMessageKeys) { 160 MapMessage message = mMessages.get(messageKey); 161 if (message != null) { 162 ttsMessage.append(". ").append(message.getText()); 163 } 164 } 165 166 mTTSHelper.requestPlay(ttsMessage.toString(), 167 new TTSHelper.Listener() { 168 @Override 169 public void onTTSStarted() { 170 updateNotificationFor(senderKey, notificationInfo, true); 171 } 172 173 @Override 174 public void onTTSStopped() { 175 updateNotificationFor(senderKey, notificationInfo, false); 176 } 177 178 @Override 179 public void onTTSError() { 180 Toast.makeText(mContext, R.string.tts_failed_toast, Toast.LENGTH_SHORT).show(); 181 onTTSStopped(); 182 } 183 }); 184 } 185 186 void stopPlayout() { 187 mTTSHelper.requestStop(); 188 } 189 190 boolean sendAutoReply(SenderKey senderKey, BluetoothMapClient mapClient) { 191 if (DBG) { 192 Log.d(TAG, "Sending auto-reply to: " + senderKey); 193 } 194 BluetoothDevice device = 195 BluetoothAdapter.getDefaultAdapter().getRemoteDevice(senderKey.mDeviceAddress); 196 NotificationInfo notificationInfo = mNotificationInfos.get(senderKey); 197 if (notificationInfo == null) { 198 Log.w(TAG, "No notificationInfo found for senderKey: " + senderKey); 199 return false; 200 } 201 if (notificationInfo.mSenderContactUri == null) { 202 Log.w(TAG, "Do not have contact URI for sender!"); 203 return false; 204 } 205 Uri recipientUris[] = { Uri.parse(notificationInfo.mSenderContactUri) }; 206 207 final int requestCode = senderKey.hashCode(); 208 PendingIntent sentIntent = 209 PendingIntent.getBroadcast(mContext, requestCode, new Intent( 210 BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY), 211 PendingIntent.FLAG_ONE_SHOT); 212 String message = mContext.getString(R.string.auto_reply_message); 213 return mapClient.sendMessage(device, recipientUris, message, sentIntent, null); 214 } 215 216 void handleMapDisconnect() { 217 cleanupMessagesAndNotifications((key) -> true); 218 } 219 220 void handleDeviceDisconnect(BluetoothDevice device) { 221 cleanupMessagesAndNotifications((key) -> key.matches(device.getAddress())); 222 } 223 224 private void cleanupMessagesAndNotifications(Predicate<CompositeKey> predicate) { 225 Iterator<Map.Entry<MessageKey, MapMessage>> messageIt = mMessages.entrySet().iterator(); 226 while (messageIt.hasNext()) { 227 if (predicate.test(messageIt.next().getKey())) { 228 messageIt.remove(); 229 } 230 } 231 Iterator<Map.Entry<SenderKey, NotificationInfo>> notificationIt = 232 mNotificationInfos.entrySet().iterator(); 233 while (notificationIt.hasNext()) { 234 Map.Entry<SenderKey, NotificationInfo> entry = notificationIt.next(); 235 if (predicate.test(entry.getKey())) { 236 mNotificationManager.cancel(entry.getValue().mNotificationId); 237 notificationIt.remove(); 238 } 239 } 240 } 241 242 void cleanup() { 243 mBluetoothMapReceiver.cleanup(); 244 mTTSHelper.cleanup(); 245 } 246 247 // Used to monitor for new incoming messages and sent-message broadcast. 248 private class BluetoothMapReceiver extends BroadcastReceiver { 249 BluetoothMapReceiver() { 250 if (DBG) { 251 Log.d(TAG, "Registering receiver for new messages"); 252 } 253 IntentFilter intentFilter = new IntentFilter(); 254 intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY); 255 intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED); 256 mContext.registerReceiver(this, intentFilter); 257 } 258 259 void cleanup() { 260 mContext.unregisterReceiver(this); 261 } 262 263 @Override 264 public void onReceive(Context context, Intent intent) { 265 if (BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY.equals(intent.getAction())) { 266 if (DBG) { 267 Log.d(TAG, "SMS was sent successfully!"); 268 } 269 } else if (BluetoothMapClient.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) { 270 handleNewMessage(intent); 271 } else { 272 Log.w(TAG, "Ignoring unknown broadcast " + intent.getAction()); 273 } 274 } 275 } 276 277 /** 278 * Key used in HashMap that is composed from a BT device-address and device-specific "sub key" 279 */ 280 private abstract static class CompositeKey { 281 final String mDeviceAddress; 282 final String mSubKey; 283 284 CompositeKey(String deviceAddress, String subKey) { 285 mDeviceAddress = deviceAddress; 286 mSubKey = subKey; 287 } 288 289 @Override 290 public boolean equals(Object o) { 291 if (this == o) { 292 return true; 293 } 294 if (o == null || getClass() != o.getClass()) { 295 return false; 296 } 297 298 CompositeKey that = (CompositeKey) o; 299 return Objects.equals(mDeviceAddress, that.mDeviceAddress) 300 && Objects.equals(mSubKey, that.mSubKey); 301 } 302 303 boolean matches(String deviceAddress) { 304 return mDeviceAddress.equals(deviceAddress); 305 } 306 307 @Override 308 public int hashCode() { 309 return Objects.hash(mDeviceAddress, mSubKey); 310 } 311 312 @Override 313 public String toString() { 314 return String.format("%s, deviceAddress: %s, subKey: %s", 315 getClass().getSimpleName(), mDeviceAddress, mSubKey); 316 } 317 } 318 319 /** 320 * {@link CompositeKey} subclass used to identify specific messages; it uses message-handle as 321 * the secondary key. 322 */ 323 private static class MessageKey extends CompositeKey { 324 MessageKey(MapMessage message) { 325 super(message.getDevice().getAddress(), message.getHandle()); 326 } 327 } 328 329 /** 330 * CompositeKey used to identify Notification info for a sender; it uses a combination of 331 * senderContactUri and senderContactName as the secondary key. 332 */ 333 static class SenderKey extends CompositeKey implements Parcelable { 334 private SenderKey(String deviceAddress, String key) { 335 super(deviceAddress, key); 336 } 337 338 SenderKey(MapMessage message) { 339 // Use a combination of senderName and senderContactUri for key. Ideally we would use 340 // only senderContactUri (which is encoded phone no.). However since some phones don't 341 // provide these, we fall back to senderName. Since senderName may not be unique, we 342 // include senderContactUri also to provide uniqueness in cases it is available. 343 this(message.getDevice().getAddress(), 344 message.getSenderName() + "/" + message.getSenderContactUri()); 345 } 346 347 @Override 348 public int describeContents() { 349 return 0; 350 } 351 352 @Override 353 public void writeToParcel(Parcel dest, int flags) { 354 dest.writeString(mDeviceAddress); 355 dest.writeString(mSubKey); 356 } 357 358 public static final Parcelable.Creator<SenderKey> CREATOR = 359 new Parcelable.Creator<SenderKey>() { 360 @Override 361 public SenderKey createFromParcel(Parcel source) { 362 return new SenderKey(source.readString(), source.readString()); 363 } 364 365 @Override 366 public SenderKey[] newArray(int size) { 367 return new SenderKey[size]; 368 } 369 }; 370 } 371 372 /** 373 * Information about a single notification that is displayed. 374 */ 375 private static class NotificationInfo { 376 private static int NEXT_NOTIFICATION_ID = 0; 377 378 final int mNotificationId = NEXT_NOTIFICATION_ID++; 379 final String mSenderName; 380 @Nullable 381 final String mSenderContactUri; 382 final List<MessageKey> mMessageKeys = new LinkedList<>(); 383 384 NotificationInfo(String senderName, @Nullable String senderContactUri) { 385 mSenderName = senderName; 386 mSenderContactUri = senderContactUri; 387 } 388 } 389 } 390