Merge QQ3A.200605.002 into master

Bug: 158095402
Merged-In: Ia015d92a7c4855b41c068e9c1e8d623dd5665a40
Change-Id: I92439e5e4d5481775b719554e70f65b9e293dfba
diff --git a/Android.bp b/Android.bp
index 12b0f47..671cc6c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -31,8 +31,11 @@
 
     privileged: true,
 
+    libs: ["android.car"],
+
     static_libs: [
         "car-apps-common",
+        "car-messenger-common",
         "car-telephony-common",
         "androidx.annotation_annotation",
         "glide-prebuilt",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 71ffe31..3460deb 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -28,6 +28,7 @@
     <uses-permission android:name="android.permission.SEND_SMS"/>
     <uses-permission android:name="android.permission.READ_SMS"/>
     <uses-permission android:name="android.permission.WRITE_SMS"/>
+    <uses-permission android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS"/>
 
     <application android:label="@string/app_name">
         <service android:name=".MessengerService"
diff --git a/src/com/android/car/messenger/MapMessage.java b/src/com/android/car/messenger/MapMessage.java
index b4b7aee..95c932d 100644
--- a/src/com/android/car/messenger/MapMessage.java
+++ b/src/com/android/car/messenger/MapMessage.java
@@ -19,6 +19,7 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothMapClient;
 import android.content.Intent;
+import com.android.car.messenger.log.L;
 
 import androidx.annotation.Nullable;
 
@@ -26,6 +27,7 @@
  * Represents a message obtained via MAP service from a connected Bluetooth device.
  */
 class MapMessage {
+    private static final String TAG = "CM.MapMessage";
     private String mDeviceAddress;
     private String mHandle;
     private String mSenderName;
@@ -34,18 +36,25 @@
     private String mMessageText;
     private long mReceiveTime;
     private boolean mIsReadOnPhone;
-    private boolean mIsReadOnCar;
+    private boolean mShouldInclude;
 
     /**
      * Constructs a {@link MapMessage} from {@code intent} that was received from MAP service via
      * {@link BluetoothMapClient#ACTION_MESSAGE_RECEIVED} broadcast.
      *
      * @param intent intent received from MAP service
-     * @return message constructed from extras in {@code intent}
+     * @return message constructed from extras in {@code intent}, or null if this is a group
+     *         conversation.
      * @throws NullPointerException if {@code intent} is missing the device extra
      * @throws IllegalArgumentException if {@code intent} is missing any other required extras
      */
+    @Nullable
     public static MapMessage parseFrom(Intent intent) {
+        if (intent.getStringArrayExtra(Intent.EXTRA_CC) != null
+            && intent.getStringArrayExtra(Intent.EXTRA_CC).length > 0) {
+            L.i(TAG, "Skipping group conversation message");
+            return null;
+        }
         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
         String handle = intent.getStringExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE);
         String senderUri = intent.getStringExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_URI);
@@ -101,6 +110,7 @@
         mSenderName = senderName;
         mReceiveTime = receiveTime;
         mIsReadOnPhone = isRead;
+        mShouldInclude = true;
     }
 
     /**
@@ -151,8 +161,13 @@
         return mMessageText;
     }
 
-    public void markMessageAsRead() {
-        mIsReadOnCar = true;
+    /**
+     * Sets the message to be excluded from the notification. Messages that have been read aloud on
+     * the car, or that have been dismissed by the user should be excluded from the notification if/
+     * when the notification gets updated. Note: this state will not be propagated to the phone.
+     */
+    public void excludeFromNotification() {
+        mShouldInclude = false;
     }
 
     /**
@@ -163,10 +178,12 @@
     }
 
     /**
-     * Returns {@code true} if message was read on the car.
+     * Returns {@code true} if message should be included in the notification. Messages that
+     * have been read aloud on the car, or that have been dismissed by the user should be excluded
+     * from the notification if/when the notification gets updated.
      */
-    public boolean isReadOnCar() {
-        return mIsReadOnCar;
+    public boolean shouldIncludeInNotification() {
+        return mShouldInclude;
     }
 
     @Override
@@ -179,7 +196,7 @@
                 ", mSenderName='" + mSenderName + '\'' +
                 ", mReceiveTime=" + mReceiveTime + '\'' +
                 ", mIsReadOnPhone= " + mIsReadOnPhone + '\'' +
-                ", mIsReadOnCar= " + mIsReadOnCar +
+                ", mShouldInclude= " + mShouldInclude +
                 "}";
     }
 }
