/* * 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.dreams; import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.AlarmManager; import android.content.Context; import android.content.res.Resources; import android.hardware.SensorPrivacyManager; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.view.View; import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.FakeLogBuffer; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController; import com.android.systemui.statusbar.policy.NextAlarmController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.statusbar.window.StatusBarWindowStateListener; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.time.DateFormatUtil; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.List; import java.util.Optional; import java.util.concurrent.Executor; @SmallTest @RunWith(AndroidTestingRunner.class) public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { private static final String NOTIFICATION_INDICATOR_FORMATTER_STRING = "{count, plural, =1 {# notification} other {# notifications}}"; @Mock MockDreamOverlayStatusBarView mView; @Mock ConnectivityManager mConnectivityManager; @Mock NetworkCapabilities mNetworkCapabilities; @Mock Network mNetwork; @Mock TouchInsetManager.TouchInsetSession mTouchSession; @Mock Resources mResources; @Mock AlarmManager mAlarmManager; @Mock AlarmManager.AlarmClockInfo mAlarmClockInfo; @Mock NextAlarmController mNextAlarmController; @Mock DateFormatUtil mDateFormatUtil; @Mock IndividualSensorPrivacyController mSensorPrivacyController; @Mock ZenModeController mZenModeController; @Mock DreamOverlayNotificationCountProvider mDreamOverlayNotificationCountProvider; @Mock StatusBarWindowStateController mStatusBarWindowStateController; @Mock DreamOverlayStatusBarItemsProvider mDreamOverlayStatusBarItemsProvider; @Mock DreamOverlayStatusBarItemsProvider.StatusBarItem mStatusBarItem; @Mock View mStatusBarItemView; @Mock DreamOverlayStateController mDreamOverlayStateController; @Mock UserTracker mUserTracker; LogBuffer mLogBuffer = FakeLogBuffer.Factory.Companion.create(); @Captor private ArgumentCaptor mCallbackCaptor; private final Executor mMainExecutor = Runnable::run; DreamOverlayStatusBarViewController mController; @Before public void setup() { MockitoAnnotations.initMocks(this); when(mResources.getString(R.string.dream_overlay_status_bar_notification_indicator)) .thenReturn(NOTIFICATION_INDICATOR_FORMATTER_STRING); doCallRealMethod().when(mView).setVisibility(anyInt()); doCallRealMethod().when(mView).getVisibility(); when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser()); mController = new DreamOverlayStatusBarViewController( mView, mResources, mMainExecutor, mConnectivityManager, mTouchSession, mAlarmManager, mNextAlarmController, mDateFormatUtil, mSensorPrivacyController, Optional.of(mDreamOverlayNotificationCountProvider), mZenModeController, mStatusBarWindowStateController, mDreamOverlayStatusBarItemsProvider, mDreamOverlayStateController, mUserTracker, mLogBuffer); } @Test public void testOnViewAttachedAddsCallbacks() { mController.onViewAttached(); verify(mNextAlarmController).addCallback(any()); verify(mSensorPrivacyController).addCallback(any()); verify(mZenModeController).addCallback(any()); verify(mDreamOverlayNotificationCountProvider).addCallback(any()); verify(mDreamOverlayStatusBarItemsProvider).addCallback(any()); verify(mDreamOverlayStateController).addCallback(any()); } @Test public void testOnViewAttachedRegistersNetworkCallback() { mController.onViewAttached(); verify(mConnectivityManager) .registerNetworkCallback(any(NetworkRequest.class), any( ConnectivityManager.NetworkCallback.class)); } @Test public void testOnViewAttachedShowsWifiIconWhenWifiUnavailable() { when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) .thenReturn(false); when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(mNetworkCapabilities); mController.onViewAttached(); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_WIFI_UNAVAILABLE, true, null); } @Test public void testOnViewAttachedHidesWifiIconWhenWifiAvailable() { when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) .thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(mNetworkCapabilities); mController.onViewAttached(); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_WIFI_UNAVAILABLE, false, null); } @Test public void testOnViewAttachedShowsWifiIconWhenNetworkCapabilitiesUnavailable() { when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(null); mController.onViewAttached(); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_WIFI_UNAVAILABLE, true, null); } @Test public void testOnViewAttachedShowsAlarmIconWhenAlarmExists() { when(mAlarmClockInfo.getTriggerTime()).thenReturn(1L); when(mAlarmManager.getNextAlarmClock(anyInt())).thenReturn(mAlarmClockInfo); mController.onViewAttached(); verify(mView).showIcon( eq(DreamOverlayStatusBarView.STATUS_ICON_ALARM_SET), eq(true), any()); } @Test public void testOnViewAttachedHidesAlarmIconWhenNoAlarmExists() { when(mAlarmManager.getNextAlarmClock(anyInt())).thenReturn(null); mController.onViewAttached(); verify(mView).showIcon( eq(DreamOverlayStatusBarView.STATUS_ICON_ALARM_SET), eq(false), isNull()); } @Test public void testOnViewAttachedShowsMicIconWhenDisabled() { when(mSensorPrivacyController.isSensorBlocked(SensorPrivacyManager.Sensors.MICROPHONE)) .thenReturn(true); when(mSensorPrivacyController.isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA)) .thenReturn(false); mController.onViewAttached(); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_MIC_DISABLED, true, null); } @Test public void testOnViewAttachedShowsCameraIconWhenDisabled() { when(mSensorPrivacyController.isSensorBlocked(SensorPrivacyManager.Sensors.MICROPHONE)) .thenReturn(false); when(mSensorPrivacyController.isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA)) .thenReturn(true); mController.onViewAttached(); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_CAMERA_DISABLED, true, null); } @Test public void testOnViewAttachedShowsMicCameraIconWhenDisabled() { when(mSensorPrivacyController.isSensorBlocked(SensorPrivacyManager.Sensors.MICROPHONE)) .thenReturn(true); when(mSensorPrivacyController.isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA)) .thenReturn(true); mController.onViewAttached(); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_MIC_CAMERA_DISABLED, true, null); } @Test public void testOnViewAttachedShowsNotificationsIconWhenNotificationsExist() { mController.onViewAttached(); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(DreamOverlayNotificationCountProvider.Callback.class); verify(mDreamOverlayNotificationCountProvider).addCallback(callbackCapture.capture()); callbackCapture.getValue().onNotificationCountChanged(1); verify(mView).showIcon( eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any()); } @Test public void testOnViewAttachedHidesNotificationsIconWhenNoNotificationsExist() { mController.onViewAttached(); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(DreamOverlayNotificationCountProvider.Callback.class); verify(mDreamOverlayNotificationCountProvider).addCallback(callbackCapture.capture()); callbackCapture.getValue().onNotificationCountChanged(0); verify(mView).showIcon( eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(false), isNull()); } @Test public void testNotificationsIconNotShownWhenCountProviderAbsent() { DreamOverlayStatusBarViewController controller = new DreamOverlayStatusBarViewController( mView, mResources, mMainExecutor, mConnectivityManager, mTouchSession, mAlarmManager, mNextAlarmController, mDateFormatUtil, mSensorPrivacyController, Optional.empty(), mZenModeController, mStatusBarWindowStateController, mDreamOverlayStatusBarItemsProvider, mDreamOverlayStateController, mUserTracker, mLogBuffer); controller.onViewAttached(); verify(mView, never()).showIcon( eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any()); } @Test public void testOnViewAttachedShowsPriorityModeIconWhenEnabled() { when(mZenModeController.getZen()).thenReturn( Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); mController.onViewAttached(); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_PRIORITY_MODE_ON, true, null); } @Test public void testOnViewAttachedHidesPriorityModeIconWhenDisabled() { when(mZenModeController.getZen()).thenReturn( Settings.Global.ZEN_MODE_OFF); mController.onViewAttached(); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_PRIORITY_MODE_ON, false, null); } @Test public void testOnViewDetachedUnregistersNetworkCallback() { mController.onViewDetached(); verify(mConnectivityManager) .unregisterNetworkCallback(any(ConnectivityManager.NetworkCallback.class)); } @Test public void testOnViewDetachedRemovesCallbacks() { mController.onViewDetached(); verify(mNextAlarmController).removeCallback(any()); verify(mSensorPrivacyController).removeCallback(any()); verify(mZenModeController).removeCallback(any()); verify(mDreamOverlayNotificationCountProvider).removeCallback(any()); verify(mDreamOverlayStatusBarItemsProvider).removeCallback(any()); verify(mDreamOverlayStateController).removeCallback(any()); } @Test public void testOnViewDetachedRemovesViews() { mController.onViewDetached(); verify(mView).removeAllExtraStatusBarItemViews(); } @Test public void testWifiIconHiddenWhenWifiBecomesAvailable() { // Make sure wifi starts out unavailable when onViewAttached is called, and then returns // true on the second query. when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) .thenReturn(false).thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(mNetworkCapabilities); mController.onViewAttached(); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class); verify(mConnectivityManager).registerNetworkCallback(any(), callbackCapture.capture()); callbackCapture.getValue().onAvailable(mNetwork); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_WIFI_UNAVAILABLE, false, null); } @Test public void testWifiIconShownWhenWifiBecomesUnavailable() { // Make sure wifi starts out available when onViewAttached is called, then returns false // on the second query. when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) .thenReturn(true).thenReturn(false); when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(mNetworkCapabilities); mController.onViewAttached(); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class); verify(mConnectivityManager).registerNetworkCallback(any(), callbackCapture.capture()); callbackCapture.getValue().onLost(mNetwork); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_WIFI_UNAVAILABLE, true, null); } @Test public void testWifiIconHiddenWhenCapabilitiesChange() { // Make sure wifi starts out unavailable when onViewAttached is called. when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) .thenReturn(false); when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(mNetworkCapabilities); mController.onViewAttached(); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class); verify(mConnectivityManager).registerNetworkCallback(any(), callbackCapture.capture()); when(mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) .thenReturn(true); callbackCapture.getValue().onCapabilitiesChanged(mNetwork, mNetworkCapabilities); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_WIFI_UNAVAILABLE, false, null); } @Test public void testNotificationsIconShownWhenNotificationAdded() { mController.onViewAttached(); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(DreamOverlayNotificationCountProvider.Callback.class); verify(mDreamOverlayNotificationCountProvider).addCallback(callbackCapture.capture()); callbackCapture.getValue().onNotificationCountChanged(1); verify(mView).showIcon( eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any()); } @Test public void testNotificationsIconHiddenWhenLastNotificationRemoved() { mController.onViewAttached(); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(DreamOverlayNotificationCountProvider.Callback.class); verify(mDreamOverlayNotificationCountProvider).addCallback(callbackCapture.capture()); callbackCapture.getValue().onNotificationCountChanged(0); verify(mView).showIcon( eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(false), any()); } @Test public void testMicCameraIconShownWhenSensorsBlocked() { mController.onViewAttached(); when(mSensorPrivacyController.isSensorBlocked(SensorPrivacyManager.Sensors.MICROPHONE)) .thenReturn(true); when(mSensorPrivacyController.isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA)) .thenReturn(true); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(IndividualSensorPrivacyController.Callback.class); verify(mSensorPrivacyController).addCallback(callbackCapture.capture()); callbackCapture.getValue().onSensorBlockedChanged( SensorPrivacyManager.Sensors.MICROPHONE, true); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_MIC_CAMERA_DISABLED, true, null); } @Test public void testPriorityModeIconShownWhenZenModeEnabled() { mController.onViewAttached(); when(mZenModeController.getZen()).thenReturn(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(ZenModeController.Callback.class); verify(mZenModeController).addCallback(callbackCapture.capture()); callbackCapture.getValue().onZenChanged(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_PRIORITY_MODE_ON, true, null); } @Test public void testPriorityModeIconHiddenWhenZenModeDisabled() { when(mZenModeController.getZen()).thenReturn(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS) .thenReturn(Settings.Global.ZEN_MODE_OFF); mController.onViewAttached(); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(ZenModeController.Callback.class); verify(mZenModeController).addCallback(callbackCapture.capture()); callbackCapture.getValue().onZenChanged(Settings.Global.ZEN_MODE_OFF); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_PRIORITY_MODE_ON, false, null); } @Test public void testAssistantAttentionIconShownWhenAttentionGained() { mController.onViewAttached(); when(mDreamOverlayStateController.hasAssistantAttention()).thenReturn(true); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); verify(mDreamOverlayStateController).addCallback(callbackCapture.capture()); callbackCapture.getValue().onStateChanged(); verify(mView).showIcon( DreamOverlayStatusBarView.STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE, true, null); } @Test public void testStatusBarHiddenWhenSystemStatusBarShown() { mController.onViewAttached(); updateEntryAnimationsFinished(); updateStatusBarWindowState(true); assertThat(mView.getVisibility()).isEqualTo(View.INVISIBLE); } @Test public void testStatusBarShownWhenSystemStatusBarHidden() { mController.onViewAttached(); reset(mView); updateEntryAnimationsFinished(); updateStatusBarWindowState(false); assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE); } @Test public void testUnattachedStatusBarVisibilityUnchangedWhenSystemStatusBarHidden() { mController.onViewAttached(); updateEntryAnimationsFinished(); mController.onViewDetached(); reset(mView); updateStatusBarWindowState(true); verify(mView, never()).setVisibility(anyInt()); } @Test public void testNoChangeToVisibilityBeforeDreamStartedWhenStatusBarHidden() { mController.onViewAttached(); // Trigger status bar window state change. final StatusBarWindowStateListener listener = updateStatusBarWindowState(false); // Verify no visibility change because dream not started. verify(mView, never()).setVisibility(anyInt()); // Dream entry animations finished. updateEntryAnimationsFinished(); // Trigger another status bar window state change, and verify visibility change. listener.onStatusBarWindowStateChanged(WINDOW_STATE_HIDDEN); assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE); } @Test public void testExtraStatusBarItemSetWhenItemsChange() { mController.onViewAttached(); when(mStatusBarItem.getView()).thenReturn(mStatusBarItemView); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass( DreamOverlayStatusBarItemsProvider.Callback.class); verify(mDreamOverlayStatusBarItemsProvider).addCallback(callbackCapture.capture()); callbackCapture.getValue().onStatusBarItemsChanged(List.of(mStatusBarItem)); verify(mView).setExtraStatusBarItemViews(List.of(mStatusBarItemView)); } @Test public void testLowLightHidesStatusBar() { when(mDreamOverlayStateController.isLowLightActive()).thenReturn(true); mController.onViewAttached(); updateEntryAnimationsFinished(); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); verify(mDreamOverlayStateController).addCallback(callbackCapture.capture()); callbackCapture.getValue().onStateChanged(); assertThat(mView.getVisibility()).isEqualTo(View.INVISIBLE); reset(mView); when(mDreamOverlayStateController.isLowLightActive()).thenReturn(false); callbackCapture.getValue().onStateChanged(); assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE); } @Test public void testNoChangeToVisibilityBeforeDreamStartedWhenLowLightStateChange() { when(mDreamOverlayStateController.isLowLightActive()).thenReturn(false); mController.onViewAttached(); // No change to visibility because dream not fully started. verify(mView, never()).setVisibility(anyInt()); // Dream entry animations finished. updateEntryAnimationsFinished(); // Trigger state change and verify visibility changed. final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); verify(mDreamOverlayStateController).addCallback(callbackCapture.capture()); callbackCapture.getValue().onStateChanged(); assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE); } @Test public void testDreamOverlayStatusBarVisibleSetToFalseOnDetach() { mController.onViewAttached(); mController.onViewDetached(); verify(mDreamOverlayStateController).setDreamOverlayStatusBarVisible(false); } private StatusBarWindowStateListener updateStatusBarWindowState(boolean show) { when(mStatusBarWindowStateController.windowIsShowing()).thenReturn(show); final ArgumentCaptor callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class); verify(mStatusBarWindowStateController).addListener(callbackCapture.capture()); final StatusBarWindowStateListener listener = callbackCapture.getValue(); listener.onStatusBarWindowStateChanged(show ? WINDOW_STATE_SHOWING : WINDOW_STATE_HIDDEN); return listener; } private void updateEntryAnimationsFinished() { when(mDreamOverlayStateController.areEntryAnimationsFinished()).thenReturn(true); verify(mDreamOverlayStateController).addCallback(mCallbackCaptor.capture()); final DreamOverlayStateController.Callback callback = mCallbackCaptor.getValue(); callback.onStateChanged(); } private static class MockDreamOverlayStatusBarView extends DreamOverlayStatusBarView { private int mVisibility = View.VISIBLE; private MockDreamOverlayStatusBarView(Context context) { super(context); } @Override public void setVisibility(int visibility) { mVisibility = visibility; } @Override public int getVisibility() { return mVisibility; } } }