Merge "[Ongoing Call] Use the notification's key to identify when to remove the ongoing call chip, rather than the notification's template." into sc-dev
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 7dccf01..ef7fac3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -53,8 +53,8 @@
     private val logger: OngoingCallLogger
 ) : CallbackController<OngoingCallListener> {
 
-    /** Null if there's no ongoing call. */
-    private var ongoingCallInfo: OngoingCallInfo? = null
+    /** Non-null if there's an active call notification. */
+    private var callNotificationInfo: CallNotificationInfo? = null
     /** True if the application managing the call is visible to the user. */
     private var isCallAppVisible: Boolean = true
     private var chipView: View? = null
@@ -76,19 +76,34 @@
         }
 
         override fun onEntryUpdated(entry: NotificationEntry) {
-            if (isOngoingCallNotification(entry)) {
-                ongoingCallInfo = OngoingCallInfo(
-                    entry.sbn.notification.`when`,
-                    entry.sbn.notification.contentIntent.intent,
-                    entry.sbn.uid)
-                updateChip()
-            } else if (isCallNotification(entry)) {
-                removeChip()
+            // We have a new call notification or our existing call notification has been updated.
+            // TODO(b/183229367): This likely won't work if you take a call from one app then
+            //  switch to a call from another app.
+            if (callNotificationInfo == null && isCallNotification(entry) ||
+                    (entry.sbn.key == callNotificationInfo?.key)) {
+                val newOngoingCallInfo = CallNotificationInfo(
+                        entry.sbn.key,
+                        entry.sbn.notification.`when`,
+                        entry.sbn.notification.contentIntent.intent,
+                        entry.sbn.uid,
+                        entry.sbn.notification.extras.getInt(
+                                Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING
+                )
+                if (newOngoingCallInfo == callNotificationInfo) {
+                    return
+                }
+
+                callNotificationInfo = newOngoingCallInfo
+                if (newOngoingCallInfo.isOngoing) {
+                    updateChip()
+                } else {
+                    removeChip()
+                }
             }
         }
 
         override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
-            if (isOngoingCallNotification(entry)) {
+            if (entry.sbn.key == callNotificationInfo?.key) {
                 removeChip()
             }
         }
@@ -126,7 +141,7 @@
      * Returns true if there's an active ongoing call that should be displayed in a status bar chip.
      */
     fun hasOngoingCall(): Boolean {
-        return ongoingCallInfo != null &&
+        return callNotificationInfo?.isOngoing == true &&
                 // When the user is in the phone app, don't show the chip.
                 !isCallAppVisible
     }
@@ -146,7 +161,7 @@
     }
 
     private fun updateChip() {
-        val currentOngoingCallInfo = ongoingCallInfo ?: return
+        val currentCallNotificationInfo = callNotificationInfo ?: return
 
         val currentChipView = chipView
         val timeView =
@@ -155,7 +170,7 @@
             currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background)
 
         if (currentChipView != null && timeView != null && backgroundView != null) {
-            timeView.base = currentOngoingCallInfo.callStartTime -
+            timeView.base = currentCallNotificationInfo.callStartTime -
                     System.currentTimeMillis() +
                     systemClock.elapsedRealtime()
             timeView.start()
@@ -163,17 +178,17 @@
             currentChipView.setOnClickListener {
                 logger.logChipClicked()
                 activityStarter.postStartActivityDismissingKeyguard(
-                        currentOngoingCallInfo.intent, 0,
+                        currentCallNotificationInfo.intent, 0,
                         ActivityLaunchAnimator.Controller.fromView(backgroundView))
             }
 
-            setUpUidObserver(currentOngoingCallInfo)
+            setUpUidObserver(currentCallNotificationInfo)
 
             mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
         } else {
-            // If we failed to update the chip, don't store the ongoing call info. Then
-            // [hasOngoingCall] will return false and we fall back to typical notification handling.
-            ongoingCallInfo = null
+            // If we failed to update the chip, don't store the call info. Then [hasOngoingCall]
+            // will return false and we fall back to typical notification handling.
+            callNotificationInfo = null
 
             if (DEBUG) {
                 Log.w(TAG, "Ongoing call chip view could not be found; " +
@@ -185,14 +200,14 @@
     /**
      * Sets up an [IUidObserver] to monitor the status of the application managing the ongoing call.
      */
-    private fun setUpUidObserver(currentOngoingCallInfo: OngoingCallInfo) {
+    private fun setUpUidObserver(currentCallNotificationInfo: CallNotificationInfo) {
         isCallAppVisible = isProcessVisibleToUser(
-                iActivityManager.getUidProcessState(currentOngoingCallInfo.uid, null))
+                iActivityManager.getUidProcessState(currentCallNotificationInfo.uid, null))
 
         uidObserver = object : IUidObserver.Stub() {
             override fun onUidStateChanged(
                     uid: Int, procState: Int, procStateSeq: Long, capability: Int) {
-                if (uid == currentOngoingCallInfo.uid) {
+                if (uid == currentCallNotificationInfo.uid) {
                     val oldIsCallAppVisible = isCallAppVisible
                     isCallAppVisible = isProcessVisibleToUser(procState)
                     if (oldIsCallAppVisible != isCallAppVisible) {
@@ -225,26 +240,23 @@
     }
 
     private fun removeChip() {
-        ongoingCallInfo = null
+        callNotificationInfo = null
         mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
         if (uidObserver != null) {
             iActivityManager.unregisterUidObserver(uidObserver)
         }
     }
 
-    private class OngoingCallInfo(
+    private data class CallNotificationInfo(
+        val key: String,
         val callStartTime: Long,
         val intent: Intent,
-        val uid: Int
+        val uid: Int,
+        /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
+        val isOngoing: Boolean
     )
 }
 
-private fun isOngoingCallNotification(entry: NotificationEntry): Boolean {
-    val extras = entry.sbn.notification.extras
-    return isCallNotification(entry) &&
-            extras.getInt(Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING
-}
-
 private fun isCallNotification(entry: NotificationEntry): Boolean {
     return entry.sbn.notification.isStyle(Notification.CallStyle::class.java)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index 3a71ecf..ca3cff9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -55,6 +55,8 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.reset
+
 import org.mockito.MockitoAnnotations
 
 private const val CALL_UID = 900
@@ -138,15 +140,51 @@
                 .onOngoingCallStateChanged(anyBoolean())
     }
 
+    /**
+     * If a call notification is never added before #onEntryRemoved is called, then the listener
+     * should never be notified.
+     */
     @Test
-    fun onEntryRemoved_ongoingCallNotif_listenerNotified() {
+    fun onEntryRemoved_callNotifNeverAddedBeforehand_listenerNotNotified() {
         notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED)
 
+        verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
+    }
+
+    @Test
+    fun onEntryRemoved_callNotifAddedThenRemoved_listenerNotified() {
+        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
+        notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
+        reset(mockOngoingCallListener)
+
+        notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
+
         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
     }
 
+    /** Regression test for b/188491504. */
     @Test
-    fun onEntryRemoved_notOngoingCallNotif_listenerNotNotified() {
+    fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_listenerNotified() {
+        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
+        notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
+        reset(mockOngoingCallListener)
+
+        // Create another notification based on the ongoing call one, but remove the features that
+        // made it a call notification.
+        val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry)
+        removedEntryBuilder.modifyNotification(context).style = null
+
+        notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED)
+
+        verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
+    }
+
+
+    @Test
+    fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_listenerNotNotified() {
+        notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry())
+        reset(mockOngoingCallListener)
+
         notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED)
 
         verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
@@ -158,6 +196,20 @@
     }
 
     @Test
+    fun hasOngoingCall_unrelatedNotifSent_returnsFalse() {
+        notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
+
+        assertThat(controller.hasOngoingCall()).isFalse()
+    }
+
+    @Test
+    fun hasOngoingCall_screeningCallNotifSent_returnsFalse() {
+        notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
+
+        assertThat(controller.hasOngoingCall()).isFalse()
+    }
+
+    @Test
     fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() {
         `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
                 .thenReturn(PROC_STATE_INVISIBLE)
@@ -224,6 +276,7 @@
     fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() {
         // Start an ongoing call.
         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+        reset(mockOngoingCallListener)
 
         lateinit var newChipView: View
         TestableLooper.get(this).runWithLooper {
@@ -233,10 +286,7 @@
         // Change the chip view associated with the controller.
         controller.setChipView(newChipView)
 
-        // Verify the listener was notified once for the initial call and again when the new view
-        // was set.
-        verify(mockOngoingCallListener, times(2))
-                .onOngoingCallStateChanged(anyBoolean())
+        verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
     }
 
     @Test
@@ -245,6 +295,7 @@
         `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
                 .thenReturn(PROC_STATE_INVISIBLE)
         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+        reset(mockOngoingCallListener)
 
         val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
         verify(mockIActivityManager).registerUidObserver(
@@ -256,9 +307,7 @@
         mainExecutor.advanceClockToLast()
         mainExecutor.runAllReady();
 
-        // Once for when the call was started, and another time when the process visibility changes.
-        verify(mockOngoingCallListener, times(2))
-                .onOngoingCallStateChanged(anyBoolean())
+        verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
     }
 
     @Test
@@ -267,6 +316,7 @@
         `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
                 .thenReturn(PROC_STATE_VISIBLE)
         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+        reset(mockOngoingCallListener)
 
         val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
         verify(mockIActivityManager).registerUidObserver(
@@ -278,9 +328,7 @@
         mainExecutor.advanceClockToLast()
         mainExecutor.runAllReady();
 
-        // Once for when the call was started, and another time when the process visibility changes.
-        verify(mockOngoingCallListener, times(2))
-                .onOngoingCallStateChanged(anyBoolean())
+        verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
     }
 
     @Test