blob: d26db4c69ecefb8c7b9110ac4bb7b4ed950a3237 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.phone.ongoingcall
import android.app.ActivityManager
import android.app.IActivityManager
import android.app.IUidObserver
import android.app.Notification
import android.app.PendingIntent
import android.app.Person
import android.content.Intent
import android.service.notification.NotificationListenerService.REASON_USER_STOPPED
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.FeatureFlags
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.nullable
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
private const val CALL_UID = 900
// A process state that represents the process being visible to the user.
private const val PROC_STATE_VISIBLE = ActivityManager.PROCESS_STATE_TOP
// A process state that represents the process being invisible to the user.
private const val PROC_STATE_INVISIBLE = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class OngoingCallControllerTest : SysuiTestCase() {
private val clock = FakeSystemClock()
private val mainExecutor = FakeExecutor(clock)
private val uiEventLoggerFake = UiEventLoggerFake()
private lateinit var controller: OngoingCallController
private lateinit var notifCollectionListener: NotifCollectionListener
@Mock private lateinit var mockOngoingCallListener: OngoingCallListener
@Mock private lateinit var mockActivityStarter: ActivityStarter
@Mock private lateinit var mockIActivityManager: IActivityManager
private lateinit var chipView: View
@Before
fun setUp() {
allowTestableLooperAsMainThread()
TestableLooper.get(this).runWithLooper {
chipView = LayoutInflater.from(mContext).inflate(R.layout.ongoing_call_chip, null)
}
MockitoAnnotations.initMocks(this)
val featureFlags = mock(FeatureFlags::class.java)
`when`(featureFlags.isOngoingCallStatusBarChipEnabled).thenReturn(true)
val notificationCollection = mock(CommonNotifCollection::class.java)
controller = OngoingCallController(
notificationCollection,
featureFlags,
clock,
mockActivityStarter,
mainExecutor,
mockIActivityManager,
OngoingCallLogger(uiEventLoggerFake))
controller.init()
controller.addCallback(mockOngoingCallListener)
controller.setChipView(chipView)
val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java)
verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture())
notifCollectionListener = collectionListenerCaptor.value!!
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
.thenReturn(PROC_STATE_INVISIBLE)
}
@After
fun tearDown() {
controller.tearDownChipView()
}
@Test
fun onEntryUpdated_isOngoingCallNotif_listenerNotified() {
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
}
@Test
fun onEntryUpdated_notOngoingCallNotif_listenerNotNotified() {
notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
}
@Test
fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_listenerNotifiedTwice() {
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
verify(mockOngoingCallListener, times(2))
.onOngoingCallStateChanged(anyBoolean())
}
/** Regression test for b/191472854. */
@Test
fun onEntryUpdated_notifHasNullContentIntent_noCrash() {
notifCollectionListener.onEntryUpdated(
createCallNotifEntry(ongoingCallStyle, nullContentIntent = true))
}
/** Regression test for b/192379214. */
@Test
fun onEntryUpdated_notificationWhenIsZero_timeHidden() {
val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
notification.modifyNotification(context).setWhen(0)
notifCollectionListener.onEntryUpdated(notification.build())
chipView.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
assertThat(chipView.findViewById<View>(R.id.ongoing_call_chip_time)?.measuredWidth)
.isEqualTo(0)
}
@Test
fun onEntryUpdated_notificationWhenIsValid_timeShown() {
val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
notification.modifyNotification(context).setWhen(clock.currentTimeMillis())
notifCollectionListener.onEntryUpdated(notification.build())
chipView.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
assertThat(chipView.findViewById<View>(R.id.ongoing_call_chip_time)?.measuredWidth)
.isGreaterThan(0)
}
/** Regression test for b/194731244. */
@Test
fun onEntryUpdated_calledManyTimes_uidObserverUnregisteredManyTimes() {
val numCalls = 4
for (i in 0 until numCalls) {
// Re-create the notification each time so that it's considered a different object and
// observers will get re-registered (and hopefully unregistered).
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
}
// There should be 1 observer still registered, so we should unregister n-1 times.
verify(mockIActivityManager, times(numCalls - 1)).unregisterUidObserver(any())
}
/**
* If a call notification is never added before #onEntryRemoved is called, then the listener
* should never be notified.
*/
@Test
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_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())
}
@Test
fun hasOngoingCall_noOngoingCallNotifSent_returnsFalse() {
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
fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() {
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
.thenReturn(PROC_STATE_INVISIBLE)
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
assertThat(controller.hasOngoingCall()).isTrue()
}
@Test
fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() {
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
.thenReturn(PROC_STATE_VISIBLE)
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
assertThat(controller.hasOngoingCall()).isFalse()
}
@Test
fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() {
val invalidChipView = LinearLayout(context)
controller.setChipView(invalidChipView)
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
assertThat(controller.hasOngoingCall()).isFalse()
}
@Test
fun hasOngoingCall_ongoingCallNotifSentThenRemoved_returnsFalse() {
val ongoingCallNotifEntry = createOngoingCallNotifEntry()
notifCollectionListener.onEntryUpdated(ongoingCallNotifEntry)
notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
assertThat(controller.hasOngoingCall()).isFalse()
}
@Test
fun hasOngoingCall_ongoingCallNotifSentThenScreeningCallNotifSent_returnsFalse() {
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
assertThat(controller.hasOngoingCall()).isFalse()
}
@Test
fun hasOngoingCall_ongoingCallNotifSentThenUnrelatedNotifSent_returnsTrue() {
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
assertThat(controller.hasOngoingCall()).isTrue()
}
/**
* This test fakes a theme change during an ongoing call.
*
* When a theme change happens, [CollapsedStatusBarFragment] and its views get re-created, so
* [OngoingCallController.setChipView] gets called with a new view. If there's an active ongoing
* call when the theme changes, the new view needs to be updated with the call information.
*/
@Test
fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() {
// Start an ongoing call.
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
reset(mockOngoingCallListener)
lateinit var newChipView: View
TestableLooper.get(this).runWithLooper {
newChipView = LayoutInflater.from(mContext).inflate(R.layout.ongoing_call_chip, null)
}
// Change the chip view associated with the controller.
controller.setChipView(newChipView)
verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
}
@Test
fun callProcessChangesToVisible_listenerNotified() {
// Start the call while the process is invisible.
`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(
captor.capture(), any(), any(), nullable(String::class.java))
val uidObserver = captor.value
// Update the process to visible.
uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0)
mainExecutor.advanceClockToLast()
mainExecutor.runAllReady();
verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
}
@Test
fun callProcessChangesToInvisible_listenerNotified() {
// Start the call while the process is visible.
`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(
captor.capture(), any(), any(), nullable(String::class.java))
val uidObserver = captor.value
// Update the process to invisible.
uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0)
mainExecutor.advanceClockToLast()
mainExecutor.runAllReady();
verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
}
@Test
fun chipClicked_clickEventLogged() {
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
chipView.performClick()
assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
assertThat(uiEventLoggerFake.eventId(0))
.isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_CLICKED.id)
}
@Test
fun notifyChipVisibilityChanged_visibleEventLogged() {
controller.notifyChipVisibilityChanged(true)
assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
assertThat(uiEventLoggerFake.eventId(0))
.isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_VISIBLE.id)
}
// Other tests for notifyChipVisibilityChanged are in [OngoingCallLogger], since
// [OngoingCallController.notifyChipVisibilityChanged] just delegates to that class.
private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle)
private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle)
private fun createCallNotifEntry(
callStyle: Notification.CallStyle,
nullContentIntent: Boolean = false
): NotificationEntry {
val notificationEntryBuilder = NotificationEntryBuilder()
notificationEntryBuilder.modifyNotification(context).style = callStyle
notificationEntryBuilder.setUid(CALL_UID)
if (nullContentIntent) {
notificationEntryBuilder.modifyNotification(context).setContentIntent(null)
} else {
val contentIntent = mock(PendingIntent::class.java)
`when`(contentIntent.intent).thenReturn(mock(Intent::class.java))
notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent)
}
return notificationEntryBuilder.build()
}
private fun createNotCallNotifEntry() = NotificationEntryBuilder().build()
}
private val person = Person.Builder().setName("name").build()
private val hangUpIntent = mock(PendingIntent::class.java)
private val ongoingCallStyle = Notification.CallStyle.forOngoingCall(person, hangUpIntent)
private val screeningCallStyle = Notification.CallStyle.forScreeningCall(
person, hangUpIntent, /* answerIntent= */ mock(PendingIntent::class.java))