| /* |
| * Copyright (C) 2023 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.car.occupantconnection; |
| |
| import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED; |
| import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; |
| import static android.car.CarOccupantZoneManager.INVALID_USER_ID; |
| import static android.car.CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER; |
| import static android.car.CarOccupantZoneManager.OCCUPANT_TYPE_FRONT_PASSENGER; |
| import static android.car.CarOccupantZoneManager.OCCUPANT_TYPE_REAR_PASSENGER; |
| import static android.car.CarRemoteDeviceManager.FLAG_CLIENT_INSTALLED; |
| import static android.car.CarRemoteDeviceManager.FLAG_CLIENT_IN_FOREGROUND; |
| import static android.car.CarRemoteDeviceManager.FLAG_CLIENT_RUNNING; |
| import static android.car.CarRemoteDeviceManager.FLAG_CLIENT_SAME_LONG_VERSION; |
| import static android.car.CarRemoteDeviceManager.FLAG_CLIENT_SAME_SIGNATURE; |
| import static android.car.CarRemoteDeviceManager.FLAG_OCCUPANT_ZONE_CONNECTION_READY; |
| import static android.car.CarRemoteDeviceManager.FLAG_OCCUPANT_ZONE_POWER_ON; |
| import static android.car.VehicleAreaSeat.SEAT_ROW_1_LEFT; |
| import static android.car.VehicleAreaSeat.SEAT_ROW_1_RIGHT; |
| import static android.car.VehicleAreaSeat.SEAT_ROW_2_RIGHT; |
| import static android.car.test.mocks.AndroidMockitoHelper.mockContextCreateContextAsUser; |
| import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_INVISIBLE; |
| import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED; |
| |
| import static com.android.car.occupantconnection.CarRemoteDeviceService.INITIAL_APP_STATE; |
| import static com.android.car.occupantconnection.CarRemoteDeviceService.INITIAL_OCCUPANT_ZONE_STATE; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.junit.Assert.assertThrows; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.anyLong; |
| import static org.mockito.Mockito.any; |
| import static org.mockito.Mockito.doAnswer; |
| import static org.mockito.Mockito.eq; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| |
| import android.app.ActivityManager; |
| import android.app.ActivityManager.RunningAppProcessInfo; |
| import android.car.Car; |
| import android.car.CarOccupantZoneManager.OccupantZoneInfo; |
| import android.car.builtin.app.ActivityManagerHelper.ProcessObserverCallback; |
| import android.car.occupantconnection.IStateCallback; |
| import android.car.user.CarUserManager.UserLifecycleEvent; |
| import android.car.user.CarUserManager.UserLifecycleListener; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.hardware.display.DisplayManager; |
| import android.hardware.display.DisplayManager.DisplayListener; |
| import android.net.Uri; |
| import android.os.Binder; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.SparseArray; |
| |
| import com.android.car.CarLocalServices; |
| import com.android.car.CarOccupantZoneService; |
| import com.android.car.SystemActivityMonitoringService; |
| import com.android.car.internal.util.BinderKeyValueContainer; |
| import com.android.car.occupantconnection.CarRemoteDeviceService.PerUserInfo; |
| import com.android.car.power.CarPowerManagementService; |
| import com.android.car.user.CarUserService; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Mock; |
| import org.mockito.junit.MockitoJUnitRunner; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Set; |
| |
| @RunWith(MockitoJUnitRunner.class) |
| public class CarRemoteDeviceServiceTest { |
| |
| private static final String PACKAGE_NAME = "my_package_name"; |
| private static final String FAKE_PACKAGE_NAME = "fake_package_name"; |
| |
| private static final int OCCUPANT_ZONE_ID = 321; |
| private static final int USER_ID = 123; |
| private static final int USER_ID2 = 234; |
| private static final int PID = 456; |
| |
| // This value is copied from android.os.UserHandle#PER_USER_RANGE. |
| private static final int PER_USER_RANGE = 100000; |
| // This value is copied from android.os.UserHandle#USER_SYSTEM. |
| private static final int USER_SYSTEM = 0; |
| |
| @Mock |
| private Context mContext; |
| @Mock |
| private CarOccupantZoneService mOccupantZoneService; |
| @Mock |
| private CarPowerManagementService mPowerManagementService; |
| @Mock |
| private SystemActivityMonitoringService mSystemActivityMonitoringService; |
| @Mock |
| private CarUserService mUserService; |
| @Mock |
| private ActivityManager mActivityManager; |
| @Mock |
| private UserManager mUserManager; |
| @Mock |
| private DisplayManager mDisplayManager; |
| @Mock |
| private IStateCallback mCallback; |
| @Mock |
| private IBinder mCallbackBinder; |
| |
| private final SparseArray<PerUserInfo> mPerUserInfoMap = new SparseArray<>(); |
| private final BinderKeyValueContainer<ClientId, IStateCallback> mCallbackMap = |
| new BinderKeyValueContainer<>(); |
| private final ArrayMap<ClientId, Integer> mAppStateMap = new ArrayMap<>(); |
| private final ArrayMap<OccupantZoneInfo, Integer> mOccupantZoneStateMap = new ArrayMap<>(); |
| |
| private final OccupantZoneInfo mOccupantZone = |
| new OccupantZoneInfo(OCCUPANT_ZONE_ID, OCCUPANT_TYPE_DRIVER, SEAT_ROW_1_LEFT); |
| private final int mMyUserId = Binder.getCallingUserHandle().getIdentifier(); |
| |
| private CarRemoteDeviceService mService; |
| |
| @Before |
| public void setUp() throws PackageManager.NameNotFoundException { |
| // Stored as static: Other tests can leave things behind and fail this test in add call. |
| // So just remove as safety guard. |
| CarLocalServices.removeServiceForTest(CarOccupantZoneService.class); |
| CarLocalServices.addService(CarOccupantZoneService.class, mOccupantZoneService); |
| CarLocalServices.removeServiceForTest(CarPowerManagementService.class); |
| CarLocalServices.addService(CarPowerManagementService.class, mPowerManagementService); |
| CarLocalServices.removeServiceForTest(SystemActivityMonitoringService.class); |
| CarLocalServices.addService(SystemActivityMonitoringService.class, |
| mSystemActivityMonitoringService); |
| CarLocalServices.removeServiceForTest(CarUserService.class); |
| CarLocalServices.addService(CarUserService.class, mUserService); |
| |
| mService = new CarRemoteDeviceService(mContext, mOccupantZoneService, |
| mPowerManagementService, mSystemActivityMonitoringService, mActivityManager, |
| mUserManager, mPerUserInfoMap, mCallbackMap, mAppStateMap, mOccupantZoneStateMap); |
| when(mContext.getSystemService(DisplayManager.class)).thenReturn(mDisplayManager); |
| mService.init(); |
| mockPackageName(); |
| when(mCallback.asBinder()).thenReturn(mCallbackBinder); |
| } |
| |
| @After |
| public void tearDown() { |
| CarLocalServices.removeServiceForTest(CarOccupantZoneService.class); |
| CarLocalServices.removeServiceForTest(CarPowerManagementService.class); |
| CarLocalServices.removeServiceForTest(SystemActivityMonitoringService.class); |
| CarLocalServices.removeServiceForTest(CarUserService.class); |
| } |
| |
| @Test |
| public void testInit() { |
| // There are three occupant zones: zone1 is assigned with a foreground user, zone2 is not |
| // assigned a user yet, and zone3 is assigned with the system user. |
| OccupantZoneInfo zone1 = new OccupantZoneInfo(/* zoneId= */ 0, |
| OCCUPANT_TYPE_DRIVER, SEAT_ROW_1_LEFT); |
| OccupantZoneInfo zone2 = new OccupantZoneInfo(/* zoneId= */ 1, |
| OCCUPANT_TYPE_FRONT_PASSENGER, SEAT_ROW_1_RIGHT); |
| OccupantZoneInfo zone3 = new OccupantZoneInfo(/* zoneId= */ 2, |
| OCCUPANT_TYPE_REAR_PASSENGER, SEAT_ROW_2_RIGHT); |
| List<OccupantZoneInfo> allZones = Arrays.asList(zone1, zone2, zone3); |
| when(mOccupantZoneService.getAllOccupantZones()).thenReturn(allZones); |
| when(mOccupantZoneService.getUserForOccupant(zone1.zoneId)).thenReturn(USER_ID); |
| when(mOccupantZoneService.getUserForOccupant(zone2.zoneId)).thenReturn(INVALID_USER_ID); |
| when(mOccupantZoneService.getUserForOccupant(zone3.zoneId)).thenReturn(USER_SYSTEM); |
| |
| Context userContext1 = mock(Context.class); |
| when(mContext.createContextAsUser(eq(UserHandle.of(USER_ID)), anyInt())) |
| .thenReturn(userContext1); |
| PackageManager pm1 = mock(PackageManager.class); |
| when(userContext1.getPackageManager()).thenReturn(pm1); |
| |
| mService.init(); |
| |
| verify(userContext1).registerReceiver(any(), any()); |
| assertThat(mPerUserInfoMap.size()).isEqualTo(1); |
| assertThat(mAppStateMap.size()).isEqualTo(0); |
| } |
| |
| @Test |
| public void testPeerClientInstallUninstall() { |
| // There are two occupant zone: my zone, peer zone. |
| mockPerUserInfo(mMyUserId, mOccupantZone); |
| |
| OccupantZoneInfo peerZone = new OccupantZoneInfo(/* zoneId= */ 0, |
| OCCUPANT_TYPE_DRIVER, SEAT_ROW_1_LEFT); |
| PerUserInfo peerUserInfo = mockPerUserInfo(USER_ID, peerZone); |
| // The BroadcastReceiver in the peerUserInfo is a mock and will do nothing when calling |
| // peerUserInfo.receiver.onReceive(), so remove it from the map. When mService.init() is |
| // called, because the map doesn't have the PerUserInfo, it will create a real |
| // BroadcastReceiver, create a new PerUserInfo with the real BroadcastReceiver, and put it |
| // into the map. |
| mPerUserInfoMap.remove(USER_ID); |
| |
| List<OccupantZoneInfo> allZones = Arrays.asList(mOccupantZone, peerZone); |
| when(mOccupantZoneService.getAllOccupantZones()).thenReturn(allZones); |
| |
| mService.init(); |
| mService.registerStateCallback(PACKAGE_NAME, mCallback); |
| // Get the PerUserInfo containing the real BroadcastReceiver. |
| peerUserInfo = mPerUserInfoMap.get(USER_ID); |
| |
| // Pretend that the peer app is installed in the beginning. |
| ClientId peerClient = new ClientId(peerZone, USER_ID, PACKAGE_NAME); |
| mAppStateMap.put(peerClient, |
| FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE); |
| |
| // Then the peer app is uninstalled. |
| Uri uri = mock(Uri.class); |
| when(uri.getSchemeSpecificPart()).thenReturn(PACKAGE_NAME); |
| Intent intent = mock(Intent.class); |
| when(intent.getData()).thenReturn(uri); |
| when(intent.getAction()).thenReturn(Intent.ACTION_PACKAGE_REMOVED); |
| peerUserInfo.receiver.onReceive(mock(Context.class), intent); |
| |
| assertThat(mAppStateMap.get(peerClient)).isEqualTo(INITIAL_APP_STATE); |
| |
| // Then the peer app is installed. |
| PackageInfo packageInfo = mock(PackageInfo.class); |
| try { |
| when(peerUserInfo.pm.getPackageInfo(eq(PACKAGE_NAME), any())).thenReturn(packageInfo); |
| } catch (PackageManager.NameNotFoundException e) { |
| throw new RuntimeException(e); |
| } |
| |
| when(intent.getAction()).thenReturn(Intent.ACTION_PACKAGE_ADDED); |
| peerUserInfo.receiver.onReceive(mock(Context.class), intent); |
| |
| assertThat(mAppStateMap.get(peerClient)).isEqualTo( |
| FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE); |
| } |
| |
| @Test |
| public void testNonPeerClientUninstall() { |
| // There are two occupant zone: my zone, peer zone. |
| mockPerUserInfo(mMyUserId, mOccupantZone); |
| |
| OccupantZoneInfo peerZone = new OccupantZoneInfo(/* zoneId= */ 0, |
| OCCUPANT_TYPE_DRIVER, SEAT_ROW_1_LEFT); |
| PerUserInfo peerUserInfo = mockPerUserInfo(USER_ID, peerZone); |
| // The BroadcastReceiver in the peerUserInfo is a mock and will do nothing when calling |
| // peerUserInfo.receiver.onReceive(), so remove it from the map. When mService.init() is |
| // called, because the map doesn't have the PerUserInfo, it will create a real |
| // BroadcastReceiver, create a new PerUserInfo with the real BroadcastReceiver, and put it |
| // into the map. |
| mPerUserInfoMap.remove(USER_ID); |
| |
| List<OccupantZoneInfo> allZones = Arrays.asList(mOccupantZone, peerZone); |
| when(mOccupantZoneService.getAllOccupantZones()).thenReturn(allZones); |
| |
| mService.init(); |
| mService.registerStateCallback(PACKAGE_NAME, mCallback); |
| // Get the PerUserInfo containing the real BroadcastReceiver. |
| peerUserInfo = mPerUserInfoMap.get(USER_ID); |
| |
| // Pretend that the peer app is installed in the beginning. |
| ClientId peerClient = new ClientId(peerZone, USER_ID, PACKAGE_NAME); |
| mAppStateMap.put(peerClient, |
| FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE); |
| |
| // Nothing should happen if an app with another package name is uninstalled. |
| String anotherPackageName = PACKAGE_NAME + "abc"; |
| Uri uri = mock(Uri.class); |
| when(uri.getSchemeSpecificPart()).thenReturn(anotherPackageName); |
| Intent intent = mock(Intent.class); |
| when(intent.getData()).thenReturn(uri); |
| when(intent.getAction()).thenReturn(Intent.ACTION_PACKAGE_REMOVED); |
| peerUserInfo.receiver.onReceive(mock(Context.class), intent); |
| |
| assertThat(mAppStateMap.get(peerClient)).isEqualTo( |
| FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE); |
| } |
| |
| @Test |
| public void testGetEndpointPackageInfoWithoutPermission_throwsException() { |
| when(mContext.checkCallingOrSelfPermission(Car.PERMISSION_MANAGE_REMOTE_DEVICE)) |
| .thenReturn(PackageManager.PERMISSION_DENIED); |
| |
| assertThrows(SecurityException.class, |
| () -> mService.getEndpointPackageInfo(OCCUPANT_ZONE_ID, PACKAGE_NAME)); |
| } |
| |
| @Test |
| public void testGetEndpointPackageInfoWithFakePackageName_throwsException() { |
| assertThrows(SecurityException.class, |
| () -> mService.getEndpointPackageInfo(OCCUPANT_ZONE_ID, FAKE_PACKAGE_NAME)); |
| } |
| |
| @Test |
| public void testGetEndpointPackageInfoWithInvalidUserId() { |
| when(mOccupantZoneService.getUserForOccupant(OCCUPANT_ZONE_ID)).thenReturn( |
| INVALID_USER_ID); |
| |
| assertThat(mService.getEndpointPackageInfo(OCCUPANT_ZONE_ID, PACKAGE_NAME)).isNull(); |
| } |
| |
| @Test |
| public void testGetEndpointPackageInfo() throws PackageManager.NameNotFoundException { |
| PackageInfo packageInfo = mock(PackageInfo.class); |
| PerUserInfo perUserInfo = mockPerUserInfo(USER_ID, mOccupantZone); |
| when(perUserInfo.pm.getPackageInfo(eq(PACKAGE_NAME), any())).thenReturn(packageInfo); |
| |
| assertThat(mService.getEndpointPackageInfo(mOccupantZone.zoneId, PACKAGE_NAME)) |
| .isEqualTo(packageInfo); |
| } |
| |
| @Test |
| public void testChangePowerStateOn() { |
| int displayId = 1; |
| int[] displays = {displayId}; |
| when(mOccupantZoneService.getAllDisplaysForOccupantZone(OCCUPANT_ZONE_ID)) |
| .thenReturn(displays); |
| |
| mService.setOccupantZonePower(mOccupantZone, true); |
| verify(mPowerManagementService).setDisplayPowerState(displayId, true); |
| } |
| |
| @Test |
| public void testChangePowerStateOff() { |
| int displayId = 1; |
| int[] displays = {displayId}; |
| when(mOccupantZoneService.getAllDisplaysForOccupantZone(OCCUPANT_ZONE_ID)) |
| .thenReturn(displays); |
| |
| mService.setOccupantZonePower(mOccupantZone, false); |
| verify(mPowerManagementService).setDisplayPowerState(displayId, false); |
| } |
| |
| @Test |
| public void testGetPowerStateOn() { |
| when(mOccupantZoneService.areDisplaysOnForOccupantZone(OCCUPANT_ZONE_ID)) |
| .thenReturn(true); |
| |
| assertThat(mService.isOccupantZonePowerOn(mOccupantZone)).isTrue(); |
| } |
| |
| @Test |
| public void testGetPowerStateOff() { |
| when(mOccupantZoneService.areDisplaysOnForOccupantZone(OCCUPANT_ZONE_ID)) |
| .thenReturn(false); |
| |
| assertThat(mService.isOccupantZonePowerOn(mOccupantZone)).isFalse(); |
| } |
| |
| @Test |
| public void testCalculateAppStateLocked_notInstalled() { |
| ClientId clientId = new ClientId(mOccupantZone, USER_ID, PACKAGE_NAME); |
| |
| assertThat(mService.calculateAppState(clientId)).isEqualTo(0); |
| } |
| |
| @Test |
| public void testCalculateAppStateLocked_installedNotRunning() { |
| ClientId clientId = new ClientId(mOccupantZone, USER_ID, PACKAGE_NAME); |
| mockAppInstalledAsUser(USER_ID, mOccupantZone); |
| |
| assertThat(mService.calculateAppState(clientId)).isEqualTo( |
| FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE); |
| } |
| |
| @Test |
| public void testCalculateAppStateLocked_runningInBackground() { |
| ClientId clientId = new ClientId(mOccupantZone, USER_ID, PACKAGE_NAME); |
| mockAppRunningAsUser(USER_ID, PID, mOccupantZone, IMPORTANCE_CACHED); |
| |
| assertThat(mService.calculateAppState(clientId)) |
| .isEqualTo(FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION |
| | FLAG_CLIENT_SAME_SIGNATURE | FLAG_CLIENT_RUNNING); |
| } |
| |
| @Test |
| public void testCalculateAppStateLocked_runningInForeground() { |
| ClientId clientId = new ClientId(mOccupantZone, USER_ID, PACKAGE_NAME); |
| mockAppRunningAsUser(USER_ID, PID, mOccupantZone, IMPORTANCE_FOREGROUND); |
| |
| assertThat(mService.calculateAppState(clientId)) |
| .isEqualTo(FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION |
| | FLAG_CLIENT_SAME_SIGNATURE | FLAG_CLIENT_RUNNING |
| | FLAG_CLIENT_IN_FOREGROUND); |
| } |
| |
| @Test |
| public void testCalculateOccupantZoneState_notPowerOn() { |
| assertThat(mService.calculateOccupantZoneState(mOccupantZone)) |
| .isEqualTo(INITIAL_OCCUPANT_ZONE_STATE); |
| } |
| |
| @Test |
| public void testCalculateOccupantZoneState_powerOn() { |
| mockOccupantZonePowerOn(mOccupantZone); |
| |
| assertThat(mService.calculateOccupantZoneState(mOccupantZone)) |
| .isEqualTo(FLAG_OCCUPANT_ZONE_POWER_ON); |
| } |
| |
| @Test |
| public void testCalculateOccupantZoneState_connectionReady() { |
| mockOccupantZoneConnectionReady(mOccupantZone, USER_ID); |
| |
| assertThat(mService.calculateOccupantZoneState(mOccupantZone)) |
| .isEqualTo(FLAG_OCCUPANT_ZONE_CONNECTION_READY); |
| } |
| |
| @Test |
| public void testRegisterStateCallbackWithoutPermission_throwsException() { |
| when(mContext.checkCallingOrSelfPermission(Car.PERMISSION_MANAGE_REMOTE_DEVICE)) |
| .thenReturn(PackageManager.PERMISSION_DENIED); |
| |
| assertThrows(SecurityException.class, |
| () -> mService.registerStateCallback(PACKAGE_NAME, any(IStateCallback.class))); |
| } |
| |
| @Test |
| public void testRegisterStateCallbackWithFakePackageName_throwsException() { |
| assertThrows(SecurityException.class, |
| () -> mService.registerStateCallback(FAKE_PACKAGE_NAME, any(IStateCallback.class))); |
| } |
| |
| @Test |
| public void testRegisterDuplicateStateCallback_throwsException() { |
| UserHandle userHandle = Binder.getCallingUserHandle(); |
| when(mOccupantZoneService.getOccupantZoneForUser(userHandle)).thenReturn(mOccupantZone); |
| mService.registerStateCallback(PACKAGE_NAME, mCallback); |
| |
| assertThrows(IllegalStateException.class, |
| () -> mService.registerStateCallback(PACKAGE_NAME, any(IStateCallback.class))); |
| } |
| |
| @Test |
| public void testRegisterStateCallback() throws RemoteException { |
| // There are three occupant zones assigned with a foreground user. |
| OccupantZoneInfo myZone = new OccupantZoneInfo(/* zoneId= */ 0, |
| OCCUPANT_TYPE_DRIVER, SEAT_ROW_1_LEFT); |
| OccupantZoneInfo peerZone1 = new OccupantZoneInfo(/* zoneId= */ 1, |
| OCCUPANT_TYPE_FRONT_PASSENGER, SEAT_ROW_1_RIGHT); |
| OccupantZoneInfo peerZone2 = new OccupantZoneInfo(/* zoneId= */ 2, |
| OCCUPANT_TYPE_REAR_PASSENGER, SEAT_ROW_2_RIGHT); |
| List<OccupantZoneInfo> allZones = Arrays.asList(myZone, peerZone1, peerZone2); |
| when(mOccupantZoneService.getAllOccupantZones()).thenReturn(allZones); |
| |
| int peerUserId1 = mMyUserId + 10; |
| int peerUserId2 = mMyUserId + 11; |
| |
| // The caller zone is powered on and ready for connection. |
| // Peer zone 1 is powered on, and peer zone 2 is not powered on. |
| mockOccupantZonePowerOn(myZone); |
| mockOccupantZoneConnectionReady(myZone, mMyUserId); |
| mockOccupantZonePowerOn(peerZone1); |
| |
| // The discovering client is running in the foreground. Its peer client1 is installed but |
| // not running, and peer client2 is not installed. |
| mockAppRunningAsUser(mMyUserId, PID, myZone, IMPORTANCE_FOREGROUND); |
| mockAppInstalledAsUser(peerUserId1, peerZone1); |
| mockPerUserInfo(peerUserId2, peerZone2); |
| |
| mService.init(); |
| // The app state and occupant zone state are up-to-date before registering the callback. |
| mService.registerStateCallback(PACKAGE_NAME, mCallback); |
| |
| verify(mCallback).onAppStateChanged(peerZone1, |
| FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE); |
| verify(mCallback).onAppStateChanged(peerZone2, INITIAL_APP_STATE); |
| |
| verify(mCallback).onOccupantZoneStateChanged(peerZone1, FLAG_OCCUPANT_ZONE_POWER_ON); |
| verify(mCallback).onOccupantZoneStateChanged(peerZone2, INITIAL_OCCUPANT_ZONE_STATE); |
| } |
| |
| @Test |
| public void testAppStateChanged() throws RemoteException { |
| ProcessObserverCallback[] processObserver = new ProcessObserverCallback[1]; |
| doAnswer((invocation) -> { |
| Object[] args = invocation.getArguments(); |
| processObserver[0] = (ProcessObserverCallback) args[0]; |
| return null; |
| }).when(mSystemActivityMonitoringService).registerProcessObserverCallback(any()); |
| |
| // There are three occupant zones assigned with a foreground user. |
| OccupantZoneInfo myZone = new OccupantZoneInfo(/* zoneId= */ 0, |
| OCCUPANT_TYPE_DRIVER, SEAT_ROW_1_LEFT); |
| OccupantZoneInfo peerZone1 = new OccupantZoneInfo(/* zoneId= */ 1, |
| OCCUPANT_TYPE_FRONT_PASSENGER, SEAT_ROW_1_RIGHT); |
| OccupantZoneInfo peerZone2 = new OccupantZoneInfo(/* zoneId= */ 2, |
| OCCUPANT_TYPE_REAR_PASSENGER, SEAT_ROW_2_RIGHT); |
| List<OccupantZoneInfo> allZones = Arrays.asList(myZone, peerZone1, peerZone2); |
| when(mOccupantZoneService.getAllOccupantZones()).thenReturn(allZones); |
| |
| int peerUserId1 = mMyUserId + 10; |
| int peerUserId2 = mMyUserId + 11; |
| mockPerUserInfo(mMyUserId, myZone); |
| mockPerUserInfo(peerUserId1, peerZone1); |
| mockPerUserInfo(peerUserId2, peerZone2); |
| |
| mService.init(); |
| mService.registerStateCallback(PACKAGE_NAME, mCallback); |
| |
| verify(mCallback).onAppStateChanged(eq(peerZone1), anyInt()); |
| verify(mCallback).onAppStateChanged(eq(peerZone2), anyInt()); |
| |
| // Peer app1 is running in foreground. |
| int myPid = PID; |
| int peerPid1 = myPid + 1; |
| mockAppRunningAsUser(peerUserId1, peerPid1, peerZone1, IMPORTANCE_FOREGROUND); |
| processObserver[0].onForegroundActivitiesChanged(peerPid1, userIdToUid(peerUserId1), |
| /* foregroundActivities= */ true); |
| |
| verify(mCallback).onAppStateChanged(peerZone1, |
| FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE |
| | FLAG_CLIENT_RUNNING | FLAG_CLIENT_IN_FOREGROUND); |
| |
| // Peer app2 is running in background. |
| int peerPid2 = myPid + 2; |
| mockAppRunningAsUser(peerUserId2, peerPid2, peerZone2, IMPORTANCE_CACHED); |
| processObserver[0].onForegroundActivitiesChanged(peerPid2, userIdToUid(peerUserId2), |
| /* foregroundActivities= */ false); |
| |
| verify(mCallback).onAppStateChanged(peerZone2, |
| FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE |
| | FLAG_CLIENT_RUNNING); |
| |
| // Peer app1 is dead. |
| mockAppInstalledAsUser(peerUserId1, peerZone1); |
| processObserver[0].onProcessDied(peerPid1, userIdToUid(peerUserId1)); |
| |
| verify(mCallback).onAppStateChanged(peerZone1, |
| FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE); |
| } |
| |
| @Test |
| public void testProcessObserverCallbackInvokedBeforeOccupantZoneCallback() |
| throws RemoteException { |
| ProcessObserverCallback[] processObserver = new ProcessObserverCallback[1]; |
| doAnswer((invocation) -> { |
| Object[] args = invocation.getArguments(); |
| processObserver[0] = (ProcessObserverCallback) args[0]; |
| return null; |
| }).when(mSystemActivityMonitoringService).registerProcessObserverCallback(any()); |
| |
| // There is only one occupant zones assigned with a foreground user. |
| OccupantZoneInfo myZone = new OccupantZoneInfo(/* zoneId= */ 0, |
| OCCUPANT_TYPE_DRIVER, SEAT_ROW_1_LEFT); |
| List<OccupantZoneInfo> allZones = Arrays.asList(myZone); |
| when(mOccupantZoneService.getAllOccupantZones()).thenReturn(allZones); |
| mockPerUserInfo(mMyUserId, myZone); |
| |
| mService.init(); |
| mService.registerStateCallback(PACKAGE_NAME, mCallback); |
| |
| // Peer zone is assigned, but the ICarOccupantZoneCallback is not invoked yet, so |
| // mPerUserInfoMap has no entry for peerUserId. |
| int peerUserId = mMyUserId + 10; |
| OccupantZoneInfo peerZone = new OccupantZoneInfo(/* zoneId= */ 1, |
| OCCUPANT_TYPE_FRONT_PASSENGER, SEAT_ROW_1_RIGHT); |
| mockAppInstalledAsUser(peerUserId, peerZone); |
| mPerUserInfoMap.remove(peerUserId); |
| processObserver[0].onProcessDied(PID, userIdToUid(peerUserId)); |
| |
| verify(mCallback).onAppStateChanged(peerZone, |
| FLAG_CLIENT_INSTALLED | FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE); |
| } |
| |
| @Test |
| public void testOccupantZoneStateChanged() throws RemoteException { |
| UserLifecycleListener[] userLifecycleListeners = new UserLifecycleListener[1]; |
| doAnswer((invocation) -> { |
| Object[] args = invocation.getArguments(); |
| userLifecycleListeners[0] = (UserLifecycleListener) args[1]; |
| return null; |
| }).when(mUserService).addUserLifecycleListener(any(), any()); |
| |
| // There are three occupant zones assigned with a foreground user. |
| OccupantZoneInfo myZone = new OccupantZoneInfo(/* zoneId= */ 0, |
| OCCUPANT_TYPE_DRIVER, SEAT_ROW_1_LEFT); |
| OccupantZoneInfo peerZone1 = new OccupantZoneInfo(/* zoneId= */ 1, |
| OCCUPANT_TYPE_FRONT_PASSENGER, SEAT_ROW_1_RIGHT); |
| OccupantZoneInfo peerZone2 = new OccupantZoneInfo(/* zoneId= */ 2, |
| OCCUPANT_TYPE_REAR_PASSENGER, SEAT_ROW_2_RIGHT); |
| List<OccupantZoneInfo> allZones = Arrays.asList(myZone, peerZone1, peerZone2); |
| when(mOccupantZoneService.getAllOccupantZones()).thenReturn(allZones); |
| |
| int peerUserId1 = mMyUserId + 10; |
| int peerUserId2 = mMyUserId + 11; |
| mockPerUserInfo(mMyUserId, myZone); |
| mockPerUserInfo(peerUserId1, peerZone1); |
| mockPerUserInfo(peerUserId2, peerZone2); |
| |
| mService.init(); |
| mService.registerStateCallback(PACKAGE_NAME, mCallback); |
| |
| // The callback should be invoked when it is registered. |
| verify(mCallback).onOccupantZoneStateChanged(eq(peerZone1), anyInt()); |
| verify(mCallback).onOccupantZoneStateChanged(eq(peerZone2), anyInt()); |
| |
| // Peer zone 1 has a user change. It is powered on and is ready for connection. |
| int newPeerUserId1 = peerUserId1 + 10; |
| mockPerUserInfo(newPeerUserId1, peerZone1); |
| mockOccupantZonePowerOn(peerZone1); |
| mockOccupantZoneConnectionReady(peerZone1, newPeerUserId1); |
| UserLifecycleEvent event = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED, |
| /* from= */ newPeerUserId1, /* to= */ newPeerUserId1); |
| userLifecycleListeners[0].onEvent(event); |
| |
| verify(mCallback).onOccupantZoneStateChanged(peerZone1, |
| FLAG_OCCUPANT_ZONE_POWER_ON | FLAG_OCCUPANT_ZONE_CONNECTION_READY); |
| |
| // Peer zone 2 has a user change too, and it is powered on. |
| int newPeerUserId2 = peerUserId2 + 10; |
| mockPerUserInfo(newPeerUserId2, peerZone2); |
| mockOccupantZonePowerOn(peerZone2); |
| event = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED, |
| /* from= */ newPeerUserId2, /* to= */ newPeerUserId2); |
| userLifecycleListeners[0].onEvent(event); |
| |
| verify(mCallback).onOccupantZoneStateChanged(peerZone2, FLAG_OCCUPANT_ZONE_POWER_ON); |
| } |
| |
| @Test |
| public void testOccupantZonePowerStateChanged() throws RemoteException { |
| DisplayListener[] displayListener = new DisplayListener[1]; |
| doAnswer((invocation) -> { |
| Object[] args = invocation.getArguments(); |
| displayListener[0] = (DisplayListener) args[0]; |
| return null; |
| }).when(mDisplayManager).registerDisplayListener(any(), any(), anyLong()); |
| |
| mService.init(); |
| mOccupantZoneStateMap.put(mOccupantZone, INITIAL_OCCUPANT_ZONE_STATE); |
| mockOccupantZonePowerOn(mOccupantZone); |
| int displayId = 789; |
| when(mOccupantZoneService.getOccupantZoneForDisplayId(displayId)).thenReturn(mOccupantZone); |
| |
| displayListener[0].onDisplayChanged(displayId); |
| |
| assertThat(mOccupantZoneStateMap.get(mOccupantZone)).isEqualTo(FLAG_OCCUPANT_ZONE_POWER_ON); |
| } |
| |
| @Test |
| public void testUserAssigned() throws RemoteException { |
| UserLifecycleListener[] userLifecycleListeners = new UserLifecycleListener[1]; |
| doAnswer((invocation) -> { |
| Object[] args = invocation.getArguments(); |
| userLifecycleListeners[0] = (UserLifecycleListener) args[1]; |
| return null; |
| }).when(mUserService).addUserLifecycleListener(any(), any()); |
| |
| mService.init(); |
| mOccupantZoneStateMap.put(mOccupantZone, FLAG_OCCUPANT_ZONE_POWER_ON); |
| |
| mockPerUserInfo(USER_ID, mOccupantZone); |
| // Remove the item added by previous line, then check whether it can be added back |
| // after onEvent(). |
| mPerUserInfoMap.remove(USER_ID); |
| UserLifecycleEvent event = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED, |
| /* from= */ USER_ID, /* to= */ USER_ID); |
| userLifecycleListeners[0].onEvent(event); |
| |
| assertThat(mPerUserInfoMap.get(USER_ID).zone).isEqualTo(mOccupantZone); |
| } |
| |
| @Test |
| public void testUserUnassigned() throws RemoteException { |
| UserLifecycleListener[] userLifecycleListeners = new UserLifecycleListener[1]; |
| doAnswer((invocation) -> { |
| Object[] args = invocation.getArguments(); |
| userLifecycleListeners[0] = (UserLifecycleListener) args[1]; |
| return null; |
| }).when(mUserService).addUserLifecycleListener(any(), any()); |
| |
| mService.init(); |
| mOccupantZoneStateMap.put(mOccupantZone, FLAG_OCCUPANT_ZONE_POWER_ON); |
| |
| mockPerUserInfo(USER_ID, mOccupantZone); |
| assertThat(mPerUserInfoMap.size()).isEqualTo(1); |
| |
| when(mOccupantZoneService.getUserForOccupant(mOccupantZone.zoneId)) |
| .thenReturn(INVALID_USER_ID); |
| UserLifecycleEvent event = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED, |
| /* from= */ USER_ID, /* to= */ USER_ID); |
| userLifecycleListeners[0].onEvent(event); |
| |
| assertThat(mPerUserInfoMap.size()).isEqualTo(0); |
| } |
| |
| @Test |
| public void testUserSwitched() throws RemoteException { |
| UserLifecycleListener[] userLifecycleListeners = new UserLifecycleListener[1]; |
| doAnswer((invocation) -> { |
| Object[] args = invocation.getArguments(); |
| userLifecycleListeners[0] = (UserLifecycleListener) args[1]; |
| return null; |
| }).when(mUserService).addUserLifecycleListener(any(), any()); |
| |
| mService.init(); |
| mOccupantZoneStateMap.put(mOccupantZone, FLAG_OCCUPANT_ZONE_POWER_ON); |
| mockPerUserInfo(USER_ID, mOccupantZone); |
| |
| assertThat(mPerUserInfoMap.get(USER_ID).zone).isEqualTo(mOccupantZone); |
| |
| when(mOccupantZoneService.getUserForOccupant(mOccupantZone.zoneId)).thenReturn(USER_ID2); |
| mockPerUserInfo(USER_ID2, mOccupantZone); |
| // Remove the item added by previous line, then check whether it can be added back |
| // after onEvent(). |
| mPerUserInfoMap.remove(USER_ID2); |
| UserLifecycleEvent event = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_INVISIBLE, |
| /* from= */ USER_ID, /* to= */ USER_ID); |
| userLifecycleListeners[0].onEvent(event); |
| |
| assertThat(mPerUserInfoMap.get(USER_ID2).zone).isEqualTo(mOccupantZone); |
| } |
| |
| @Test |
| public void testUnregisterStateCallbackWithoutPermission_throwsException() { |
| when(mContext.checkCallingOrSelfPermission(Car.PERMISSION_MANAGE_REMOTE_DEVICE)) |
| .thenReturn(PackageManager.PERMISSION_DENIED); |
| |
| assertThrows(SecurityException.class, () -> mService.unregisterStateCallback(PACKAGE_NAME)); |
| } |
| |
| @Test |
| public void testUnregisterStateCallbackWithFakePackageName_throwsException() { |
| assertThrows(SecurityException.class, |
| () -> mService.unregisterStateCallback(FAKE_PACKAGE_NAME)); |
| } |
| |
| @Test |
| public void testUnregisterNonexistentStateCallback_throwsException() { |
| UserHandle userHandle = Binder.getCallingUserHandle(); |
| when(mOccupantZoneService.getOccupantZoneForUser(userHandle)).thenReturn(mOccupantZone); |
| |
| assertThrows(IllegalStateException.class, |
| () -> mService.unregisterStateCallback(PACKAGE_NAME)); |
| } |
| |
| @Test |
| public void testUnregisterStateCallbackWithoutOtherDiscoverers() { |
| // There is only one discoverer. |
| mockPerUserInfo(mMyUserId, mOccupantZone); |
| ClientId discoveringClient = new ClientId(mOccupantZone, mMyUserId, PACKAGE_NAME); |
| mService.registerStateCallback(PACKAGE_NAME, mCallback); |
| |
| assertThat(mCallbackMap.containsKey(discoveringClient)).isTrue(); |
| assertThat(mAppStateMap.containsKey(discoveringClient)).isTrue(); |
| |
| // Unregister the only discoverer. |
| mService.unregisterStateCallback(PACKAGE_NAME); |
| |
| assertThat(mCallbackMap.containsKey(discoveringClient)).isFalse(); |
| for (int i = 0; i < mAppStateMap.size(); i++) { |
| ClientId anotherDiscoveringClient = mAppStateMap.keyAt(i); |
| assertThat(anotherDiscoveringClient.packageName).isNotEqualTo(PACKAGE_NAME); |
| } |
| } |
| |
| @Test |
| public void testUnregisterStateCallbackWithOtherDiscoverers() { |
| // There are two discoverers. |
| mockPerUserInfo(mMyUserId, mOccupantZone); |
| OccupantZoneInfo zone2 = new OccupantZoneInfo(/* zoneId= */ 1, |
| OCCUPANT_TYPE_FRONT_PASSENGER, SEAT_ROW_1_RIGHT); |
| mockPerUserInfo(USER_ID, zone2); |
| ClientId discoveringClient = new ClientId(mOccupantZone, mMyUserId, PACKAGE_NAME); |
| ClientId discoveringClient2 = new ClientId(zone2, USER_ID, PACKAGE_NAME); |
| mService.registerStateCallback(PACKAGE_NAME, mCallback); |
| IStateCallback callback2 = mock(IStateCallback.class); |
| IBinder callbackBinder = mock(IBinder.class); |
| when(callback2.asBinder()).thenReturn(callbackBinder); |
| mCallbackMap.put(discoveringClient2, callback2); |
| |
| assertThat(mCallbackMap.containsKey(discoveringClient)).isTrue(); |
| assertThat(mCallbackMap.containsKey(discoveringClient2)).isTrue(); |
| assertThat(mAppStateMap.containsKey(discoveringClient)).isTrue(); |
| assertThat(mAppStateMap.containsKey(discoveringClient2)).isTrue(); |
| |
| // Unregister the first discoverer. |
| mService.unregisterStateCallback(PACKAGE_NAME); |
| |
| assertThat(mCallbackMap.containsKey(discoveringClient)).isFalse(); |
| assertThat(mCallbackMap.containsKey(discoveringClient2)).isTrue(); |
| assertThat(mAppStateMap.containsKey(discoveringClient)).isTrue(); |
| assertThat(mAppStateMap.containsKey(discoveringClient2)).isTrue(); |
| } |
| |
| private void mockPackageName() throws PackageManager.NameNotFoundException { |
| PackageManager pm = mock(PackageManager.class); |
| when(mContext.getPackageManager()).thenReturn(pm); |
| when(pm.getPackageUidAsUser(eq(PACKAGE_NAME), anyInt())).thenReturn(Binder.getCallingUid()); |
| } |
| |
| private void mockAppInstalledAsUser(int userId, OccupantZoneInfo occupantZone) { |
| PerUserInfo userInfo = mockPerUserInfo(userId, occupantZone); |
| PackageInfo packageInfo = mock(PackageInfo.class); |
| try { |
| when(userInfo.pm.getPackageInfo(eq(PACKAGE_NAME), any())).thenReturn(packageInfo); |
| } catch (PackageManager.NameNotFoundException e) { |
| throw new RuntimeException(e); |
| } |
| String[] packageNames = {PACKAGE_NAME}; |
| int uid = userIdToUid(userId); |
| when(userInfo.pm.getPackagesForUid(uid)).thenReturn(packageNames); |
| } |
| |
| private void mockAppRunningAsUser(int userId, int pid, OccupantZoneInfo occupantZone, |
| int importance) { |
| mockAppInstalledAsUser(userId, occupantZone); |
| RunningAppProcessInfo process = new RunningAppProcessInfo(); |
| process.processName = PACKAGE_NAME; |
| process.uid = userIdToUid(userId); |
| process.pid = pid; |
| process.importance = importance; |
| List<RunningAppProcessInfo> processList = Arrays.asList(process); |
| when(mActivityManager.getRunningAppProcesses()).thenReturn(processList); |
| } |
| |
| private void mockOccupantZonePowerOn(OccupantZoneInfo occupantZone) { |
| when(mOccupantZoneService.areDisplaysOnForOccupantZone(occupantZone.zoneId)) |
| .thenReturn(true); |
| } |
| |
| private void mockOccupantZoneConnectionReady(OccupantZoneInfo occupantZone, int userId) { |
| when(mOccupantZoneService.getUserForOccupant(occupantZone.zoneId)).thenReturn(userId); |
| UserHandle userHandle = UserHandle.of(userId); |
| when(mUserManager.isUserRunning(userHandle)).thenReturn(true); |
| when(mUserManager.isUserUnlocked(userHandle)).thenReturn(true); |
| Set<UserHandle> visibleUsers = new ArraySet<>(); |
| visibleUsers.add(userHandle); |
| when(mUserManager.getVisibleUsers()).thenReturn(visibleUsers); |
| } |
| |
| private PerUserInfo mockPerUserInfo(int userId, OccupantZoneInfo occupantZone) { |
| when(mOccupantZoneService.getUserForOccupant(occupantZone.zoneId)).thenReturn(userId); |
| when(mOccupantZoneService.getOccupantZoneForUser(UserHandle.of(userId))) |
| .thenReturn(occupantZone); |
| |
| Context userContext = mock(Context.class); |
| mockContextCreateContextAsUser(mContext, userContext, userId); |
| PackageManager pm = mock(PackageManager.class); |
| when(userContext.getPackageManager()).thenReturn(pm); |
| BroadcastReceiver receiver = mock(BroadcastReceiver.class); |
| PerUserInfo userInfo = new PerUserInfo(occupantZone, userContext, pm, receiver); |
| mPerUserInfoMap.put(userId, userInfo); |
| return userInfo; |
| } |
| |
| private static int userIdToUid(int userId) { |
| return userId * PER_USER_RANGE; |
| } |
| } |