diff --git a/src/com/android/car/messenger/MessengerDelegate.java b/src/com/android/car/messenger/MessengerDelegate.java
index 58193d4..7f864b6 100644
--- a/src/com/android/car/messenger/MessengerDelegate.java
+++ b/src/com/android/car/messenger/MessengerDelegate.java
@@ -14,7 +14,6 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
-import android.util.Log;
 import android.widget.Toast;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -28,6 +27,7 @@
 import com.android.car.apps.common.LetterTileDrawable;
 import com.android.car.messenger.bluetooth.BluetoothHelper;
 import com.android.car.messenger.bluetooth.BluetoothMonitor;
+import com.android.car.messenger.common.ProjectionStateListener;
 import com.android.car.messenger.log.L;
 import com.android.car.telephony.common.TelecomUtils;
 import com.android.internal.annotations.GuardedBy;
@@ -68,9 +68,15 @@
     final Map<String, Long> mBTDeviceAddressToConnectionTimestamp = new HashMap<>();
     final Map<SenderKey, Bitmap> mSenderToLargeIconBitmap = new HashMap<>();
 
+    /** Tracks whether a projection application is active in the foreground. **/
+    private ProjectionStateListener mProjectionStateListener;
+
     public MessengerDelegate(Context context) {
         mContext = context;
 
+        mProjectionStateListener = new ProjectionStateListener(context);
+        mProjectionStateListener.start();
+
         mNotificationManager =
                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
         mSmsDatabaseHandler = new SmsDatabaseHandler(mContext);
@@ -94,6 +100,7 @@
     public void onMessageReceived(Intent intent) {
         try {
             MapMessage message = MapMessage.parseFrom(intent);
+            if (message == null) return;
             L.d(TAG, "Received message from " + message.getDeviceAddress());
 
             MessageKey messageKey = new MessageKey(message);
@@ -210,12 +217,17 @@
         }
     }
 
-    protected void markAsRead(SenderKey senderKey) {
+
+    /**
+     * Excludes messages from a notification so that the messages are not shown to the user once
+     * the notification gets updated with newer messages.
+     */
+    protected void excludeFromNotification(SenderKey senderKey) {
         NotificationInfo info = mNotificationInfos.get(senderKey);
         for (MessageKey key : info.mMessageKeys) {
             MapMessage message = mMessages.get(key);
-            if (!message.isReadOnCar()) {
-                message.markMessageAsRead();
+            if (message.shouldIncludeInNotification()) {
+                message.excludeFromNotification();
                 mSmsDatabaseHandler.addOrUpdate(message);
             }
         }
@@ -227,6 +239,7 @@
         if (mPhoneNumberInfoFuture != null) {
             mPhoneNumberInfoFuture.cancel(true);
         }
+        mProjectionStateListener.stop();
     }
 
     /**
@@ -239,6 +252,7 @@
             if (predicate.test(senderKey)) {
                 mNotificationManager.cancel(notificationInfo.mNotificationId);
             }
+            excludeFromNotification(senderKey);
         });
     }
 
@@ -249,11 +263,11 @@
                 mSmsDatabaseHandler.removeMessagesForDevice(key.getDeviceAddress());
             }
         }
-        mMessages.entrySet().removeIf(
-                messageKeyMapMessageEntry -> predicate.test(messageKeyMapMessageEntry.getKey()));
         clearNotifications(predicate);
         mNotificationInfos.entrySet().removeIf(entry -> predicate.test(entry.getKey()));
         mSenderToLargeIconBitmap.entrySet().removeIf(entry -> predicate.test(entry.getKey()));
+        mMessages.entrySet().removeIf(
+                messageKeyMapMessageEntry -> predicate.test(messageKeyMapMessageEntry.getKey()));
     }
 
     private void updateNotification(MessageKey messageKey, MapMessage mapMessage) {
@@ -361,17 +375,26 @@
                 .setUri(notificationInfo.mSenderContactUri)
                 .build();
         notificationInfo.mMessageKeys.stream().map(mMessages::get).forEachOrdered(message -> {
-            if (!message.isReadOnCar()) {
+            if (message.shouldIncludeInNotification()) {
                 messagingStyle.addMessage(
                         message.getMessageText(),
                         message.getReceiveTime(),
                         sender);
+            } else {
+                L.d(TAG, "excluding message received at: " + message.getReceiveTime()
+                        + " from notification.");
             }
         });
 
-        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext,
-                MessengerService.SMS_CHANNEL_ID)
-                .setContentTitle(senderName)
+        NotificationCompat.Builder builder;
+        if (mProjectionStateListener.isProjectionInActiveForeground(senderKey.getDeviceAddress())) {
+            builder = new NotificationCompat.Builder(mContext,
+                    MessengerService.SILENT_SMS_CHANNEL_ID);
+        } else {
+            builder = new NotificationCompat.Builder(mContext, MessengerService.SMS_CHANNEL_ID);
+        }
+
+        builder.setContentTitle(senderName)
                 .setContentText(contentText)
                 .setStyle(messagingStyle)
                 .setCategory(Notification.CATEGORY_MESSAGE)
diff --git a/src/com/android/car/messenger/MessengerService.java b/src/com/android/car/messenger/MessengerService.java
index 0b0e4e4..2f71de5 100644
--- a/src/com/android/car/messenger/MessengerService.java
+++ b/src/com/android/car/messenger/MessengerService.java
@@ -59,6 +59,7 @@
 
     /* NOTIFICATIONS */
     static final String SMS_CHANNEL_ID = "SMS_CHANNEL_ID";
+    static final String SILENT_SMS_CHANNEL_ID = "SILENT_SMS_CHANNEL_ID";
     private static final String APP_RUNNING_CHANNEL_ID = "APP_RUNNING_CHANNEL_ID";
     private static final int SERVICE_STARTED_NOTIFICATION_ID = Integer.MAX_VALUE;
 
@@ -111,6 +112,16 @@
             notificationManager.createNotificationChannel(appRunningNotificationChannel);
         }
 
+        // Create notification channel for notifications that should be posted silently in the
+        // notification center, without a heads up notification.
+        {
+            NotificationChannel silentNotificationChannel =
+                    new NotificationChannel(SILENT_SMS_CHANNEL_ID,
+                            getString(R.string.sms_channel_description),
+                            NotificationManager.IMPORTANCE_LOW);
+            notificationManager.createNotificationChannel(silentNotificationChannel);
+        }
+
         {
             AudioAttributes attributes = new AudioAttributes.Builder()
                     .setUsage(AudioAttributes.USAGE_NOTIFICATION)
@@ -243,7 +254,7 @@
     public void markAsRead(Intent intent) {
         final SenderKey senderKey = intent.getParcelableExtra(EXTRA_SENDER_KEY);
         L.d(TAG, "markAsRead");
-        mMessengerDelegate.markAsRead(senderKey);
+        mMessengerDelegate.excludeFromNotification(senderKey);
     }
 
     /**
diff --git a/src/com/android/car/messenger/SmsDatabaseHandler.java b/src/com/android/car/messenger/SmsDatabaseHandler.java
index 77d8e29..a8fd107 100644
--- a/src/com/android/car/messenger/SmsDatabaseHandler.java
+++ b/src/com/android/car/messenger/SmsDatabaseHandler.java
@@ -168,7 +168,8 @@
         newMessage.put(Telephony.Sms.PERSON,
                 getContactId(mContentResolver,
                         message.getSenderContactUri()));
-        newMessage.put(Telephony.Sms.READ, (message.isReadOnPhone() || message.isReadOnCar()));
+        newMessage.put(Telephony.Sms.READ, (message.isReadOnPhone()
+                || !message.shouldIncludeInNotification()));
         return newMessage;
     }
 
diff --git a/tests/robotests/src/com/android/car/messenger/MessengerDelegateTest.java b/tests/robotests/src/com/android/car/messenger/MessengerDelegateTest.java
index 2f9b182..307624b 100644
--- a/tests/robotests/src/com/android/car/messenger/MessengerDelegateTest.java
+++ b/tests/robotests/src/com/android/car/messenger/MessengerDelegateTest.java
@@ -189,12 +189,12 @@
     public void testHandleMarkAsRead() {
         mMessengerDelegate.onMessageReceived(mMessageOneIntent);
 
-        mMessengerDelegate.markAsRead(mSenderKey);
+        mMessengerDelegate.excludeFromNotification(mSenderKey);
 
         MessengerDelegate.NotificationInfo info = mMessengerDelegate.mNotificationInfos.get(
                 mSenderKey);
         MessengerDelegate.MessageKey key = info.mMessageKeys.get(0);
-        assertThat(mMessengerDelegate.mMessages.get(key).isReadOnCar()).isTrue();
+        assertThat(mMessengerDelegate.mMessages.get(key).shouldIncludeInNotification()).isFalse();
     }
 
     @Test
@@ -208,7 +208,7 @@
         MessengerDelegate.NotificationInfo info = mMessengerDelegate.mNotificationInfos.get(
                 mSenderKey);
         MessengerDelegate.MessageKey key = info.mMessageKeys.get(0);
-        assertThat(mMessengerDelegate.mMessages.get(key).isReadOnCar()).isFalse();
+        assertThat(mMessengerDelegate.mMessages.get(key).shouldIncludeInNotification()).isTrue();
         assertThat(mMessengerDelegate.mMessages.get(key).isReadOnPhone()).isTrue();
     }