/* * Copyright (C) 20019 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.server; import static android.app.UiModeManager.MODE_NIGHT_AUTO; import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; import static android.app.UiModeManager.MODE_NIGHT_NO; import static android.app.UiModeManager.MODE_NIGHT_YES; import static android.app.UiModeManager.PROJECTION_TYPE_ALL; import static android.app.UiModeManager.PROJECTION_TYPE_AUTOMOTIVE; import static android.app.UiModeManager.PROJECTION_TYPE_NONE; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static org.testng.Assert.assertThrows; import android.Manifest; import android.app.AlarmManager; import android.app.IOnProjectionStateChangedListener; import android.app.IUiModeManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; import android.os.RemoteException; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import com.android.server.twilight.TwilightListener; import com.android.server.twilight.TwilightManager; import com.android.server.twilight.TwilightState; import com.android.server.wm.WindowManagerInternal; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.util.List; import java.util.function.Consumer; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class UiModeManagerServiceTest extends UiServiceTestCase { private static final String PACKAGE_NAME = "Diane Coffee"; private UiModeManagerService mUiManagerService; private IUiModeManager mService; @Mock private ContentResolver mContentResolver; @Mock private WindowManagerInternal mWindowManager; @Mock private Context mContext; @Mock private Resources mResources; @Mock private TwilightManager mTwilightManager; @Mock private PowerManager.WakeLock mWakeLock; @Mock private AlarmManager mAlarmManager; @Mock private PowerManager mPowerManager; @Mock private TwilightState mTwilightState; @Mock PowerManagerInternal mLocalPowerManager; @Mock private PackageManager mPackageManager; @Mock private IBinder mBinder; private BroadcastReceiver mScreenOffCallback; private BroadcastReceiver mTimeChangedCallback; private AlarmManager.OnAlarmListener mCustomListener; private Consumer mPowerSaveConsumer; private TwilightListener mTwilightListener; @Before public void setUp() { initMocks(this); when(mContext.checkCallingOrSelfPermission(anyString())) .thenReturn(PackageManager.PERMISSION_GRANTED); doAnswer(inv -> { mTwilightListener = (TwilightListener) inv.getArgument(0); return null; }).when(mTwilightManager).registerListener(any(), any()); doAnswer(inv -> { mPowerSaveConsumer = (Consumer) inv.getArgument(1); return null; }).when(mLocalPowerManager).registerLowPowerModeObserver(anyInt(), any()); when(mLocalPowerManager.getLowPowerState(anyInt())) .thenReturn(new PowerSaveState.Builder().setBatterySaverEnabled(false).build()); when(mContext.getResources()).thenReturn(mResources); when(mContext.getContentResolver()).thenReturn(mContentResolver); when(mContext.getPackageManager()).thenReturn(mPackageManager); when(mPowerManager.isInteractive()).thenReturn(true); when(mPowerManager.newWakeLock(anyInt(), anyString())).thenReturn(mWakeLock); when(mTwilightManager.getLastTwilightState()).thenReturn(mTwilightState); when(mTwilightState.isNight()).thenReturn(true); when(mContext.registerReceiver(notNull(), notNull())).then(inv -> { IntentFilter filter = inv.getArgument(1); if (filter.hasAction(Intent.ACTION_TIMEZONE_CHANGED)) { mTimeChangedCallback = inv.getArgument(0); } if (filter.hasAction(Intent.ACTION_SCREEN_OFF)) { mScreenOffCallback = inv.getArgument(0); } return null; }); doAnswer(inv -> { mCustomListener = inv.getArgument(3); return null; }).when(mAlarmManager).setExact(anyInt(), anyLong(), anyString(), any(AlarmManager.OnAlarmListener.class), any(Handler.class)); doAnswer(inv -> { mCustomListener = () -> {}; return null; }).when(mAlarmManager).cancel(eq(mCustomListener)); when(mContext.getSystemService(eq(Context.POWER_SERVICE))) .thenReturn(mPowerManager); when(mContext.getSystemService(eq(Context.ALARM_SERVICE))) .thenReturn(mAlarmManager); addLocalService(WindowManagerInternal.class, mWindowManager); addLocalService(PowerManagerInternal.class, mLocalPowerManager); addLocalService(TwilightManager.class, mTwilightManager); mUiManagerService = new UiModeManagerService(mContext, /* setupWizardComplete= */ true, mTwilightManager, new TestInjector()); try { mUiManagerService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); } catch (SecurityException e) {/* ignore for permission denial */} mService = mUiManagerService.getService(); } private void addLocalService(Class clazz, T service) { LocalServices.removeServiceForTest(clazz); LocalServices.addService(clazz, service); } @Ignore // b/152719290 - Fails on stage-aosp-master @Test public void setNightMoveActivated_overridesFunctionCorrectly() throws RemoteException { // set up when(mPowerManager.isInteractive()).thenReturn(false); mService.setNightMode(MODE_NIGHT_NO); assertFalse(mUiManagerService.getConfiguration().isNightModeActive()); // assume it is day time doReturn(false).when(mTwilightState).isNight(); // set mode to auto mService.setNightMode(MODE_NIGHT_AUTO); // set night mode on overriding current config mService.setNightModeActivated(true); assertTrue(mUiManagerService.getConfiguration().isNightModeActive()); // now it is night time doReturn(true).when(mTwilightState).isNight(); mTwilightListener.onTwilightStateChanged(mTwilightState); assertTrue(mUiManagerService.getConfiguration().isNightModeActive()); // now it is next day mid day doReturn(false).when(mTwilightState).isNight(); mTwilightListener.onTwilightStateChanged(mTwilightState); assertFalse(mUiManagerService.getConfiguration().isNightModeActive()); } @Test public void setAutoMode_screenOffRegistered() throws RemoteException { try { mService.setNightMode(MODE_NIGHT_NO); } catch (SecurityException e) { /* we should ignore this update config exception*/ } mService.setNightMode(MODE_NIGHT_AUTO); verify(mContext, atLeastOnce()).registerReceiver(any(BroadcastReceiver.class), any()); } @Ignore // b/152719290 - Fails on stage-aosp-master @Test public void setAutoMode_screenOffUnRegistered() throws RemoteException { try { mService.setNightMode(MODE_NIGHT_AUTO); } catch (SecurityException e) { /* we should ignore this update config exception*/ } try { mService.setNightMode(MODE_NIGHT_NO); } catch (SecurityException e) { /*we should ignore this update config exception*/ } given(mContext.registerReceiver(any(), any())).willThrow(SecurityException.class); verify(mContext, atLeastOnce()).unregisterReceiver(any(BroadcastReceiver.class)); } @Test public void setNightModeActivated_fromNoToYesAndBAck() throws RemoteException { mService.setNightMode(MODE_NIGHT_NO); mService.setNightModeActivated(true); assertTrue(isNightModeActivated()); mService.setNightModeActivated(false); assertFalse(isNightModeActivated()); } @Test public void setNightModeActivated_permissiontoChangeOtherUsers() throws RemoteException { SystemService.TargetUser user = mock(SystemService.TargetUser.class); doReturn(9).when(user).getUserIdentifier(); mUiManagerService.onUserSwitching(user, user); when(mContext.checkCallingOrSelfPermission( eq(Manifest.permission.INTERACT_ACROSS_USERS))) .thenReturn(PackageManager.PERMISSION_DENIED); assertFalse(mService.setNightModeActivated(true)); } @Test public void autoNightModeSwitch_batterySaverOn() throws RemoteException { mService.setNightMode(MODE_NIGHT_NO); when(mTwilightState.isNight()).thenReturn(false); mService.setNightMode(MODE_NIGHT_AUTO); // night NO assertFalse(isNightModeActivated()); mPowerSaveConsumer.accept( new PowerSaveState.Builder().setBatterySaverEnabled(true).build()); // night YES assertTrue(isNightModeActivated()); } @Test public void setAutoMode_clearCache() throws RemoteException { try { mService.setNightMode(MODE_NIGHT_AUTO); } catch (SecurityException e) { /* we should ignore this update config exception*/ } try { mService.setNightMode(MODE_NIGHT_NO); } catch (SecurityException e) { /* we should ignore this update config exception*/ } verify(mWindowManager).clearSnapshotCache(); } @Test public void setNightModeActive_fromNightModeYesToNoWhenFalse() throws RemoteException { try { mService.setNightMode(MODE_NIGHT_YES); } catch (SecurityException e) { /* we should ignore this update config exception*/ } try { mService.setNightModeActivated(false); } catch (SecurityException e) { /* we should ignore this update config exception*/ } assertEquals(MODE_NIGHT_NO, mService.getNightMode()); } @Test public void setNightModeActive_fromNightModeNoToYesWhenTrue() throws RemoteException { try { mService.setNightMode(MODE_NIGHT_NO); } catch (SecurityException e) { /* we should ignore this update config exception*/ } try { mService.setNightModeActivated(true); } catch (SecurityException e) { /* we should ignore this update config exception*/ } assertEquals(MODE_NIGHT_YES, mService.getNightMode()); } @Test public void setNightModeActive_autoNightModeNoChanges() throws RemoteException { try { mService.setNightMode(MODE_NIGHT_AUTO); } catch (SecurityException e) { /* we should ignore this update config exception*/ } try { mService.setNightModeActivated(true); } catch (SecurityException e) { /* we should ignore this update config exception*/ } assertEquals(MODE_NIGHT_AUTO, mService.getNightMode()); } @Test public void isNightModeActive_nightModeYes() throws RemoteException { try { mService.setNightMode(MODE_NIGHT_YES); } catch (SecurityException e) { /* we should ignore this update config exception*/ } assertTrue(isNightModeActivated()); } @Test public void isNightModeActive_nightModeNo() throws RemoteException { try { mService.setNightMode(MODE_NIGHT_NO); } catch (SecurityException e) { /* we should ignore this update config exception*/ } assertFalse(isNightModeActivated()); } @Test public void customTime_darkThemeOn() throws RemoteException { LocalTime now = LocalTime.now(); mService.setNightMode(MODE_NIGHT_NO); mService.setCustomNightModeStart(now.minusHours(1L).toNanoOfDay() / 1000); mService.setCustomNightModeEnd(now.plusHours(1L).toNanoOfDay() / 1000); mService.setNightMode(MODE_NIGHT_CUSTOM); mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); assertTrue(isNightModeActivated()); } @Test public void customTime_darkThemeOff() throws RemoteException { LocalTime now = LocalTime.now(); mService.setNightMode(MODE_NIGHT_YES); mService.setCustomNightModeStart(now.plusHours(1L).toNanoOfDay() / 1000); mService.setCustomNightModeEnd(now.minusHours(1L).toNanoOfDay() / 1000); mService.setNightMode(MODE_NIGHT_CUSTOM); mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); assertFalse(isNightModeActivated()); } @Test public void customTime_darkThemeOff_afterStartEnd() throws RemoteException { LocalTime now = LocalTime.now(); mService.setNightMode(MODE_NIGHT_YES); mService.setCustomNightModeStart(now.plusHours(1L).toNanoOfDay() / 1000); mService.setCustomNightModeEnd(now.plusHours(2L).toNanoOfDay() / 1000); mService.setNightMode(MODE_NIGHT_CUSTOM); mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); assertFalse(isNightModeActivated()); } @Test public void customTime_darkThemeOn_afterStartEnd() throws RemoteException { LocalTime now = LocalTime.now(); mService.setNightMode(MODE_NIGHT_YES); mService.setCustomNightModeStart(now.plusHours(1L).toNanoOfDay() / 1000); mService.setCustomNightModeEnd(now.plusHours(2L).toNanoOfDay() / 1000); mService.setNightMode(MODE_NIGHT_CUSTOM); mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); assertFalse(isNightModeActivated()); } @Test public void customTime_darkThemeOn_beforeStartEnd() throws RemoteException { LocalTime now = LocalTime.now(); mService.setNightMode(MODE_NIGHT_YES); mService.setCustomNightModeStart(now.minusHours(1L).toNanoOfDay() / 1000); mService.setCustomNightModeEnd(now.minusHours(2L).toNanoOfDay() / 1000); mService.setNightMode(MODE_NIGHT_CUSTOM); mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); assertTrue(isNightModeActivated()); } @Test public void customTime_darkThemeOff_beforeStartEnd() throws RemoteException { LocalTime now = LocalTime.now(); mService.setNightMode(MODE_NIGHT_YES); mService.setCustomNightModeStart(now.minusHours(2L).toNanoOfDay() / 1000); mService.setCustomNightModeEnd(now.minusHours(1L).toNanoOfDay() / 1000); mService.setNightMode(MODE_NIGHT_CUSTOM); mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); assertFalse(isNightModeActivated()); } @Test public void customTIme_customAlarmSetWhenScreenTimeChanges() throws RemoteException { when(mPowerManager.isInteractive()).thenReturn(false); mService.setNightMode(MODE_NIGHT_CUSTOM); verify(mAlarmManager, times(1)) .setExact(anyInt(), anyLong(), anyString(), any(), any()); mTimeChangedCallback.onReceive(mContext, new Intent(Intent.ACTION_TIME_CHANGED)); verify(mAlarmManager, atLeast(2)) .setExact(anyInt(), anyLong(), anyString(), any(), any()); } @Test public void customTime_alarmSetInTheFutureWhenOn() throws RemoteException { LocalDateTime now = LocalDateTime.now(); when(mPowerManager.isInteractive()).thenReturn(false); mService.setNightMode(MODE_NIGHT_YES); mService.setCustomNightModeStart(now.toLocalTime().minusHours(1L).toNanoOfDay() / 1000); mService.setCustomNightModeEnd(now.toLocalTime().plusHours(1L).toNanoOfDay() / 1000); LocalDateTime next = now.plusHours(1L); final long millis = next.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); mService.setNightMode(MODE_NIGHT_CUSTOM); verify(mAlarmManager) .setExact(anyInt(), eq(millis), anyString(), any(), any()); } @Test public void customTime_appliesImmediatelyWhenScreenOff() throws RemoteException { when(mPowerManager.isInteractive()).thenReturn(false); LocalTime now = LocalTime.now(); mService.setNightMode(MODE_NIGHT_NO); mService.setCustomNightModeStart(now.minusHours(1L).toNanoOfDay() / 1000); mService.setCustomNightModeEnd(now.plusHours(1L).toNanoOfDay() / 1000); mService.setNightMode(MODE_NIGHT_CUSTOM); assertTrue(isNightModeActivated()); } @Test public void customTime_appliesOnlyWhenScreenOff() throws RemoteException { LocalTime now = LocalTime.now(); mService.setNightMode(MODE_NIGHT_NO); mService.setCustomNightModeStart(now.minusHours(1L).toNanoOfDay() / 1000); mService.setCustomNightModeEnd(now.plusHours(1L).toNanoOfDay() / 1000); mService.setNightMode(MODE_NIGHT_CUSTOM); assertFalse(isNightModeActivated()); mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); assertTrue(isNightModeActivated()); } @Test public void nightAuto_appliesOnlyWhenScreenOff() throws RemoteException { when(mTwilightState.isNight()).thenReturn(true); mService.setNightMode(MODE_NIGHT_NO); mService.setNightMode(MODE_NIGHT_AUTO); assertFalse(isNightModeActivated()); mScreenOffCallback.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); assertTrue(isNightModeActivated()); } private boolean isNightModeActivated() { return (mUiManagerService.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) != 0; } @Test public void requestProjection_failsForBogusPackageName() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID + 1); assertThrows(SecurityException.class, () -> mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); } @Test public void requestProjection_failsIfNameNotFound() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenThrow(new PackageManager.NameNotFoundException()); assertThrows(SecurityException.class, () -> mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); } @Test public void requestProjection_failsIfNoProjectionTypes() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); assertThrows(IllegalArgumentException.class, () -> mService.requestProjection(mBinder, PROJECTION_TYPE_NONE, PACKAGE_NAME)); verify(mContext, never()).enforceCallingPermission( eq(Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION), any()); verifyZeroInteractions(mBinder); assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); } @Test public void requestProjection_failsIfMultipleProjectionTypes() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); // Don't use PROJECTION_TYPE_ALL because that's actually == -1 and will fail the > 0 check. int multipleProjectionTypes = PROJECTION_TYPE_AUTOMOTIVE | 0x0002 | 0x0004; assertThrows(IllegalArgumentException.class, () -> mService.requestProjection(mBinder, multipleProjectionTypes, PACKAGE_NAME)); verify(mContext, never()).enforceCallingPermission( eq(Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION), any()); verifyZeroInteractions(mBinder); assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); } @Test public void requestProjection_enforcesToggleAutomotiveProjectionPermission() throws Exception { doThrow(new SecurityException()) .when(mPackageManager).getPackageUidAsUser(eq(PACKAGE_NAME), anyInt()); assertThrows(SecurityException.class, () -> mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); } @Test public void requestProjection_automotive_failsIfAlreadySetByOtherPackage() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); String otherPackage = "Raconteurs"; when(mPackageManager.getPackageUidAsUser(eq(otherPackage), anyInt())) .thenReturn(TestInjector.CALLING_UID); assertFalse(mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, otherPackage)); assertThat(mService.getProjectingPackages(PROJECTION_TYPE_AUTOMOTIVE), contains(PACKAGE_NAME)); } @Test public void requestProjection_failsIfCannotLinkToDeath() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); doThrow(new RemoteException()).when(mBinder).linkToDeath(any(), anyInt()); assertFalse(mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); } @Test public void requestProjection() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); // Should work for all powers of two. for (int i = 0; i < Integer.SIZE; ++i) { int projectionType = 1 << i; assertTrue(mService.requestProjection(mBinder, projectionType, PACKAGE_NAME)); assertTrue((mService.getActiveProjectionTypes() & projectionType) != 0); assertThat(mService.getProjectingPackages(projectionType), contains(PACKAGE_NAME)); // Subsequent calls should still succeed. assertTrue(mService.requestProjection(mBinder, projectionType, PACKAGE_NAME)); } assertEquals(PROJECTION_TYPE_ALL, mService.getActiveProjectionTypes()); } @Test public void releaseProjection_failsForBogusPackageName() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID + 1); assertThrows(SecurityException.class, () -> mService.releaseProjection( PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); } @Test public void releaseProjection_failsIfNameNotFound() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenThrow(new PackageManager.NameNotFoundException()); assertThrows(SecurityException.class, () -> mService.releaseProjection( PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); } @Test public void releaseProjection_enforcesToggleAutomotiveProjectionPermission() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); doThrow(new SecurityException()).when(mContext).enforceCallingPermission( eq(Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION), any()); // Should not be enforced for other types of projection. int nonAutomotiveProjectionType = PROJECTION_TYPE_AUTOMOTIVE * 2; mService.releaseProjection(nonAutomotiveProjectionType, PACKAGE_NAME); assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); assertThrows(SecurityException.class, () -> mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); } @Test public void releaseProjection() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); requestAllPossibleProjectionTypes(); assertEquals(PROJECTION_TYPE_ALL, mService.getActiveProjectionTypes()); assertTrue(mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); int everythingButAutomotive = PROJECTION_TYPE_ALL & ~PROJECTION_TYPE_AUTOMOTIVE; assertEquals(everythingButAutomotive, mService.getActiveProjectionTypes()); for (int i = 0; i < Integer.SIZE; ++i) { int projectionType = 1 << i; assertEquals(projectionType != PROJECTION_TYPE_AUTOMOTIVE, (boolean) mService.releaseProjection(projectionType, PACKAGE_NAME)); } assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); } @Test public void binderDeath_releasesProjection() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); requestAllPossibleProjectionTypes(); assertEquals(PROJECTION_TYPE_ALL, mService.getActiveProjectionTypes()); ArgumentCaptor deathRecipientCaptor = ArgumentCaptor.forClass( IBinder.DeathRecipient.class); verify(mBinder, atLeastOnce()).linkToDeath(deathRecipientCaptor.capture(), anyInt()); // Wipe them out. All of them. deathRecipientCaptor.getAllValues().forEach(IBinder.DeathRecipient::binderDied); assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); } @Test public void getActiveProjectionTypes() throws Exception { assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); } @Test public void getProjectingPackages() throws Exception { assertTrue(mService.getProjectingPackages(PROJECTION_TYPE_ALL).isEmpty()); when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); assertEquals(1, mService.getProjectingPackages(PROJECTION_TYPE_AUTOMOTIVE).size()); assertEquals(1, mService.getProjectingPackages(PROJECTION_TYPE_ALL).size()); assertThat(mService.getProjectingPackages(PROJECTION_TYPE_AUTOMOTIVE), contains(PACKAGE_NAME)); assertThat(mService.getProjectingPackages(PROJECTION_TYPE_ALL), contains(PACKAGE_NAME)); mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); assertThat(mService.getProjectingPackages(PROJECTION_TYPE_ALL), empty()); } @Test public void addOnProjectionStateChangedListener_enforcesReadProjStatePermission() { doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission( eq(android.Manifest.permission.READ_PROJECTION_STATE), any()); IOnProjectionStateChangedListener listener = mock(IOnProjectionStateChangedListener.class); assertThrows(SecurityException.class, () -> mService.addOnProjectionStateChangedListener( listener, PROJECTION_TYPE_ALL)); } @Test public void addOnProjectionStateChangedListener_callsListenerIfProjectionActive() throws Exception { when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); IOnProjectionStateChangedListener listener = mock(IOnProjectionStateChangedListener.class); when(listener.asBinder()).thenReturn(mBinder); // Any binder will do mService.addOnProjectionStateChangedListener(listener, PROJECTION_TYPE_ALL); verify(listener).onProjectionStateChanged(eq(PROJECTION_TYPE_AUTOMOTIVE), eq(List.of(PACKAGE_NAME))); } @Test public void removeOnProjectionStateChangedListener_enforcesReadProjStatePermission() { doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission( eq(android.Manifest.permission.READ_PROJECTION_STATE), any()); IOnProjectionStateChangedListener listener = mock(IOnProjectionStateChangedListener.class); assertThrows(SecurityException.class, () -> mService.removeOnProjectionStateChangedListener( listener)); } @Test public void removeOnProjectionStateChangedListener() throws Exception { IOnProjectionStateChangedListener listener = mock(IOnProjectionStateChangedListener.class); when(listener.asBinder()).thenReturn(mBinder); // Any binder will do. mService.addOnProjectionStateChangedListener(listener, PROJECTION_TYPE_ALL); mService.removeOnProjectionStateChangedListener(listener); // Now set automotive projection, should not call back. when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); verify(listener, never()).onProjectionStateChanged(anyInt(), any()); } @Test public void projectionStateChangedListener_calledWhenStateChanges() throws Exception { IOnProjectionStateChangedListener listener = mock(IOnProjectionStateChangedListener.class); when(listener.asBinder()).thenReturn(mBinder); // Any binder will do. mService.addOnProjectionStateChangedListener(listener, PROJECTION_TYPE_ALL); verify(listener, atLeastOnce()).asBinder(); // Called twice during register. // No calls initially, no projection state set. verifyNoMoreInteractions(listener); // Now set automotive projection, should call back. when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); verify(listener).onProjectionStateChanged(eq(PROJECTION_TYPE_AUTOMOTIVE), eq(List.of(PACKAGE_NAME))); // Subsequent calls that are noops do nothing. mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); int unsetProjectionType = 0x0002; mService.releaseProjection(unsetProjectionType, PACKAGE_NAME); verifyNoMoreInteractions(listener); // Release should call back though. mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); verify(listener).onProjectionStateChanged(eq(PROJECTION_TYPE_NONE), eq(List.of())); // But only the first time. mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); verifyNoMoreInteractions(listener); } @Test public void projectionStateChangedListener_calledForAnyRelevantStateChange() throws Exception { int fakeProjectionType = 0x0002; int otherFakeProjectionType = 0x0004; String otherPackageName = "Internet Arms"; when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); when(mPackageManager.getPackageUidAsUser(eq(otherPackageName), anyInt())) .thenReturn(TestInjector.CALLING_UID); IOnProjectionStateChangedListener listener = mock(IOnProjectionStateChangedListener.class); when(listener.asBinder()).thenReturn(mBinder); // Any binder will do. IOnProjectionStateChangedListener listener2 = mock(IOnProjectionStateChangedListener.class); when(listener2.asBinder()).thenReturn(mBinder); // Any binder will do. mService.addOnProjectionStateChangedListener(listener, fakeProjectionType); mService.addOnProjectionStateChangedListener(listener2, fakeProjectionType | otherFakeProjectionType); verify(listener, atLeastOnce()).asBinder(); // Called twice during register. verify(listener2, atLeastOnce()).asBinder(); // Called twice during register. mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); verifyNoMoreInteractions(listener, listener2); // fakeProjectionType should trigger both. mService.requestProjection(mBinder, fakeProjectionType, PACKAGE_NAME); verify(listener).onProjectionStateChanged(eq(fakeProjectionType), eq(List.of(PACKAGE_NAME))); verify(listener2).onProjectionStateChanged(eq(fakeProjectionType), eq(List.of(PACKAGE_NAME))); // otherFakeProjectionType should only trigger the second listener. mService.requestProjection(mBinder, otherFakeProjectionType, otherPackageName); verifyNoMoreInteractions(listener); verify(listener2).onProjectionStateChanged( eq(fakeProjectionType | otherFakeProjectionType), eq(List.of(PACKAGE_NAME, otherPackageName))); // Turning off fakeProjectionType should trigger both again. mService.releaseProjection(fakeProjectionType, PACKAGE_NAME); verify(listener).onProjectionStateChanged(eq(PROJECTION_TYPE_NONE), eq(List.of())); verify(listener2).onProjectionStateChanged(eq(otherFakeProjectionType), eq(List.of(otherPackageName))); // Turning off otherFakeProjectionType should only trigger the second listener. mService.releaseProjection(otherFakeProjectionType, otherPackageName); verifyNoMoreInteractions(listener); verify(listener2).onProjectionStateChanged(eq(PROJECTION_TYPE_NONE), eq(List.of())); } @Test public void projectionStateChangedListener_unregisteredOnDeath() throws Exception { IOnProjectionStateChangedListener listener = mock(IOnProjectionStateChangedListener.class); IBinder listenerBinder = mock(IBinder.class); when(listener.asBinder()).thenReturn(listenerBinder); mService.addOnProjectionStateChangedListener(listener, PROJECTION_TYPE_ALL); ArgumentCaptor listenerDeathRecipient = ArgumentCaptor.forClass( IBinder.DeathRecipient.class); verify(listenerBinder).linkToDeath(listenerDeathRecipient.capture(), anyInt()); // Now kill the binder for the listener. This should remove it from the list of listeners. listenerDeathRecipient.getValue().binderDied(); when(mPackageManager.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())) .thenReturn(TestInjector.CALLING_UID); mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); verify(listener, never()).onProjectionStateChanged(anyInt(), any()); } private void requestAllPossibleProjectionTypes() throws RemoteException { for (int i = 0; i < Integer.SIZE; ++i) { mService.requestProjection(mBinder, 1 << i, PACKAGE_NAME); } } private static class TestInjector extends UiModeManagerService.Injector { private static final int CALLING_UID = 8675309; public int getCallingUid() { return CALLING_UID; } } }