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.Notification; 20 import android.app.NotificationChannel; 21 import android.app.NotificationManager; 22 import android.app.PendingIntent; 23 import android.bluetooth.BluetoothAdapter; 24 import android.bluetooth.BluetoothDevice; 25 import android.bluetooth.BluetoothMapClient; 26 import android.bluetooth.BluetoothUuid; 27 import android.bluetooth.SdpMasRecord; 28 import android.content.BroadcastReceiver; 29 import android.content.ContentResolver; 30 import android.content.ContentUris; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.IntentFilter; 34 import android.database.Cursor; 35 import android.graphics.Bitmap; 36 import android.graphics.drawable.Drawable; 37 import android.graphics.drawable.Icon; 38 import android.media.AudioManager; 39 import android.net.Uri; 40 import android.os.Parcel; 41 import android.os.Parcelable; 42 import android.provider.ContactsContract; 43 import android.provider.Settings; 44 import android.support.annotation.Nullable; 45 import android.text.TextUtils; 46 import android.util.Log; 47 import android.widget.Toast; 48 49 import com.android.car.apps.common.LetterTileDrawable; 50 import com.android.car.messenger.tts.TTSHelper; 51 52 import com.bumptech.glide.Glide; 53 import com.bumptech.glide.request.RequestOptions; 54 import com.bumptech.glide.request.target.SimpleTarget; 55 import com.bumptech.glide.request.transition.Transition; 56 57 import java.util.ArrayList; 58 import java.util.HashMap; 59 import java.util.Iterator; 60 import java.util.LinkedList; 61 import java.util.List; 62 import java.util.Map; 63 import java.util.Objects; 64 import java.util.function.Predicate; 65 import java.util.stream.Collectors; 66 67 /** 68 * Monitors for incoming messages and posts/updates notifications. 69 * <p> 70 * It also handles notifications requests e.g. sending auto-replies and message play-out. 71 * <p> 72 * It will receive broadcasts for new incoming messages as long as the MapClient is connected in 73 * {@link MessengerService}. 74 */ 75 class MapMessageMonitor { 76 public static final String ACTION_MESSAGE_PLAY_START = 77 "car.messenger.action_message_play_start"; 78 public static final String ACTION_MESSAGE_PLAY_STOP = "car.messenger.action_message_play_stop"; 79 // reply or "upload" feature is indicated by the 3rd bit 80 private static final int REPLY_FEATURE_POS = 3; 81 82 private static final int REQUEST_CODE_VOICE_PLATE = 1; 83 private static final int REQUEST_CODE_AUTO_REPLY = 2; 84 private static final int ACTION_COUNT = 2; 85 private static final String TAG = "Messenger.MsgMonitor"; 86 private static final boolean DBG = MessengerService.DBG; 87 88 private final Context mContext; 89 private final BluetoothMapReceiver mBluetoothMapReceiver; 90 private final BluetoothSdpReceiver mBluetoothSdpReceiver; 91 private final NotificationManager mNotificationManager; 92 private final Map<MessageKey, MapMessage> mMessages = new HashMap<>(); 93 private final Map<SenderKey, NotificationInfo> mNotificationInfos = new HashMap<>(); 94 private final TTSHelper mTTSHelper; 95 private final HashMap<String, Boolean> mReplyFeatureMap = new HashMap<>(); 96 private final AudioManager mAudioManager; 97 private final AudioManager.OnAudioFocusChangeListener mNoOpAFChangeListener = (f) -> {}; 98 MapMessageMonitor(Context context)99 MapMessageMonitor(Context context) { 100 mContext = context; 101 mBluetoothMapReceiver = new BluetoothMapReceiver(); 102 mBluetoothSdpReceiver = new BluetoothSdpReceiver(); 103 mNotificationManager = 104 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 105 mTTSHelper = new TTSHelper(mContext); 106 107 mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 108 } 109 isPlaying()110 public boolean isPlaying() { 111 return mTTSHelper.isSpeaking(); 112 } 113 handleNewMessage(Intent intent)114 private void handleNewMessage(Intent intent) { 115 if (DBG) { 116 Log.d(TAG, "Handling new message"); 117 } 118 try { 119 MapMessage message = MapMessage.parseFrom(intent); 120 if (MessengerService.DBG) { 121 Log.v(TAG, "Parsed message: " + message); 122 } 123 MessageKey messageKey = new MessageKey(message); 124 boolean repeatMessage = mMessages.containsKey(messageKey); 125 mMessages.put(messageKey, message); 126 if (!repeatMessage) { 127 updateNotificationInfo(message, messageKey); 128 } 129 } catch (IllegalArgumentException e) { 130 Log.e(TAG, "Dropping invalid MAP message", e); 131 } 132 } 133 updateNotificationInfo(MapMessage message, MessageKey messageKey)134 private void updateNotificationInfo(MapMessage message, MessageKey messageKey) { 135 SenderKey senderKey = new SenderKey(message); 136 // check the version/feature of the 137 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 138 adapter.getRemoteDevice(senderKey.mDeviceAddress).sdpSearch(BluetoothUuid.MAS); 139 140 NotificationInfo notificationInfo = mNotificationInfos.get(senderKey); 141 if (notificationInfo == null) { 142 notificationInfo = 143 new NotificationInfo(message.getSenderName(), message.getSenderContactUri()); 144 mNotificationInfos.put(senderKey, notificationInfo); 145 } 146 notificationInfo.mMessageKeys.add(messageKey); 147 updateNotificationFor(senderKey, notificationInfo); 148 } 149 150 private static final String[] CONTACT_ID = new String[] { 151 ContactsContract.PhoneLookup._ID 152 }; 153 getContactIdFromName(ContentResolver cr, String name)154 private static int getContactIdFromName(ContentResolver cr, String name) { 155 if (DBG) { 156 Log.d(TAG, "getting contactId for: " + name); 157 } 158 if (TextUtils.isEmpty(name)) { 159 return 0; 160 } 161 162 String[] mSelectionArgs = { name }; 163 164 Cursor cursor = 165 cr.query( 166 ContactsContract.Contacts.CONTENT_URI, 167 CONTACT_ID, 168 ContactsContract.Contacts.DISPLAY_NAME_PRIMARY + " LIKE ?", 169 mSelectionArgs, 170 null); 171 try { 172 if (cursor != null && cursor.moveToFirst()) { 173 int id = cursor.getInt(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID)); 174 return id; 175 } 176 } finally { 177 if (cursor != null) { 178 cursor.close(); 179 } 180 } 181 return 0; 182 } 183 updateNotificationFor(SenderKey senderKey, NotificationInfo notificationInfo)184 private void updateNotificationFor(SenderKey senderKey, NotificationInfo notificationInfo) { 185 if (DBG) { 186 Log.d(TAG, "updateNotificationFor" + notificationInfo); 187 } 188 String contentText = mContext.getResources().getQuantityString( 189 R.plurals.notification_new_message, notificationInfo.mMessageKeys.size(), 190 notificationInfo.mMessageKeys.size()); 191 long lastReceivedTimeMs = 192 mMessages.get(notificationInfo.mMessageKeys.getLast()).getReceivedTimeMs(); 193 194 Uri photoUri = ContentUris.withAppendedId( 195 ContactsContract.Contacts.CONTENT_URI, getContactIdFromName( 196 mContext.getContentResolver(), notificationInfo.mSenderName)); 197 if (DBG) { 198 Log.d(TAG, "start Glide loading... " + photoUri); 199 } 200 Glide.with(mContext) 201 .asBitmap() 202 .load(photoUri) 203 .apply(RequestOptions.circleCropTransform()) 204 .into(new SimpleTarget<Bitmap>() { 205 @Override 206 public void onResourceReady(Bitmap bitmap, 207 Transition<? super Bitmap> transition) { 208 sendNotification(bitmap); 209 } 210 211 @Override 212 public void onLoadFailed(@Nullable Drawable fallback) { 213 sendNotification(null); 214 } 215 216 private void sendNotification(Bitmap bitmap) { 217 if (DBG) { 218 Log.d(TAG, "Glide loaded. " + bitmap); 219 } 220 if (bitmap == null) { 221 LetterTileDrawable letterTileDrawable = 222 new LetterTileDrawable(mContext.getResources()); 223 letterTileDrawable.setContactDetails( 224 notificationInfo.mSenderName, notificationInfo.mSenderName); 225 letterTileDrawable.setIsCircular(true); 226 bitmap = letterTileDrawable.toBitmap( 227 mContext.getResources().getDimensionPixelSize( 228 R.dimen.notification_contact_photo_size)); 229 } 230 PendingIntent LaunchPlayMessageActivityIntent = PendingIntent.getActivity( 231 mContext, 232 REQUEST_CODE_VOICE_PLATE, 233 getPlayMessageIntent(senderKey, notificationInfo), 234 0); 235 236 Notification.Builder builder = new Notification.Builder( 237 mContext, NotificationChannel.DEFAULT_CHANNEL_ID) 238 .setContentIntent(LaunchPlayMessageActivityIntent) 239 .setLargeIcon(bitmap) 240 .setSmallIcon(R.drawable.ic_message) 241 .setContentTitle(notificationInfo.mSenderName) 242 .setContentText(contentText) 243 .setWhen(lastReceivedTimeMs) 244 .setShowWhen(true) 245 .setActions(getActionsFor(senderKey, notificationInfo)) 246 .setDeleteIntent(buildIntentFor( 247 MessengerService.ACTION_CLEAR_NOTIFICATION_STATE, 248 senderKey, notificationInfo)); 249 if (notificationInfo.muted) { 250 builder.setPriority(Notification.PRIORITY_MIN); 251 } else { 252 builder.setPriority(Notification.PRIORITY_HIGH) 253 .setSound(Settings.System.DEFAULT_NOTIFICATION_URI); 254 } 255 mNotificationManager.notify( 256 notificationInfo.mNotificationId, builder.build()); 257 } 258 }); 259 } 260 getPlayMessageIntent(SenderKey senderKey, NotificationInfo notificationInfo)261 private Intent getPlayMessageIntent(SenderKey senderKey, NotificationInfo notificationInfo) { 262 Intent intent = new Intent(mContext, PlayMessageActivity.class); 263 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 264 intent.putExtra(PlayMessageActivity.EXTRA_MESSAGE_KEY, senderKey); 265 intent.putExtra( 266 PlayMessageActivity.EXTRA_SENDER_NAME, 267 notificationInfo.mSenderName); 268 if (!supportsReply(senderKey.mDeviceAddress)) { 269 intent.putExtra( 270 PlayMessageActivity.EXTRA_REPLY_DISABLED_FLAG, 271 true); 272 } 273 return intent; 274 } 275 supportsReply(String deviceAddress)276 private boolean supportsReply(String deviceAddress) { 277 return mReplyFeatureMap.containsKey(deviceAddress) 278 && mReplyFeatureMap.get(deviceAddress); 279 } 280 getActionsFor( SenderKey senderKey, NotificationInfo notificationInfo)281 private Notification.Action[] getActionsFor( 282 SenderKey senderKey, 283 NotificationInfo notificationInfo) { 284 // Icon doesn't appear to be used; using fixed icon for all actions. 285 final Icon icon = Icon.createWithResource(mContext, android.R.drawable.ic_media_play); 286 287 List<Notification.Action.Builder> builders = new ArrayList<>(ACTION_COUNT); 288 289 // show auto reply options of device supports it 290 if (supportsReply(senderKey.mDeviceAddress)) { 291 Intent replyIntent = getPlayMessageIntent(senderKey, notificationInfo); 292 replyIntent.putExtra(PlayMessageActivity.EXTRA_SHOW_REPLY_LIST_FLAG, true); 293 PendingIntent autoReplyIntent = PendingIntent.getActivity( 294 mContext, REQUEST_CODE_AUTO_REPLY, replyIntent, 0); 295 builders.add(new Notification.Action.Builder(icon, 296 mContext.getString(R.string.action_reply), autoReplyIntent)); 297 } 298 299 // add mute/unmute. 300 if (notificationInfo.muted) { 301 PendingIntent muteIntent = buildIntentFor(MessengerService.ACTION_UNMUTE_CONVERSATION, 302 senderKey, notificationInfo); 303 builders.add(new Notification.Action.Builder(icon, 304 mContext.getString(R.string.action_unmute), muteIntent)); 305 } else { 306 PendingIntent muteIntent = buildIntentFor(MessengerService.ACTION_MUTE_CONVERSATION, 307 senderKey, notificationInfo); 308 builders.add(new Notification.Action.Builder(icon, 309 mContext.getString(R.string.action_mute), muteIntent)); 310 } 311 312 Notification.Action actions[] = new Notification.Action[builders.size()]; 313 for (int i = 0; i < builders.size(); i++) { 314 actions[i] = builders.get(i).build(); 315 } 316 return actions; 317 } 318 buildIntentFor(String action, SenderKey senderKey, NotificationInfo notificationInfo)319 private PendingIntent buildIntentFor(String action, SenderKey senderKey, 320 NotificationInfo notificationInfo) { 321 Intent intent = new Intent(mContext, MessengerService.class) 322 .setAction(action) 323 .putExtra(MessengerService.EXTRA_SENDER_KEY, senderKey); 324 return PendingIntent.getService(mContext, 325 notificationInfo.mNotificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); 326 } 327 clearNotificationState(SenderKey senderKey)328 void clearNotificationState(SenderKey senderKey) { 329 if (DBG) { 330 Log.d(TAG, "Clearing notification state for: " + senderKey); 331 } 332 mNotificationInfos.remove(senderKey); 333 } 334 playMessages(SenderKey senderKey)335 void playMessages(SenderKey senderKey) { 336 NotificationInfo notificationInfo = mNotificationInfos.get(senderKey); 337 if (notificationInfo == null) { 338 Log.e(TAG, "Unknown senderKey! " + senderKey); 339 return; 340 } 341 List<CharSequence> ttsMessages = new ArrayList<>(); 342 // TODO: play unread messages instead of the last. 343 String ttsMessage = 344 notificationInfo.mMessageKeys.stream().map((key) -> mMessages.get(key).getText()) 345 .collect(Collectors.toCollection(LinkedList::new)).getLast(); 346 // Insert something like "foo says" before their message content. 347 ttsMessages.add(mContext.getString(R.string.tts_sender_says, notificationInfo.mSenderName)); 348 ttsMessages.add(ttsMessage); 349 350 int result = mAudioManager.requestAudioFocus(mNoOpAFChangeListener, 351 // Use the music stream. 352 AudioManager.STREAM_MUSIC, 353 // Request permanent focus. 354 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); 355 if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 356 mTTSHelper.requestPlay(ttsMessages, 357 new TTSHelper.Listener() { 358 @Override 359 public void onTTSStarted() { 360 Intent intent = new Intent(ACTION_MESSAGE_PLAY_START); 361 mContext.sendBroadcast(intent); 362 } 363 364 @Override 365 public void onTTSStopped(boolean error) { 366 mAudioManager.abandonAudioFocus(mNoOpAFChangeListener); 367 Intent intent = new Intent(ACTION_MESSAGE_PLAY_STOP); 368 mContext.sendBroadcast(intent); 369 if (error) { 370 Toast.makeText(mContext, R.string.tts_failed_toast, 371 Toast.LENGTH_SHORT).show(); 372 } 373 } 374 }); 375 } else { 376 Log.w(TAG, "failed to require audio focus."); 377 } 378 } 379 stopPlayout()380 void stopPlayout() { 381 mTTSHelper.requestStop(); 382 } 383 toggleMuteConversation(SenderKey senderKey, boolean mute)384 void toggleMuteConversation(SenderKey senderKey, boolean mute) { 385 NotificationInfo notificationInfo = mNotificationInfos.get(senderKey); 386 if (notificationInfo == null) { 387 Log.e(TAG, "Unknown senderKey! " + senderKey); 388 return; 389 } 390 notificationInfo.muted = mute; 391 updateNotificationFor(senderKey, notificationInfo); 392 } 393 sendAutoReply(SenderKey senderKey, BluetoothMapClient mapClient, String message)394 boolean sendAutoReply(SenderKey senderKey, BluetoothMapClient mapClient, String message) { 395 if (DBG) { 396 Log.d(TAG, "Sending auto-reply to: " + senderKey); 397 } 398 BluetoothDevice device = 399 BluetoothAdapter.getDefaultAdapter().getRemoteDevice(senderKey.mDeviceAddress); 400 NotificationInfo notificationInfo = mNotificationInfos.get(senderKey); 401 if (notificationInfo == null) { 402 Log.w(TAG, "No notificationInfo found for senderKey: " + senderKey); 403 return false; 404 } 405 if (notificationInfo.mSenderContactUri == null) { 406 Log.w(TAG, "Do not have contact URI for sender!"); 407 return false; 408 } 409 Uri recipientUris[] = { Uri.parse(notificationInfo.mSenderContactUri) }; 410 411 final int requestCode = senderKey.hashCode(); 412 PendingIntent sentIntent = 413 PendingIntent.getBroadcast(mContext, requestCode, new Intent( 414 BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY), 415 PendingIntent.FLAG_ONE_SHOT); 416 return mapClient.sendMessage(device, recipientUris, message, sentIntent, null); 417 } 418 handleMapDisconnect()419 void handleMapDisconnect() { 420 cleanupMessagesAndNotifications((key) -> true); 421 } 422 handleDeviceDisconnect(BluetoothDevice device)423 void handleDeviceDisconnect(BluetoothDevice device) { 424 cleanupMessagesAndNotifications((key) -> key.matches(device.getAddress())); 425 } 426 cleanupMessagesAndNotifications(Predicate<CompositeKey> predicate)427 private void cleanupMessagesAndNotifications(Predicate<CompositeKey> predicate) { 428 Iterator<Map.Entry<MessageKey, MapMessage>> messageIt = mMessages.entrySet().iterator(); 429 while (messageIt.hasNext()) { 430 if (predicate.test(messageIt.next().getKey())) { 431 messageIt.remove(); 432 } 433 } 434 Iterator<Map.Entry<SenderKey, NotificationInfo>> notificationIt = 435 mNotificationInfos.entrySet().iterator(); 436 while (notificationIt.hasNext()) { 437 Map.Entry<SenderKey, NotificationInfo> entry = notificationIt.next(); 438 if (predicate.test(entry.getKey())) { 439 mNotificationManager.cancel(entry.getValue().mNotificationId); 440 notificationIt.remove(); 441 } 442 } 443 } 444 cleanup()445 void cleanup() { 446 mBluetoothMapReceiver.cleanup(); 447 mBluetoothSdpReceiver.cleanup(); 448 mTTSHelper.cleanup(); 449 } 450 451 private class BluetoothSdpReceiver extends BroadcastReceiver { BluetoothSdpReceiver()452 BluetoothSdpReceiver() { 453 if (DBG) { 454 Log.d(TAG, "Registering receiver for sdp"); 455 } 456 IntentFilter intentFilter = new IntentFilter(); 457 intentFilter.addAction(BluetoothDevice.ACTION_SDP_RECORD); 458 mContext.registerReceiver(this, intentFilter); 459 } 460 cleanup()461 void cleanup() { 462 mContext.unregisterReceiver(this); 463 } 464 465 @Override onReceive(Context context, Intent intent)466 public void onReceive(Context context, Intent intent) { 467 if (BluetoothDevice.ACTION_SDP_RECORD.equals(intent.getAction())) { 468 if (DBG) { 469 Log.d(TAG, "get SDP record: " + intent.getExtras()); 470 } 471 Parcelable parcelable = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD); 472 if (!(parcelable instanceof SdpMasRecord)) { 473 if (DBG) { 474 Log.d(TAG, "not SdpMasRecord: " + parcelable); 475 } 476 return; 477 } 478 SdpMasRecord masRecord = (SdpMasRecord) parcelable; 479 int features = masRecord.getSupportedFeatures(); 480 int version = masRecord.getProfileVersion(); 481 boolean supportsReply = false; 482 // we only consider the device supports reply feature of the version 483 // is higher than 1.02 and the feature flag is turned on. 484 if (version >= 0x102 && isOn(features, REPLY_FEATURE_POS)) { 485 supportsReply = true; 486 } 487 BluetoothDevice bluetoothDevice = 488 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 489 mReplyFeatureMap.put(bluetoothDevice.getAddress(), supportsReply); 490 } else { 491 Log.w(TAG, "Ignoring unknown broadcast " + intent.getAction()); 492 } 493 } 494 isOn(int input, int postion)495 private boolean isOn(int input, int postion) { 496 return ((input >> postion) & 1) == 1; 497 } 498 } 499 500 // Used to monitor for new incoming messages and sent-message broadcast. 501 private class BluetoothMapReceiver extends BroadcastReceiver { BluetoothMapReceiver()502 BluetoothMapReceiver() { 503 if (DBG) { 504 Log.d(TAG, "Registering receiver for bluetooth MAP"); 505 } 506 IntentFilter intentFilter = new IntentFilter(); 507 intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY); 508 intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED); 509 mContext.registerReceiver(this, intentFilter); 510 } 511 cleanup()512 void cleanup() { 513 mContext.unregisterReceiver(this); 514 } 515 516 @Override onReceive(Context context, Intent intent)517 public void onReceive(Context context, Intent intent) { 518 if (BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY.equals(intent.getAction())) { 519 if (DBG) { 520 Log.d(TAG, "SMS was sent successfully!"); 521 } 522 } else if (BluetoothMapClient.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) { 523 if (DBG) { 524 Log.d(TAG, "SMS message received"); 525 } 526 handleNewMessage(intent); 527 } else { 528 Log.w(TAG, "Ignoring unknown broadcast " + intent.getAction()); 529 } 530 } 531 } 532 533 /** 534 * Key used in HashMap that is composed from a BT device-address and device-specific "sub key" 535 */ 536 private abstract static class CompositeKey { 537 final String mDeviceAddress; 538 final String mSubKey; 539 CompositeKey(String deviceAddress, String subKey)540 CompositeKey(String deviceAddress, String subKey) { 541 mDeviceAddress = deviceAddress; 542 mSubKey = subKey; 543 } 544 545 @Override equals(Object o)546 public boolean equals(Object o) { 547 if (this == o) { 548 return true; 549 } 550 if (o == null || getClass() != o.getClass()) { 551 return false; 552 } 553 554 CompositeKey that = (CompositeKey) o; 555 return Objects.equals(mDeviceAddress, that.mDeviceAddress) 556 && Objects.equals(mSubKey, that.mSubKey); 557 } 558 matches(String deviceAddress)559 boolean matches(String deviceAddress) { 560 return mDeviceAddress.equals(deviceAddress); 561 } 562 563 @Override hashCode()564 public int hashCode() { 565 return Objects.hash(mDeviceAddress, mSubKey); 566 } 567 568 @Override toString()569 public String toString() { 570 return String.format("%s, deviceAddress: %s, subKey: %s", 571 getClass().getSimpleName(), mDeviceAddress, mSubKey); 572 } 573 } 574 575 /** 576 * {@link CompositeKey} subclass used to identify specific messages; it uses message-handle as 577 * the secondary key. 578 */ 579 private static class MessageKey extends CompositeKey { MessageKey(MapMessage message)580 MessageKey(MapMessage message) { 581 super(message.getDevice().getAddress(), message.getHandle()); 582 } 583 } 584 585 /** 586 * CompositeKey used to identify Notification info for a sender; it uses a combination of 587 * senderContactUri and senderContactName as the secondary key. 588 */ 589 static class SenderKey extends CompositeKey implements Parcelable { SenderKey(String deviceAddress, String key)590 private SenderKey(String deviceAddress, String key) { 591 super(deviceAddress, key); 592 } 593 SenderKey(MapMessage message)594 SenderKey(MapMessage message) { 595 // Use a combination of senderName and senderContactUri for key. Ideally we would use 596 // only senderContactUri (which is encoded phone no.). However since some phones don't 597 // provide these, we fall back to senderName. Since senderName may not be unique, we 598 // include senderContactUri also to provide uniqueness in cases it is available. 599 this(message.getDevice().getAddress(), 600 message.getSenderName() + "/" + message.getSenderContactUri()); 601 } 602 603 @Override describeContents()604 public int describeContents() { 605 return 0; 606 } 607 608 @Override writeToParcel(Parcel dest, int flags)609 public void writeToParcel(Parcel dest, int flags) { 610 dest.writeString(mDeviceAddress); 611 dest.writeString(mSubKey); 612 } 613 614 public static final Parcelable.Creator<SenderKey> CREATOR = 615 new Parcelable.Creator<SenderKey>() { 616 @Override 617 public SenderKey createFromParcel(Parcel source) { 618 return new SenderKey(source.readString(), source.readString()); 619 } 620 621 @Override 622 public SenderKey[] newArray(int size) { 623 return new SenderKey[size]; 624 } 625 }; 626 } 627 628 /** 629 * Information about a single notification that is displayed. 630 */ 631 private static class NotificationInfo { 632 private static int NEXT_NOTIFICATION_ID = 0; 633 634 final int mNotificationId = NEXT_NOTIFICATION_ID++; 635 final String mSenderName; 636 @Nullable 637 final String mSenderContactUri; 638 final LinkedList<MessageKey> mMessageKeys = new LinkedList<>(); 639 boolean muted = false; 640 NotificationInfo(String senderName, @Nullable String senderContactUri)641 NotificationInfo(String senderName, @Nullable String senderContactUri) { 642 mSenderName = senderName; 643 mSenderContactUri = senderContactUri; 644 } 645 } 646 } 647