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

This commit is contained in:
Caitlin Cassidy
2021-06-02 21:11:06 +00:00
committed by Android (Google) Code Review
2 changed files with 103 additions and 43 deletions

View File

@@ -53,8 +53,8 @@ class OngoingCallController @Inject constructor(
private val logger: OngoingCallLogger private val logger: OngoingCallLogger
) : CallbackController<OngoingCallListener> { ) : CallbackController<OngoingCallListener> {
/** Null if there's no ongoing call. */ /** Non-null if there's an active call notification. */
private var ongoingCallInfo: OngoingCallInfo? = null private var callNotificationInfo: CallNotificationInfo? = null
/** True if the application managing the call is visible to the user. */ /** True if the application managing the call is visible to the user. */
private var isCallAppVisible: Boolean = true private var isCallAppVisible: Boolean = true
private var chipView: View? = null private var chipView: View? = null
@@ -76,19 +76,34 @@ class OngoingCallController @Inject constructor(
} }
override fun onEntryUpdated(entry: NotificationEntry) { override fun onEntryUpdated(entry: NotificationEntry) {
if (isOngoingCallNotification(entry)) { // We have a new call notification or our existing call notification has been updated.
ongoingCallInfo = OngoingCallInfo( // TODO(b/183229367): This likely won't work if you take a call from one app then
entry.sbn.notification.`when`, // switch to a call from another app.
entry.sbn.notification.contentIntent.intent, if (callNotificationInfo == null && isCallNotification(entry) ||
entry.sbn.uid) (entry.sbn.key == callNotificationInfo?.key)) {
updateChip() val newOngoingCallInfo = CallNotificationInfo(
} else if (isCallNotification(entry)) { entry.sbn.key,
removeChip() 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) { override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
if (isOngoingCallNotification(entry)) { if (entry.sbn.key == callNotificationInfo?.key) {
removeChip() removeChip()
} }
} }
@@ -126,7 +141,7 @@ class OngoingCallController @Inject constructor(
* Returns true if there's an active ongoing call that should be displayed in a status bar chip. * Returns true if there's an active ongoing call that should be displayed in a status bar chip.
*/ */
fun hasOngoingCall(): Boolean { fun hasOngoingCall(): Boolean {
return ongoingCallInfo != null && return callNotificationInfo?.isOngoing == true &&
// When the user is in the phone app, don't show the chip. // When the user is in the phone app, don't show the chip.
!isCallAppVisible !isCallAppVisible
} }
@@ -146,7 +161,7 @@ class OngoingCallController @Inject constructor(
} }
private fun updateChip() { private fun updateChip() {
val currentOngoingCallInfo = ongoingCallInfo ?: return val currentCallNotificationInfo = callNotificationInfo ?: return
val currentChipView = chipView val currentChipView = chipView
val timeView = val timeView =
@@ -155,7 +170,7 @@ class OngoingCallController @Inject constructor(
currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background) currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background)
if (currentChipView != null && timeView != null && backgroundView != null) { if (currentChipView != null && timeView != null && backgroundView != null) {
timeView.base = currentOngoingCallInfo.callStartTime - timeView.base = currentCallNotificationInfo.callStartTime -
System.currentTimeMillis() + System.currentTimeMillis() +
systemClock.elapsedRealtime() systemClock.elapsedRealtime()
timeView.start() timeView.start()
@@ -163,17 +178,17 @@ class OngoingCallController @Inject constructor(
currentChipView.setOnClickListener { currentChipView.setOnClickListener {
logger.logChipClicked() logger.logChipClicked()
activityStarter.postStartActivityDismissingKeyguard( activityStarter.postStartActivityDismissingKeyguard(
currentOngoingCallInfo.intent, 0, currentCallNotificationInfo.intent, 0,
ActivityLaunchAnimator.Controller.fromView(backgroundView)) ActivityLaunchAnimator.Controller.fromView(backgroundView))
} }
setUpUidObserver(currentOngoingCallInfo) setUpUidObserver(currentCallNotificationInfo)
mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
} else { } else {
// If we failed to update the chip, don't store the ongoing call info. Then // If we failed to update the chip, don't store the call info. Then [hasOngoingCall]
// [hasOngoingCall] will return false and we fall back to typical notification handling. // will return false and we fall back to typical notification handling.
ongoingCallInfo = null callNotificationInfo = null
if (DEBUG) { if (DEBUG) {
Log.w(TAG, "Ongoing call chip view could not be found; " + Log.w(TAG, "Ongoing call chip view could not be found; " +
@@ -185,14 +200,14 @@ class OngoingCallController @Inject constructor(
/** /**
* Sets up an [IUidObserver] to monitor the status of the application managing the ongoing call. * 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( isCallAppVisible = isProcessVisibleToUser(
iActivityManager.getUidProcessState(currentOngoingCallInfo.uid, null)) iActivityManager.getUidProcessState(currentCallNotificationInfo.uid, null))
uidObserver = object : IUidObserver.Stub() { uidObserver = object : IUidObserver.Stub() {
override fun onUidStateChanged( override fun onUidStateChanged(
uid: Int, procState: Int, procStateSeq: Long, capability: Int) { uid: Int, procState: Int, procStateSeq: Long, capability: Int) {
if (uid == currentOngoingCallInfo.uid) { if (uid == currentCallNotificationInfo.uid) {
val oldIsCallAppVisible = isCallAppVisible val oldIsCallAppVisible = isCallAppVisible
isCallAppVisible = isProcessVisibleToUser(procState) isCallAppVisible = isProcessVisibleToUser(procState)
if (oldIsCallAppVisible != isCallAppVisible) { if (oldIsCallAppVisible != isCallAppVisible) {
@@ -225,26 +240,23 @@ class OngoingCallController @Inject constructor(
} }
private fun removeChip() { private fun removeChip() {
ongoingCallInfo = null callNotificationInfo = null
mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
if (uidObserver != null) { if (uidObserver != null) {
iActivityManager.unregisterUidObserver(uidObserver) iActivityManager.unregisterUidObserver(uidObserver)
} }
} }
private class OngoingCallInfo( private data class CallNotificationInfo(
val key: String,
val callStartTime: Long, val callStartTime: Long,
val intent: Intent, 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 { private fun isCallNotification(entry: NotificationEntry): Boolean {
return entry.sbn.notification.isStyle(Notification.CallStyle::class.java) return entry.sbn.notification.isStyle(Notification.CallStyle::class.java)
} }

View File

@@ -55,6 +55,8 @@ import org.mockito.Mockito.mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.times import org.mockito.Mockito.times
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.reset
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
private const val CALL_UID = 900 private const val CALL_UID = 900
@@ -138,15 +140,51 @@ class OngoingCallControllerTest : SysuiTestCase() {
.onOngoingCallStateChanged(anyBoolean()) .onOngoingCallStateChanged(anyBoolean())
} }
/**
* If a call notification is never added before #onEntryRemoved is called, then the listener
* should never be notified.
*/
@Test @Test
fun onEntryRemoved_ongoingCallNotif_listenerNotified() { fun onEntryRemoved_callNotifNeverAddedBeforehand_listenerNotNotified() {
notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) 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()) verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
} }
/** Regression test for b/188491504. */
@Test @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) notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED)
verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
@@ -157,6 +195,20 @@ class OngoingCallControllerTest : SysuiTestCase() {
assertThat(controller.hasOngoingCall()).isFalse() assertThat(controller.hasOngoingCall()).isFalse()
} }
@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 @Test
fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() { fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() {
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
@@ -224,6 +276,7 @@ class OngoingCallControllerTest : SysuiTestCase() {
fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() { fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() {
// Start an ongoing call. // Start an ongoing call.
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
reset(mockOngoingCallListener)
lateinit var newChipView: View lateinit var newChipView: View
TestableLooper.get(this).runWithLooper { TestableLooper.get(this).runWithLooper {
@@ -233,10 +286,7 @@ class OngoingCallControllerTest : SysuiTestCase() {
// Change the chip view associated with the controller. // Change the chip view associated with the controller.
controller.setChipView(newChipView) controller.setChipView(newChipView)
// Verify the listener was notified once for the initial call and again when the new view verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
// was set.
verify(mockOngoingCallListener, times(2))
.onOngoingCallStateChanged(anyBoolean())
} }
@Test @Test
@@ -245,6 +295,7 @@ class OngoingCallControllerTest : SysuiTestCase() {
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
.thenReturn(PROC_STATE_INVISIBLE) .thenReturn(PROC_STATE_INVISIBLE)
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
reset(mockOngoingCallListener)
val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
verify(mockIActivityManager).registerUidObserver( verify(mockIActivityManager).registerUidObserver(
@@ -256,9 +307,7 @@ class OngoingCallControllerTest : SysuiTestCase() {
mainExecutor.advanceClockToLast() mainExecutor.advanceClockToLast()
mainExecutor.runAllReady(); mainExecutor.runAllReady();
// Once for when the call was started, and another time when the process visibility changes. verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
verify(mockOngoingCallListener, times(2))
.onOngoingCallStateChanged(anyBoolean())
} }
@Test @Test
@@ -267,6 +316,7 @@ class OngoingCallControllerTest : SysuiTestCase() {
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
.thenReturn(PROC_STATE_VISIBLE) .thenReturn(PROC_STATE_VISIBLE)
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
reset(mockOngoingCallListener)
val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
verify(mockIActivityManager).registerUidObserver( verify(mockIActivityManager).registerUidObserver(
@@ -278,9 +328,7 @@ class OngoingCallControllerTest : SysuiTestCase() {
mainExecutor.advanceClockToLast() mainExecutor.advanceClockToLast()
mainExecutor.runAllReady(); mainExecutor.runAllReady();
// Once for when the call was started, and another time when the process visibility changes. verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
verify(mockOngoingCallListener, times(2))
.onOngoingCallStateChanged(anyBoolean())
} }
@Test @Test