blob: 3be64a59ca503339718d3a96e2123b21d91d2324 [file] [log] [blame]
/*
* Copyright (C) 2019 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.pm;
import static android.car.test.mocks.CarArgumentMatchers.isUserHandle;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.when;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.car.test.mocks.AbstractExtendedMockitoTestCase;
import android.car.testapi.BlockingUserLifecycleListener;
import android.car.user.CarUserManager;
import android.car.user.CarUserManager.UserLifecycleEventType;
import android.car.userlib.CarUserManagerHelper;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
import com.android.car.CarLocalServices;
import com.android.car.hal.UserHalService;
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public final class VendorServiceControllerTest extends AbstractExtendedMockitoTestCase {
private static final String TAG = VendorServiceControllerTest.class.getSimpleName();
// TODO(b/152069895): decrease value once refactored. In fact, it should not even use
// runWithScissors(), but only rely on CountdownLatches
private static final long DEFAULT_TIMEOUT_MS = 5_000;
private static final int FG_USER_ID = 13;
private static final String SERVICE_BIND_ALL_USERS_ASAP = "com.android.car/.AllUsersService";
private static final String SERVICE_BIND_FG_USER_UNLOCKED = "com.android.car/.ForegroundUsers";
private static final String SERVICE_START_SYSTEM_UNLOCKED = "com.android.car/.SystemUser";
private static final String[] FAKE_SERVICES = new String[] {
SERVICE_BIND_ALL_USERS_ASAP + "#bind=bind,user=all,trigger=asap",
SERVICE_BIND_FG_USER_UNLOCKED + "#bind=bind,user=foreground,trigger=userUnlocked",
SERVICE_START_SYSTEM_UNLOCKED + "#bind=start,user=system,trigger=userUnlocked"
};
@Mock
private Resources mResources;
@Mock
private UserManager mUserManager;
@Mock
private UserHalService mUserHal;
private ServiceLauncherContext mContext;
private CarUserManagerHelper mUserManagerHelper;
private CarUserService mCarUserService;
private VendorServiceController mController;
@Override
protected void onSessionBuilder(CustomMockitoSessionBuilder session) {
session.spyStatic(ActivityManager.class);
}
@Before
public void setUp() {
mContext = new ServiceLauncherContext(ApplicationProvider.getApplicationContext());
mUserManagerHelper = Mockito.spy(new CarUserManagerHelper(mContext));
mCarUserService = new CarUserService(mContext, mUserHal, mUserManagerHelper, mUserManager,
ActivityManager.getService(), 2 /* max running users */);
CarLocalServices.addService(CarUserService.class, mCarUserService);
mController = new VendorServiceController(mContext, Looper.getMainLooper());
UserInfo persistentFgUser = new UserInfo(FG_USER_ID, "persistent user", 0);
when(mUserManager.getUserInfo(FG_USER_ID)).thenReturn(persistentFgUser);
when(mResources.getStringArray(com.android.car.R.array.config_earlyStartupServices))
.thenReturn(FAKE_SERVICES);
}
@After
public void tearDown() {
CarLocalServices.removeServiceForTest(CarUserService.class);
}
@Test
public void init_nothingConfigured() {
when(mResources.getStringArray(com.android.car.R.array.config_earlyStartupServices))
.thenReturn(new String[0]);
mController.init();
mContext.verifyNoMoreServiceLaunches();
}
@Test
public void init_systemUser() throws Exception {
mContext.expectServices(SERVICE_BIND_ALL_USERS_ASAP);
mockGetCurrentUser(UserHandle.USER_SYSTEM);
mController.init();
mContext.assertBoundService(SERVICE_BIND_ALL_USERS_ASAP);
mContext.verifyNoMoreServiceLaunches();
}
@Test
public void systemUserUnlocked() throws Exception {
mController.init();
mContext.reset();
// TODO(b/152069895): must refactor this test because
// SERVICE_BIND_ALL_USERS_ASAP is bound twice (users 0 and 10)
mContext.expectServices(SERVICE_START_SYSTEM_UNLOCKED);
// Unlock system user
mockUserUnlock(UserHandle.USER_SYSTEM);
sendUserLifecycleEvent(CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKING,
UserHandle.USER_SYSTEM);
mContext.assertStartedService(SERVICE_START_SYSTEM_UNLOCKED);
mContext.verifyNoMoreServiceLaunches();
}
@Test
public void fgUserUnlocked() throws Exception {
mockGetCurrentUser(UserHandle.USER_SYSTEM);
mController.init();
mContext.reset();
mContext.expectServices(SERVICE_BIND_ALL_USERS_ASAP, SERVICE_BIND_FG_USER_UNLOCKED);
// Switch user to foreground
mockGetCurrentUser(FG_USER_ID);
// TODO(b/155918094): Update this test,
UserInfo nullUser = new UserInfo(UserHandle.USER_NULL, "null user", 0);
when(mUserManager.getUserInfo(UserHandle.USER_NULL)).thenReturn(nullUser);
sendUserLifecycleEvent(CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING, FG_USER_ID);
// Expect only services with ASAP trigger to be started
mContext.assertBoundService(SERVICE_BIND_ALL_USERS_ASAP);
mContext.verifyNoMoreServiceLaunches();
// Unlock foreground user
mockUserUnlock(FG_USER_ID);
sendUserLifecycleEvent(CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKING, FG_USER_ID);
mContext.assertBoundService(SERVICE_BIND_FG_USER_UNLOCKED);
mContext.verifyNoMoreServiceLaunches();
}
private static void runOnMainThreadAndWaitForIdle(Runnable r) {
Handler.getMain().runWithScissors(r, DEFAULT_TIMEOUT_MS);
// Run empty runnable to make sure that all posted handlers are done.
Handler.getMain().runWithScissors(() -> { }, DEFAULT_TIMEOUT_MS);
}
private void mockUserUnlock(@UserIdInt int userId) {
when(mUserManager.isUserUnlockingOrUnlocked(isUserHandle(userId))).thenReturn(true);
when(mUserManager.isUserUnlockingOrUnlocked(userId)).thenReturn(true);
}
private static void assertHasService(List<Intent> intents, String service, String action) {
assertWithMessage("Service %s not %s yet", service, action).that(intents)
.hasSize(1);
assertWithMessage("Wrong component %s", action).that(intents.get(0).getComponent())
.isEqualTo(ComponentName.unflattenFromString(service));
intents.clear();
}
private void sendUserLifecycleEvent(@UserLifecycleEventType int eventType,
@UserIdInt int userId) throws InterruptedException {
// Adding a blocking listener to ensure CarUserService event notification is completed
// before proceeding with test execution.
BlockingUserLifecycleListener blockingListener = BlockingUserLifecycleListener
.newDefaultListener();
mCarUserService.addUserLifecycleListener(blockingListener);
runOnMainThreadAndWaitForIdle(() -> mCarUserService.onUserLifecycleEvent(eventType,
/* timestampMs= */ 0, /* fromUserId= */ UserHandle.USER_NULL, userId));
blockingListener.waitForEvent();
}
/** Overrides framework behavior to succeed on binding/starting processes. */
public final class ServiceLauncherContext extends ContextWrapper {
private final Object mLock = new Object();
@GuardedBy("mLock")
private List<Intent> mBoundIntents = new ArrayList<>();
@GuardedBy("mLock")
private List<Intent> mStartedServicesIntents = new ArrayList<>();
private final Map<String, CountDownLatch> mBoundLatches = new HashMap<>();
private final Map<String, CountDownLatch> mStartedLatches = new HashMap<>();
ServiceLauncherContext(Context base) {
super(base);
}
@Override
public ComponentName startServiceAsUser(Intent service, UserHandle user) {
synchronized (mLock) {
mStartedServicesIntents.add(service);
}
countdown(mStartedLatches, service, "started");
return service.getComponent();
}
@Override
public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags,
Handler handler, UserHandle user) {
synchronized (mLock) {
mBoundIntents.add(service);
Log.v(TAG, "Added service (" + service + ") to bound intents");
}
conn.onServiceConnected(service.getComponent(), null);
countdown(mBoundLatches, service, "bound");
return true;
}
@Override
public boolean bindServiceAsUser(Intent service, ServiceConnection conn,
int flags, UserHandle user) {
return bindServiceAsUser(service, conn, flags, null, user);
}
@Override
public Resources getResources() {
return mResources;
}
private void expectServices(String... services) {
for (String service : services) {
Log.v(TAG, "expecting service " + service);
mBoundLatches.put(service, new CountDownLatch(1));
mStartedLatches.put(service, new CountDownLatch(1));
}
}
private void await(Map<String, CountDownLatch> latches, String service, String method)
throws InterruptedException {
CountDownLatch latch = latches.get(service);
Preconditions.checkArgument(latch != null,
"no latch set for %s - did you call expectBoundServices()?", service);
Log.d(TAG, "waiting " + DEFAULT_TIMEOUT_MS + "ms for " + method);
if (!latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
String errorMessage = method + " not called in " + DEFAULT_TIMEOUT_MS + "ms";
Log.e(TAG, errorMessage);
fail(errorMessage);
}
Log.v(TAG, "latch.await for service (" + service + ") and method ("
+ method + ") called fine");
}
private void countdown(Map<String, CountDownLatch> latches, Intent service, String action) {
String serviceName = service.getComponent().flattenToShortString();
CountDownLatch latch = latches.get(serviceName);
if (latch == null) {
Log.e(TAG, "unexpected service (" + serviceName + ") " + action + ". Expected only "
+ mBoundLatches.keySet());
} else {
latch.countDown();
Log.v(TAG, "latch.countDown for service (" + service + ") and action ("
+ action + ") called fine");
}
}
void assertBoundService(String service) throws InterruptedException {
await(mBoundLatches, service, "bind()");
synchronized (mLock) {
assertHasService(mBoundIntents, service, "bound");
}
}
void assertStartedService(String service) throws InterruptedException {
await(mStartedLatches, service, "start()");
synchronized (mLock) {
assertHasService(mStartedServicesIntents, service, "started");
}
}
void verifyNoMoreServiceLaunches() {
synchronized (mLock) {
assertThat(mStartedServicesIntents).isEmpty();
assertThat(mBoundIntents).isEmpty();
}
}
void reset() {
synchronized (mLock) {
mStartedServicesIntents.clear();
mBoundIntents.clear();
}
}
@Override
public Object getSystemService(String name) {
if (Context.USER_SERVICE.equals(name)) {
return mUserManager;
}
return super.getSystemService(name);
}
}
}