summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2020-09-08 16:55:24 -0700
committerXin Li <delphij@google.com>2020-09-08 16:55:24 -0700
commitb68ac114f832c9a5691fc98fd97cf6307d9d3e30 (patch)
treee67c2f26c6603f0ee5e8762a5a624a9ab089e94c
parent57450c2252059c0a61962ac8ba5959e0f3bb4d98 (diff)
parentf48ff0ad05662c6cb44b6f3c8d48da1f21997f19 (diff)
downloadplatform_frameworks_opt_car_services-master.tar.gz
platform_frameworks_opt_car_services-master.tar.bz2
platform_frameworks_opt_car_services-master.zip
Merge Android RHEADmaster
Bug: 168057903 Merged-In: I11c4aae11395a7bf8f92574af746f7af3687a216 Change-Id: I33d97b2c5c48451d34a158ecb29d5db43fc0eefa
-rw-r--r--Android.bp9
-rw-r--r--OWNERS4
-rw-r--r--PREUPLOAD.cfg6
-rw-r--r--src/com/android/internal/car/CarServiceHelperService.java1165
-rw-r--r--src/com/android/internal/car/ExternalConstants.java62
-rw-r--r--src/com/android/internal/car/ICarServiceHelper.aidl25
-rw-r--r--src/com/android/server/wm/CarLaunchParamsModifier.java393
-rw-r--r--tests/Android.mk18
-rw-r--r--tests/AndroidManifest.xml3
-rw-r--r--tests/src/com/android/internal/car/CarHelperServiceTest.java1213
-rw-r--r--tests/src/com/android/server/wm/CarLaunchParamsModifierTest.java495
11 files changed, 3113 insertions, 280 deletions
diff --git a/Android.bp b/Android.bp
index 0f805d2..eac88c9 100644
--- a/Android.bp
+++ b/Android.bp
@@ -2,13 +2,18 @@ java_library {
name: "car-frameworks-service",
installable: true,
libs: ["services"],
- //LOCAL_PACKAGE_NAME := CarFrameworkService
required: ["libcar-framework-service-jni"],
srcs: [
"src/**/*.java",
"src/com/android/internal/car/ICarServiceHelper.aidl",
],
- static_libs: ["android.car.userlib"],
+ static_libs: [
+ "android.hardware.automotive.vehicle-V2.0-java",
+ "android.car.internal.event-log-tags",
+ "android.car.userlib",
+ "android.car.watchdoglib",
+ "carwatchdog_aidl_interface-java",
+ ],
}
cc_library_shared {
diff --git a/OWNERS b/OWNERS
index 857edb5..6010182 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,4 +1,6 @@
keunyoung@google.com
-randolphs@google.com
sgurun@google.com
yizheng@google.com
+gurunagarajan@google.com
+felipeal@google.com
+ycheo@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..2811ea9
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,6 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+
+[Builtin Hooks]
+commit_msg_changeid_field = true
+commit_msg_test_field = true
diff --git a/src/com/android/internal/car/CarServiceHelperService.java b/src/com/android/internal/car/CarServiceHelperService.java
index 5772e19..cd3c4f9 100644
--- a/src/com/android/internal/car/CarServiceHelperService.java
+++ b/src/com/android/internal/car/CarServiceHelperService.java
@@ -16,69 +16,120 @@
package com.android.internal.car;
+import static android.car.userlib.UserHelper.safeName;
+
+import static com.android.internal.car.ExternalConstants.CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_STARTING;
+import static com.android.internal.car.ExternalConstants.CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPED;
+import static com.android.internal.car.ExternalConstants.CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPING;
+import static com.android.internal.car.ExternalConstants.CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
+import static com.android.internal.car.ExternalConstants.CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
+import static com.android.internal.car.ExternalConstants.CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKING;
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.IActivityManager;
+import android.annotation.UserIdInt;
import android.app.admin.DevicePolicyManager;
+import android.automotive.watchdog.ICarWatchdogMonitor;
+import android.automotive.watchdog.PowerCycle;
+import android.automotive.watchdog.StateType;
import android.car.userlib.CarUserManagerHelper;
+import android.car.userlib.CommonConstants.CarUserServiceConstants;
+import android.car.userlib.HalCallback;
+import android.car.userlib.InitialUserSetter;
+import android.car.userlib.InitialUserSetter.InitialUserInfoType;
+import android.car.userlib.UserHalHelper;
+import android.car.userlib.UserHelper;
+import android.car.watchdoglib.CarWatchdogDaemonHelper;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.UserInfo;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoRequestType;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponseAction;
import android.hidl.manager.V1_0.IServiceManager;
import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
import android.os.Parcel;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.sysprop.CarProperties;
+import android.util.EventLog;
import android.util.Slog;
-import android.util.TimingsTraceLog;
import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.car.ExternalConstants.ICarConstants;
+import com.android.internal.os.IResultReceiver;
import com.android.server.SystemService;
import com.android.server.Watchdog;
import com.android.server.am.ActivityManagerService;
+import com.android.server.utils.TimingsTraceAndSlog;
+import com.android.server.wm.CarLaunchParamsModifier;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
/**
- * System service side companion service for CarService.
- * Starts car service and provide necessary API for CarService. Only for car product.
+ * System service side companion service for CarService. Starts car service and provide necessary
+ * API for CarService. Only for car product.
*/
public class CarServiceHelperService extends SystemService {
// Place holder for user name of the first user created.
private static final String TAG = "CarServiceHelper";
+
+ // TODO(b/154033860): STOPSHIP if they're still true
private static final boolean DBG = true;
- private static final String CAR_SERVICE_INTERFACE = "android.car.ICar";
- // These numbers should match with binder call order of
- // packages/services/Car/car-lib/src/android/car/ICar.aidl
- private static final int ICAR_CALL_SET_CAR_SERVICE_HELPER = 0;
- private static final int ICAR_CALL_SET_USER_UNLOCK_STATUS = 1;
- private static final int ICAR_CALL_SET_SWITCH_USER = 2;
+ private static final boolean VERBOSE = true;
private static final String PROP_RESTART_RUNTIME = "ro.car.recovery.restart_runtime.enabled";
private static final List<String> CAR_HAL_INTERFACES_OF_INTEREST = Arrays.asList(
"android.hardware.automotive.vehicle@2.0::IVehicle",
- "android.hardware.automotive.audiocontrol@1.0::IAudioControl"
+ "android.hardware.automotive.audiocontrol@1.0::IAudioControl",
+ "android.hardware.automotive.audiocontrol@2.0::IAudioControl"
);
+ // Message ID representing HAL timeout handling.
+ private static final int WHAT_HAL_TIMEOUT = 1;
+ // Message ID representing post-processing of process dumping.
+ private static final int WHAT_POST_PROCESS_DUMPING = 2;
+ // Message ID representing process killing.
+ private static final int WHAT_PROCESS_KILL = 3;
+
+ private static final long LIFECYCLE_TIMESTAMP_IGNORE = 0;
+
+ // Typically there are ~2-5 ops while system and non-system users are starting.
+ private final int NUMBER_PENDING_OPERATIONS = 5;
+
+ @UserIdInt
@GuardedBy("mLock")
private int mLastSwitchedUser = UserHandle.USER_NULL;
@@ -90,12 +141,60 @@ public class CarServiceHelperService extends SystemService {
@GuardedBy("mLock")
private boolean mSystemBootCompleted;
+ // Key: user id, value: lifecycle
@GuardedBy("mLock")
- private final HashMap<Integer, Boolean> mUserUnlockedStatus = new HashMap<>();
+ private final SparseIntArray mLastUserLifecycle = new SparseIntArray();
+
private final CarUserManagerHelper mCarUserManagerHelper;
+ private final InitialUserSetter mInitialUserSetter;
private final UserManager mUserManager;
- private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
+ private final CarLaunchParamsModifier mCarLaunchParamsModifier;
+
+ private final boolean mHalEnabled;
+ private final int mHalTimeoutMs;
+
+ // Handler is currently only used for handleHalTimedout(), which is removed once received.
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final ProcessTerminator mProcessTerminator = new ProcessTerminator();
+
+ @GuardedBy("mLock")
+ private boolean mInitialized;
+
+ /**
+ * End-to-end time (from process start) for unlocking the first non-system user.
+ */
+ private long mFirstUnlockedUserDuration;
+
+ /**
+ * Used to calculate how long it took to get the {@code INITIAL_USER_INFO} response from HAL:
+ *
+ * <ul>
+ * <li>{@code 0}: HAL not called yet
+ * <li>{@code <0}: stores the time HAL was called (multiplied by -1)
+ * <li>{@code >0}: contains the duration (in ms)
+ * </ul>
+ */
+ private int mHalResponseTime;
+
+ // TODO(b/150413515): rather than store Runnables, it would be more efficient to store some
+ // parcelables representing the operation, then pass them to setCarServiceHelper
+ @GuardedBy("mLock")
+ private ArrayList<Runnable> mPendingOperations;
+
+ @GuardedBy("mLock")
+ private boolean mCarServiceHasCrashed;
+
+ private final CarWatchdogDaemonHelper mCarWatchdogDaemonHelper;
+ private final ICarWatchdogMonitorImpl mCarWatchdogMonitor = new ICarWatchdogMonitorImpl(this);
+ private final CarWatchdogDaemonHelper.OnConnectionChangeListener mConnectionListener =
+ (connected) -> {
+ if (connected) {
+ registerMonitorToWatchdogDaemon();
+ }
+ };
+
+ private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
if (DBG) {
@@ -110,42 +209,108 @@ public class CarServiceHelperService extends SystemService {
}
};
+ private final BroadcastReceiver mShutdownEventReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Skip immediately if intent is not relevant to device shutdown.
+ // FLAG_RECEIVER_FOREGROUND is checked to ignore the intent from UserController when
+ // a user is stopped.
+ if ((!intent.getAction().equals(Intent.ACTION_REBOOT)
+ && !intent.getAction().equals(Intent.ACTION_SHUTDOWN))
+ || (intent.getFlags() & Intent.FLAG_RECEIVER_FOREGROUND) == 0) {
+ return;
+ }
+ int powerCycle = PowerCycle.POWER_CYCLE_SUSPEND;
+ try {
+ mCarWatchdogDaemonHelper.notifySystemStateChange(StateType.POWER_CYCLE,
+ powerCycle, /* arg2= */ 0);
+ if (DBG) {
+ Slog.d(TAG, "Notified car watchdog daemon a power cycle(" + powerCycle + ")");
+ }
+ } catch (RemoteException | RuntimeException e) {
+ Slog.w(TAG, "Notifying system state change failed: " + e);
+ }
+ }
+ };
+
public CarServiceHelperService(Context context) {
- this(context, new CarUserManagerHelper(context));
+ this(context,
+ new CarUserManagerHelper(context),
+ /* initialUserSetter= */ null,
+ UserManager.get(context),
+ new CarLaunchParamsModifier(context),
+ new CarWatchdogDaemonHelper(TAG),
+ CarProperties.user_hal_enabled().orElse(false),
+ CarProperties.user_hal_timeout().orElse(5_000)
+ );
}
@VisibleForTesting
- CarServiceHelperService(Context context, CarUserManagerHelper carUserManagerHelper) {
+ CarServiceHelperService(
+ Context context,
+ CarUserManagerHelper userManagerHelper,
+ InitialUserSetter initialUserSetter,
+ UserManager userManager,
+ CarLaunchParamsModifier carLaunchParamsModifier,
+ CarWatchdogDaemonHelper carWatchdogDaemonHelper,
+ boolean halEnabled,
+ int halTimeoutMs) {
super(context);
mContext = context;
- mCarUserManagerHelper = carUserManagerHelper;
- mUserManager = UserManager.get(context);
+ mCarUserManagerHelper = userManagerHelper;
+ mUserManager = userManager;
+ mCarLaunchParamsModifier = carLaunchParamsModifier;
+ mCarWatchdogDaemonHelper = carWatchdogDaemonHelper;
+ boolean halValidUserHalSettings = false;
+ if (halEnabled) {
+ if (halTimeoutMs > 0) {
+ Slog.i(TAG, "User HAL enabled with timeout of " + halTimeoutMs + "ms");
+ halValidUserHalSettings = true;
+ } else {
+ Slog.w(TAG, "Not using User HAL due to invalid value on userHalTimeoutMs config: "
+ + halTimeoutMs);
+ }
+ }
+ if (halValidUserHalSettings) {
+ mHalEnabled = true;
+ mHalTimeoutMs = halTimeoutMs;
+ } else {
+ mHalEnabled = false;
+ mHalTimeoutMs = -1;
+ Slog.i(TAG, "Not using User HAL");
+ }
+ if (initialUserSetter == null) {
+ // Called from main constructor, which cannot pass a lambda referencing itself
+ mInitialUserSetter = new InitialUserSetter(context, (u) -> setInitialUser(u));
+ } else {
+ mInitialUserSetter = initialUserSetter;
+ }
}
@Override
public void onBootPhase(int phase) {
- if (DBG) {
- Slog.d(TAG, "onBootPhase:" + phase);
- }
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_BOOT_PHASE, phase);
+ if (DBG) Slog.d(TAG, "onBootPhase:" + phase);
+
+ TimingsTraceAndSlog t = newTimingsTraceAndSlog();
if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
- checkForCarServiceConnection();
- // TODO(b/126199560) Consider doing this earlier in onStart().
- // Other than onStart, PHASE_THIRD_PARTY_APPS_CAN_START is the earliest timing.
- setupAndStartUsers();
- checkForCarServiceConnection();
+ t.traceBegin("onBootPhase.3pApps");
+ mCarLaunchParamsModifier.init();
+ checkForCarServiceConnection(t);
+ setupAndStartUsers(t);
+ checkForCarServiceConnection(t);
+ t.traceEnd();
} else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
- TimingsTraceLog t = new TimingsTraceLog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
t.traceBegin("onBootPhase.completed");
managePreCreatedUsers();
- boolean shouldNotify = false;
synchronized (mLock) {
mSystemBootCompleted = true;
- if (mCarService != null) {
- shouldNotify = true;
- }
}
- if (shouldNotify) {
- notifyAllUnlockedUsers();
+ try {
+ mCarWatchdogDaemonHelper.notifySystemStateChange(
+ StateType.BOOT_PHASE, phase, /* arg2= */ 0);
+ } catch (RemoteException | RuntimeException e) {
+ Slog.w(TAG, "Failed to notify boot phase change: " + e);
}
t.traceEnd();
}
@@ -153,55 +318,140 @@ public class CarServiceHelperService extends SystemService {
@Override
public void onStart() {
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_START, mHalEnabled ? 1 : 0);
+
+ IntentFilter filter = new IntentFilter(Intent.ACTION_REBOOT);
+ filter.addAction(Intent.ACTION_SHUTDOWN);
+ mContext.registerReceiverForAllUsers(mShutdownEventReceiver, filter, null, null);
+ mCarWatchdogDaemonHelper.addOnConnectionChangeListener(mConnectionListener);
+ mCarWatchdogDaemonHelper.connect();
Intent intent = new Intent();
intent.setPackage("com.android.car");
- intent.setAction(CAR_SERVICE_INTERFACE);
- if (!getContext().bindServiceAsUser(intent, mCarServiceConnection, Context.BIND_AUTO_CREATE,
+ intent.setAction(ICarConstants.CAR_SERVICE_INTERFACE);
+ if (!mContext.bindServiceAsUser(intent, mCarServiceConnection, Context.BIND_AUTO_CREATE,
UserHandle.SYSTEM)) {
Slog.wtf(TAG, "cannot start car service");
}
- System.loadLibrary("car-framework-service-jni");
+ loadNativeLibrary();
}
+ @Override
+ public void onUserUnlocking(@NonNull TargetUser user) {
+ if (isPreCreated(user, USER_LIFECYCLE_EVENT_TYPE_UNLOCKING)) return;
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_USER_UNLOCKING, user.getUserIdentifier());
+ if (DBG) Slog.d(TAG, "onUserUnlocking(" + user + ")");
+
+ sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKING, user);
+ }
@Override
- public void onUnlockUser(int userHandle) {
- handleUserLockStatusChange(userHandle, true);
- if (DBG) {
- Slog.d(TAG, "User" + userHandle + " unlocked");
+ public void onUserUnlocked(@NonNull TargetUser user) {
+ if (isPreCreated(user, USER_LIFECYCLE_EVENT_TYPE_UNLOCKED)) return;
+ int userId = user.getUserIdentifier();
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_USER_UNLOCKED, userId);
+ if (DBG) Slog.d(TAG, "onUserUnlocked(" + user + ")");
+
+ if (mFirstUnlockedUserDuration == 0 && !UserHelper.isHeadlessSystemUser(userId)) {
+ mFirstUnlockedUserDuration = SystemClock.elapsedRealtime()
+ - Process.getStartElapsedRealtime();
+ Slog.i(TAG, "Time to unlock 1st user(" + user + "): "
+ + TimeUtils.formatDuration(mFirstUnlockedUserDuration));
+ synchronized (mLock) {
+ mLastUserLifecycle.put(userId, USER_LIFECYCLE_EVENT_TYPE_UNLOCKED);
+ if (mCarService == null) {
+ if (DBG) Slog.d(TAG, "Queuing first user unlock for user " + user);
+ queueOperationLocked(() -> sendFirstUserUnlocked(user));
+ return;
+ }
+ }
+ sendFirstUserUnlocked(user);
+ return;
}
+ sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED, user);
+ }
+
+ @Override
+ public void onUserStarting(@NonNull TargetUser user) {
+ if (isPreCreated(user, USER_LIFECYCLE_EVENT_TYPE_STARTING)) return;
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_USER_STARTING, user.getUserIdentifier());
+ if (DBG) Slog.d(TAG, "onUserStarting(" + user + ")");
+
+ sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING, user);
}
@Override
- public void onStopUser(int userHandle) {
- handleUserLockStatusChange(userHandle, false);
+ public void onUserStopping(@NonNull TargetUser user) {
+ if (isPreCreated(user, USER_LIFECYCLE_EVENT_TYPE_STOPPING)) return;
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_USER_STOPPING, user.getUserIdentifier());
+ if (DBG) Slog.d(TAG, "onUserStopping(" + user + ")");
+
+ sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPING, user);
+ int userId = user.getUserIdentifier();
+ mCarLaunchParamsModifier.handleUserStopped(userId);
}
@Override
- public void onCleanupUser(int userHandle) {
- handleUserLockStatusChange(userHandle, false);
+ public void onUserStopped(@NonNull TargetUser user) {
+ if (isPreCreated(user, USER_LIFECYCLE_EVENT_TYPE_STOPPED)) return;
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_USER_STOPPED, user.getUserIdentifier());
+ if (DBG) Slog.d(TAG, "onUserStopped(" + user + ")");
+
+ sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPED, user);
}
@Override
- public void onSwitchUser(int userHandle) {
- synchronized (mLock) {
- mLastSwitchedUser = userHandle;
- if (mCarService == null) {
- return; // The event will be delivered upon CarService connection.
- }
+ public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
+ if (isPreCreated(to, USER_LIFECYCLE_EVENT_TYPE_SWITCHING)) return;
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_USER_SWITCHING,
+ from == null ? UserHandle.USER_NULL : from.getUserIdentifier(),
+ to.getUserIdentifier());
+ if (DBG) Slog.d(TAG, "onUserSwitching(" + from + ">>" + to + ")");
+
+ sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING, from, to);
+ int userId = to.getUserIdentifier();
+ mCarLaunchParamsModifier.handleCurrentUserSwitching(userId);
+ }
+
+ @VisibleForTesting
+ void loadNativeLibrary() {
+ System.loadLibrary("car-framework-service-jni");
+ }
+
+ private boolean isPreCreated(@NonNull TargetUser user, int eventType) {
+ UserInfo userInfo = user.getUserInfo();
+ if (userInfo == null) {
+ Slog.wtf(TAG, "no UserInfo on " + user + " on eventType " + eventType);
+ return false;
+ }
+ if (!userInfo.preCreated) return false;
+
+ if (DBG) {
+ Slog.d(TAG, "Ignoring event of type " + eventType + " for pre-created user "
+ + userInfo.toFullString());
+ }
+ return true;
+ }
+
+ /**
+ * Queues a binder operation so it's called when the service is connected.
+ */
+ private void queueOperationLocked(@NonNull Runnable operation) {
+ if (mPendingOperations == null) {
+ mPendingOperations = new ArrayList<>(NUMBER_PENDING_OPERATIONS);
}
- sendSwitchUserBindercall(userHandle);
+ mPendingOperations.add(operation);
}
// Sometimes car service onConnected call is delayed a lot. car service binder can be
// found from ServiceManager directly. So do some polling during boot-up to connect to
// car service ASAP.
- private void checkForCarServiceConnection() {
+ private void checkForCarServiceConnection(@NonNull TimingsTraceAndSlog t) {
synchronized (mLock) {
if (mCarService != null) {
return;
}
}
+ t.traceBegin("checkForCarServiceConnection");
IBinder iBinder = ServiceManager.checkService("car_service");
if (iBinder != null) {
if (DBG) {
@@ -209,50 +459,121 @@ public class CarServiceHelperService extends SystemService {
}
handleCarServiceConnection(iBinder);
}
+ t.traceEnd();
+ }
+
+ @VisibleForTesting
+ int getHalResponseTime() {
+ return mHalResponseTime;
+ }
+
+ @VisibleForTesting
+ void setInitialHalResponseTime() {
+ mHalResponseTime = -((int) SystemClock.uptimeMillis());
+ }
+
+ @VisibleForTesting
+ void setFinalHalResponseTime() {
+ mHalResponseTime += (int) SystemClock.uptimeMillis();
}
- private void handleCarServiceConnection(IBinder iBinder) {
+ @VisibleForTesting
+ void handleCarServiceConnection(IBinder iBinder) {
+ boolean carServiceHasCrashed;
int lastSwitchedUser;
- boolean systemBootCompleted;
+ ArrayList<Runnable> pendingOperations;
+ SparseIntArray lastUserLifecycle = null;
synchronized (mLock) {
if (mCarService == iBinder) {
return; // already connected.
}
- if (mCarService != null) {
- Slog.i(TAG, "car service binder changed, was:" + mCarService
- + " new:" + iBinder);
- }
+ Slog.i(TAG, "car service binder changed, was:" + mCarService + " new:" + iBinder);
mCarService = iBinder;
+ carServiceHasCrashed = mCarServiceHasCrashed;
+ mCarServiceHasCrashed = false;
lastSwitchedUser = mLastSwitchedUser;
- systemBootCompleted = mSystemBootCompleted;
+ pendingOperations = mPendingOperations;
+ mPendingOperations = null;
+ if (carServiceHasCrashed) {
+ lastUserLifecycle = mLastUserLifecycle.clone();
+ }
}
+ int numberOperations = pendingOperations == null ? 0 : pendingOperations.size();
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_SVC_CONNECTED, numberOperations);
+
Slog.i(TAG, "**CarService connected**");
+
sendSetCarServiceHelperBinderCall();
- if (systemBootCompleted) {
- notifyAllUnlockedUsers();
- }
- if (lastSwitchedUser != UserHandle.USER_NULL) {
- sendSwitchUserBindercall(lastSwitchedUser);
+ if (carServiceHasCrashed) {
+ int numUsers = lastUserLifecycle.size();
+ TimingsTraceAndSlog t = newTimingsTraceAndSlog();
+ t.traceBegin("send-uses-after-reconnect-" + numUsers);
+ // Send user0 events first
+ int user0Lifecycle = lastUserLifecycle.get(UserHandle.USER_SYSTEM,
+ USER_LIFECYCLE_EVENT_TYPE_STARTING);
+ lastUserLifecycle.delete(UserHandle.USER_SYSTEM);
+ boolean user0IsCurrent = lastSwitchedUser == UserHandle.USER_SYSTEM;
+ sendAllLifecyleToUser(UserHandle.USER_SYSTEM, user0Lifecycle, user0IsCurrent);
+ // Send current user events next
+ if (!user0IsCurrent) {
+ int currentUserLifecycle = lastUserLifecycle.get(lastSwitchedUser,
+ USER_LIFECYCLE_EVENT_TYPE_STARTING);
+ lastUserLifecycle.delete(lastSwitchedUser);
+ sendAllLifecyleToUser(lastSwitchedUser, currentUserLifecycle,
+ /* isCurrentUser= */ true);
+ }
+ // Send all other users' events
+ for (int i = 0; i < lastUserLifecycle.size(); i++) {
+ int userId = lastUserLifecycle.keyAt(i);
+ int lifecycle = lastUserLifecycle.valueAt(i);
+ sendAllLifecyleToUser(userId, lifecycle, /* isCurrentUser= */ false);
+ }
+ t.traceEnd();
+ } else if (pendingOperations != null) {
+ if (DBG) Slog.d(TAG, "Running " + numberOperations + " pending operations");
+ TimingsTraceAndSlog t = newTimingsTraceAndSlog();
+ t.traceBegin("send-pending-ops-" + numberOperations);
+ for (int i = 0; i < numberOperations; i++) {
+ Runnable operation = pendingOperations.get(i);
+ try {
+ operation.run();
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "exception running operation #" + i + ": " + e);
+ }
+ }
+ t.traceEnd();
}
}
- private void handleUserLockStatusChange(int userHandle, boolean unlocked) {
- boolean shouldNotify = false;
- synchronized (mLock) {
- Boolean oldStatus = mUserUnlockedStatus.get(userHandle);
- if (oldStatus == null || oldStatus != unlocked) {
- mUserUnlockedStatus.put(userHandle, unlocked);
- if (mCarService != null && mSystemBootCompleted) {
- shouldNotify = true;
- }
- }
+ private void sendAllLifecyleToUser(@UserIdInt int userId, int lifecycle,
+ boolean isCurrentUser) {
+ if (DBG) {
+ Slog.d(TAG, "sendAllLifecyleToUser, user:" + userId + " lifecycle:" + lifecycle);
+ }
+ if (lifecycle >= USER_LIFECYCLE_EVENT_TYPE_STARTING) {
+ sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING, LIFECYCLE_TIMESTAMP_IGNORE,
+ UserHandle.USER_NULL, userId);
+ }
+ if (isCurrentUser && userId != UserHandle.USER_SYSTEM) {
+ // Do not care about actual previous user.
+ sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING, LIFECYCLE_TIMESTAMP_IGNORE,
+ UserHandle.USER_SYSTEM, userId);
}
- if (shouldNotify) {
- sendSetUserLockStatusBinderCall(userHandle, unlocked);
+ if (lifecycle >= USER_LIFECYCLE_EVENT_TYPE_UNLOCKING) {
+ sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKING, LIFECYCLE_TIMESTAMP_IGNORE,
+ UserHandle.USER_NULL, userId);
}
+ if (lifecycle >= USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) {
+ sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED, LIFECYCLE_TIMESTAMP_IGNORE,
+ UserHandle.USER_NULL, userId);
+ }
+ }
+
+ private TimingsTraceAndSlog newTimingsTraceAndSlog() {
+ return new TimingsTraceAndSlog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
}
- private void setupAndStartUsers() {
+ private void setupAndStartUsers(@NonNull TimingsTraceAndSlog t) {
DevicePolicyManager devicePolicyManager =
mContext.getSystemService(DevicePolicyManager.class);
if (devicePolicyManager != null && devicePolicyManager.getUserProvisioningState()
@@ -260,84 +581,191 @@ public class CarServiceHelperService extends SystemService {
Slog.i(TAG, "DevicePolicyManager active, skip user unlock/switch");
return;
}
- // Offloading the whole unlock into separate thread did not help due to single locks
- // used in AMS / PMS ended up stopping the world with lots of lock contention.
- // To run these in background, there should be some improvements there.
- int targetUserId = UserHandle.USER_SYSTEM;
- if (mCarUserManagerHelper.getAllUsers().size() == 0) {
- Slog.i(TAG, "Create new admin user and switch");
- // On very first boot, create an admin user and switch to that user.
- UserInfo admin = mCarUserManagerHelper.createNewAdminUser();
- if (admin == null) {
- Slog.e(TAG, "cannot create admin user");
- return;
- }
- targetUserId = admin.id;
+ t.traceBegin("setupAndStartUsers");
+ if (mHalEnabled) {
+ Slog.i(TAG, "Delegating initial switching to HAL");
+ setupAndStartUsersUsingHal();
} else {
- targetUserId = mCarUserManagerHelper.getInitialUser();
- Slog.i(TAG, "Switching to user " + targetUserId + " on boot");
+ setupAndStartUsersDirectly(t, /* userLocales= */ null);
}
+ t.traceEnd();
+ }
- IActivityManager am = ActivityManager.getService();
- if (am == null) {
- Slog.wtf(TAG, "cannot get ActivityManagerService");
- return;
+ private void handleHalTimedout() {
+ synchronized (mLock) {
+ if (mInitialized) return;
}
- // If system user is the only user to unlock, handle it when system completes the boot.
- if (targetUserId == UserHandle.USER_SYSTEM) {
- return;
- }
+ Slog.w(TAG, "HAL didn't respond in " + mHalTimeoutMs + "ms; using default behavior");
+ setupAndStartUsersDirectly();
+ }
- TimingsTraceLog t = new TimingsTraceLog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
- unlockSystemUser(t, am);
+ private void setupAndStartUsersUsingHal() {
+ mHandler.sendMessageDelayed(obtainMessage(CarServiceHelperService::handleHalTimedout, this)
+ .setWhat(WHAT_HAL_TIMEOUT), mHalTimeoutMs);
- t.traceBegin("ForegroundUserStart" + targetUserId);
- try {
- if (!am.startUserInForegroundWithListener(targetUserId, null)) {
- Slog.e(TAG, "cannot start foreground user:" + targetUserId);
- } else {
- mCarUserManagerHelper.setLastActiveUser(targetUserId);
- }
- } catch (RemoteException e) {
- // should not happen for local call.
- Slog.wtf("RemoteException from AMS", e);
- }
- t.traceEnd();
- }
+ // TODO(b/150413515): get rid of receiver once returned?
+ IResultReceiver receiver = new IResultReceiver.Stub() {
+ @Override
+ public void send(int resultCode, Bundle resultData) {
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_HAL_RESPONSE, resultCode);
- private void unlockSystemUser(@NonNull TimingsTraceLog t, @NonNull IActivityManager am) {
- t.traceBegin("UnlockSystemUser");
- try {
- // This is for force changing state into RUNNING_LOCKED. Otherwise unlock does not
- // update the state and user 0 unlock happens twice.
- boolean started = am.startUserInBackground(UserHandle.USER_SYSTEM);
- if (!started) {
- Slog.w(TAG, "could not restart system user in foreground; trying unlock instead");
- t.traceBegin("forceUnlockSystemUser");
- boolean unlocked = am.unlockUser(UserHandle.USER_SYSTEM,
- /* token= */ null, /* secret= */ null, /* listner= */ null);
- t.traceEnd();
- if (!unlocked) {
- Slog.w(TAG, "could not unlock system user neither");
+ setFinalHalResponseTime();
+ if (DBG) {
+ Slog.d(TAG, "Got result from HAL (" +
+ UserHalHelper.halCallbackStatusToString(resultCode) + ") in "
+ + TimeUtils.formatDuration(mHalResponseTime));
+ }
+
+ mHandler.removeMessages(WHAT_HAL_TIMEOUT);
+ // TODO(b/150222501): log how long it took to receive the response
+ // TODO(b/150413515): print resultData as well on 2 logging calls below
+ synchronized (mLock) {
+ if (mInitialized) {
+ Slog.w(TAG, "Result from HAL came too late, ignoring: "
+ + UserHalHelper.halCallbackStatusToString(resultCode));
+ return;
+ }
+ }
+
+ if (resultCode != HalCallback.STATUS_OK) {
+ Slog.w(TAG, "Service returned non-ok status ("
+ + UserHalHelper.halCallbackStatusToString(resultCode)
+ + "); using default behavior");
+ fallbackToDefaultInitialUserBehavior();
+ return;
+ }
+
+ if (resultData == null) {
+ Slog.w(TAG, "Service returned null bundle");
+ fallbackToDefaultInitialUserBehavior();
return;
}
+
+ int action = resultData.getInt(CarUserServiceConstants.BUNDLE_INITIAL_INFO_ACTION,
+ InitialUserInfoResponseAction.DEFAULT);
+
+ String userLocales = resultData
+ .getString(CarUserServiceConstants.BUNDLE_USER_LOCALES);
+ if (userLocales != null) {
+ Slog.i(TAG, "Changing user locales to " + userLocales);
+ }
+
+ switch (action) {
+ case InitialUserInfoResponseAction.DEFAULT:
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_HAL_DEFAULT_BEHAVIOR,
+ /* fallback= */ 0, userLocales);
+ if (DBG) Slog.d(TAG, "User HAL returned DEFAULT behavior");
+ setupAndStartUsersDirectly(newTimingsTraceAndSlog(), userLocales);
+ return;
+ case InitialUserInfoResponseAction.SWITCH:
+ int userId = resultData.getInt(CarUserServiceConstants.BUNDLE_USER_ID);
+ startUserByHalRequest(userId, userLocales);
+ return;
+ case InitialUserInfoResponseAction.CREATE:
+ String name = resultData
+ .getString(CarUserServiceConstants.BUNDLE_USER_NAME);
+ int flags = resultData.getInt(CarUserServiceConstants.BUNDLE_USER_FLAGS);
+ createUserByHalRequest(name, userLocales, flags);
+ return;
+ default:
+ Slog.w(TAG, "Invalid InitialUserInfoResponseAction action: " + action);
+ }
+ fallbackToDefaultInitialUserBehavior();
}
- } catch (RemoteException e) {
- // should not happen for local call.
- Slog.wtf("RemoteException from AMS", e);
- } finally {
- t.traceEnd();
+ };
+ int initialUserInfoRequestType = getInitialUserInfoRequestType();
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_HAL_REQUEST, initialUserInfoRequestType);
+
+ setInitialHalResponseTime();
+ sendOrQueueGetInitialUserInfo(initialUserInfoRequestType, receiver);
+ }
+
+ @VisibleForTesting
+ int getInitialUserInfoRequestType() {
+ if (!mCarUserManagerHelper.hasInitialUser()) {
+ return InitialUserInfoRequestType.FIRST_BOOT;
+ }
+ if (mContext.getPackageManager().isDeviceUpgrading()) {
+ return InitialUserInfoRequestType.FIRST_BOOT_AFTER_OTA;
}
+ return InitialUserInfoRequestType.COLD_BOOT;
}
- private void managePreCreatedUsers() {
+ private void startUserByHalRequest(@UserIdInt int userId, @Nullable String userLocales) {
+ if (userId <= 0) {
+ Slog.w(TAG, "invalid (or missing) user id sent by HAL: " + userId);
+ fallbackToDefaultInitialUserBehavior();
+ return;
+ }
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_HAL_START_USER, userId, userLocales);
+ if (DBG) Slog.d(TAG, "Starting user " + userId + " as requested by HAL");
+
+ // It doesn't need to replace guest, as the switch would fail anyways if the requested user
+ // was a guest because it wouldn't exist.
+ mInitialUserSetter.set(newInitialUserInfoBuilder(InitialUserSetter.TYPE_SWITCH)
+ .setUserLocales(userLocales)
+ .setSwitchUserId(userId).build());
+ }
+
+ private InitialUserSetter.Builder newInitialUserInfoBuilder(@InitialUserInfoType int type) {
+ return new InitialUserSetter.Builder(type)
+ .setSupportsOverrideUserIdProperty(!CarProperties.user_hal_enabled().orElse(false));
+ }
+
+ private void createUserByHalRequest(@Nullable String name, @Nullable String userLocales,
+ int halFlags) {
+ String friendlyName = "user with name '" + safeName(name) + "', locales " + userLocales
+ + ", and flags " + UserHalHelper.userFlagsToString(halFlags);
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_HAL_CREATE_USER, halFlags, safeName(name),
+ userLocales);
+ if (DBG) Slog.d(TAG, "HAL request creation of " + friendlyName);
+
+ mInitialUserSetter.set(newInitialUserInfoBuilder(InitialUserSetter.TYPE_CREATE)
+ .setUserLocales(userLocales)
+ .setNewUserName(name)
+ .setNewUserFlags(halFlags).build());
+
+ }
+
+ private void fallbackToDefaultInitialUserBehavior() {
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_HAL_DEFAULT_BEHAVIOR, /* fallback= */ 1);
+ if (DBG) Slog.d(TAG, "Falling back to DEFAULT initial user behavior");
+ setupAndStartUsersDirectly();
+ }
+
+ private void setupAndStartUsersDirectly() {
+ setupAndStartUsersDirectly(newTimingsTraceAndSlog(), /* userLocales= */ null);
+ }
+
+ private void setupAndStartUsersDirectly(@NonNull TimingsTraceAndSlog t,
+ @Nullable String userLocales) {
+ synchronized (mLock) {
+ if (mInitialized) {
+ Slog.wtf(TAG, "Already initialized", new Exception());
+ return;
+ }
+ mInitialized = true;
+ }
+
+ mInitialUserSetter.set(newInitialUserInfoBuilder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR)
+ .setUserLocales(userLocales)
+ .build());
+ }
+
+ @VisibleForTesting
+ void managePreCreatedUsers() {
// First gets how many pre-createad users are defined by the OEM
int numberRequestedGuests = CarProperties.number_pre_created_guests().orElse(0);
int numberRequestedUsers = CarProperties.number_pre_created_users().orElse(0);
- Slog.i(TAG, "managePreCreatedUsers(): OEM asked for " + numberRequestedGuests
- + " guests and " + numberRequestedUsers + " users");
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_PRE_CREATION_REQUESTED, numberRequestedUsers,
+ numberRequestedGuests);
+ if (DBG) {
+ Slog.d(TAG, "managePreCreatedUsers(): OEM asked for " + numberRequestedGuests
+ + " guests and " + numberRequestedUsers + " users");
+ }
+
if (numberRequestedGuests < 0 || numberRequestedUsers < 0) {
Slog.w(TAG, "preCreateUsers(): invalid values provided by OEM; "
+ "number_pre_created_guests=" + numberRequestedGuests
@@ -345,24 +773,25 @@ public class CarServiceHelperService extends SystemService {
return;
}
- if (numberRequestedGuests == 0 && numberRequestedUsers == 0) {
- Slog.i(TAG, "managePreCreatedUsers(): not defined by OEM");
- return;
- }
-
// Then checks how many exist already
List<UserInfo> allUsers = mUserManager.getUsers(/* excludePartial= */ true,
/* excludeDying= */ true, /* excludePreCreated= */ false);
int allUsersSize = allUsers.size();
- if (DBG) Slog.d(TAG, "preCreateUsers: total users size is " + allUsersSize);
+ if (DBG) Slog.d(TAG, "preCreateUsers: total users size is " + allUsersSize);
int numberExistingGuests = 0;
int numberExistingUsers = 0;
// List of pre-created users that were not properly initialized. Typically happens when
// the system crashed / rebooted before they were fully started.
- SparseBooleanArray invalidUsers = new SparseBooleanArray();
+ SparseBooleanArray invalidPreCreatedUsers = new SparseBooleanArray();
+
+ // List of all pre-created users - it will be used to remove unused ones (when needed)
+ SparseBooleanArray existingPrecreatedUsers = new SparseBooleanArray();
+
+ // List of extra pre-created users and guests - they will be removed
+ List<Integer> extraPreCreatedUsers = new ArrayList<>();
for (int i = 0; i < allUsersSize; i++) {
UserInfo user = allUsers.get(i);
@@ -370,27 +799,43 @@ public class CarServiceHelperService extends SystemService {
if (!user.isInitialized()) {
Slog.w(TAG, "Found invalid pre-created user that needs to be removed: "
+ user.toFullString());
- invalidUsers.append(user.id, /* notUsed=*/ true);
+ invalidPreCreatedUsers.append(user.id, /* notUsed=*/ true);
continue;
}
- if (user.isGuest()) {
+ boolean isGuest = user.isGuest();
+ existingPrecreatedUsers.put(user.id, isGuest);
+ if (isGuest) {
numberExistingGuests++;
+ if (numberExistingGuests > numberRequestedGuests) {
+ extraPreCreatedUsers.add(user.id);
+ }
} else {
numberExistingUsers++;
+ if (numberExistingUsers > numberRequestedUsers) {
+ extraPreCreatedUsers.add(user.id);
+ }
}
}
if (DBG) {
- Slog.i(TAG, "managePreCreatedUsers(): system already has " + numberExistingGuests
+ Slog.d(TAG, "managePreCreatedUsers(): system already has " + numberExistingGuests
+ " pre-created guests," + numberExistingUsers + " pre-created users, and these"
- + " invalid users: " + invalidUsers );
+ + " invalid users: " + invalidPreCreatedUsers
+ + " extra pre-created users: " + extraPreCreatedUsers);
}
- int numberGuests = numberRequestedGuests - numberExistingGuests;
- int numberUsers = numberRequestedUsers - numberExistingUsers;
- int numberInvalidUsers = invalidUsers.size();
+ int numberGuestsToAdd = numberRequestedGuests - numberExistingGuests;
+ int numberUsersToAdd = numberRequestedUsers - numberExistingUsers;
+ int numberGuestsToRemove = numberExistingGuests - numberRequestedGuests;
+ int numberUsersToRemove = numberExistingUsers - numberRequestedUsers;
+ int numberInvalidUsersToRemove = invalidPreCreatedUsers.size();
+
+ EventLog.writeEvent(EventLogTags.CAR_HELPER_PRE_CREATION_STATUS,
+ numberExistingUsers, numberUsersToAdd, numberUsersToRemove,
+ numberExistingGuests, numberGuestsToAdd, numberGuestsToRemove,
+ numberInvalidUsersToRemove);
- if (numberGuests <= 0 && numberUsers <= 0 && numberInvalidUsers == 0) {
- Slog.i(TAG, "managePreCreatedUsers(): all pre-created and no invalid ones");
+ if (numberGuestsToAdd == 0 && numberUsersToAdd == 0 && numberInvalidUsersToRemove == 0) {
+ if (DBG) Slog.d(TAG, "managePreCreatedUsers(): everything in sync");
return;
}
@@ -400,37 +845,50 @@ public class CarServiceHelperService extends SystemService {
// submitting just 1 task, for 2 reasons:
// 1.To minimize it's effect on other system server initialization tasks.
// 2.The pre-created users will be unlocked in parallel anyways.
- new Thread( () -> {
- TimingsTraceLog t = new TimingsTraceLog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
+ runAsync(() -> {
+ TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG + "Async",
+ Trace.TRACE_TAG_SYSTEM_SERVER);
t.traceBegin("preCreateUsers");
- if (numberUsers > 0) {
- preCreateUsers(t, numberUsers, /* isGuest= */ false);
+ if (numberUsersToAdd > 0) {
+ preCreateUsers(t, numberUsersToAdd, /* isGuest= */ false);
}
- if (numberGuests > 0) {
- preCreateUsers(t, numberGuests, /* isGuest= */ true);
+ if (numberGuestsToAdd > 0) {
+ preCreateUsers(t, numberGuestsToAdd, /* isGuest= */ true);
}
+
+ int totalNumberToRemove = extraPreCreatedUsers.size();
+ if (DBG) Slog.d(TAG, "Must delete " + totalNumberToRemove + " pre-created users");
+ if (totalNumberToRemove > 0) {
+ int[] usersToRemove = new int[totalNumberToRemove];
+ for (int i = 0; i < totalNumberToRemove; i++) {
+ usersToRemove[i] = extraPreCreatedUsers.get(i);
+ }
+ removePreCreatedUsers(usersToRemove);
+ }
+
t.traceEnd();
- if (numberInvalidUsers > 0) {
+ if (numberInvalidUsersToRemove > 0) {
t.traceBegin("removeInvalidPreCreatedUsers");
- for (int i = 0; i < numberInvalidUsers; i++) {
- int userId = invalidUsers.keyAt(i);
+ for (int i = 0; i < numberInvalidUsersToRemove; i++) {
+ int userId = invalidPreCreatedUsers.keyAt(i);
Slog.i(TAG, "removing invalid pre-created user " + userId);
mUserManager.removeUser(userId);
}
t.traceEnd();
}
- }, "CarServiceHelperManagePreCreatedUsers").start();
+ });
}
- private void preCreateUsers(@NonNull TimingsTraceLog t, int size, boolean isGuest) {
+ private void preCreateUsers(@NonNull TimingsTraceAndSlog t, int size, boolean isGuest) {
String msg = isGuest ? "preCreateGuests-" + size : "preCreateUsers-" + size;
+ if (DBG) Slog.d(TAG, "preCreateUsers: " + msg);
t.traceBegin(msg);
for (int i = 1; i <= size; i++) {
UserInfo preCreated = preCreateUsers(t, isGuest);
if (preCreated == null) {
- Slog.w(TAG, "Could not pre-create " + (isGuest ? " guest " : "")
+ Slog.w(TAG, "Could not pre-create" + (isGuest ? " guest" : "")
+ " user #" + i);
continue;
}
@@ -438,74 +896,175 @@ public class CarServiceHelperService extends SystemService {
t.traceEnd();
}
- // TODO(b/111451156): add unit test?
+ @VisibleForTesting
+ void runAsync(Runnable r) {
+ // We cannot use SystemServerInitThreadPool because user pre-creation can take too long,
+ // which would crash the SystemServer on SystemServerInitThreadPool.shutdown();
+ String threadName = TAG + ".AsyncTask";
+ Slog.i(TAG, "Starting thread " + threadName);
+ new Thread(() -> {
+ try {
+ r.run();
+ Slog.i(TAG, "Finishing thread " + threadName);
+ } catch (RuntimeException e) {
+ Slog.e(TAG, "runAsync() failed", e);
+ throw e;
+ }
+ }, threadName).start();
+ }
+
@Nullable
- public UserInfo preCreateUsers(@NonNull TimingsTraceLog t, boolean isGuest) {
- int flags = 0;
- String traceMsg = "pre-create";
- if (isGuest) {
- flags |= UserInfo.FLAG_GUEST;
- traceMsg += "-guest";
- } else {
- traceMsg += "-user";
- }
+ public UserInfo preCreateUsers(@NonNull TimingsTraceAndSlog t, boolean isGuest) {
+ String traceMsg = "pre-create" + (isGuest ? "-guest" : "-user");
t.traceBegin(traceMsg);
// NOTE: we want to get rid of UserManagerHelper, so let's call UserManager directly
- UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
- UserInfo user = um.preCreateUser(flags);
+ String userType =
+ isGuest ? UserManager.USER_TYPE_FULL_GUEST : UserManager.USER_TYPE_FULL_SECONDARY;
+ UserInfo user = null;
try {
+ user = mUserManager.preCreateUser(userType);
if (user == null) {
- // Couldn't create user, most likely because there are too many.
- Slog.w(TAG, "couldn't " + traceMsg);
- return null;
+ logPrecreationFailure(traceMsg, /* cause= */ null);
}
+ } catch (Exception e) {
+ logPrecreationFailure(traceMsg, e);
} finally {
t.traceEnd();
}
return user;
}
- private void notifyAllUnlockedUsers() {
- // only care about unlocked users
- LinkedList<Integer> users = new LinkedList<>();
- synchronized (mLock) {
- for (Map.Entry<Integer, Boolean> entry : mUserUnlockedStatus.entrySet()) {
- if (entry.getValue()) {
- users.add(entry.getKey());
- }
- }
- }
- if (DBG) {
- Slog.d(TAG, "notifyAllUnlockedUsers:" + users);
+ private void removePreCreatedUsers(int[] usersToRemove) {
+ for (int userId : usersToRemove) {
+ Slog.i(TAG, "removing pre-created user with id " + userId);
+ mUserManager.removeUser(userId);
}
- for (Integer i : users) {
- sendSetUserLockStatusBinderCall(i, true);
+ }
+
+ /**
+ * Logs proper message when user pre-creation fails (most likely because there are too many).
+ */
+ @VisibleForTesting
+ void logPrecreationFailure(@NonNull String operation, @Nullable Exception cause) {
+ int maxNumberUsers = UserManager.getMaxSupportedUsers();
+ int currentNumberUsers = mUserManager.getUserCount();
+ StringBuilder message = new StringBuilder(operation.length() + 100)
+ .append(operation).append(" failed. Number users: ").append(currentNumberUsers)
+ .append(" Max: ").append(maxNumberUsers);
+ if (cause == null) {
+ Slog.w(TAG, message.toString());
+ } else {
+ Slog.w(TAG, message.toString(), cause);
}
}
private void sendSetCarServiceHelperBinderCall() {
Parcel data = Parcel.obtain();
- data.writeInterfaceToken(CAR_SERVICE_INTERFACE);
+ data.writeInterfaceToken(ICarConstants.CAR_SERVICE_INTERFACE);
data.writeStrongBinder(mHelper.asBinder());
// void setCarServiceHelper(in IBinder helper)
- sendBinderCallToCarService(data, ICAR_CALL_SET_CAR_SERVICE_HELPER);
+ sendBinderCallToCarService(data, ICarConstants.ICAR_CALL_SET_CAR_SERVICE_HELPER);
+ }
+
+ private void sendUserLifecycleEvent(int eventType, @NonNull TargetUser user) {
+ sendUserLifecycleEvent(eventType, /* from= */ null, user);
+ }
+
+ private void sendUserLifecycleEvent(int eventType, @Nullable TargetUser from,
+ @NonNull TargetUser to) {
+ long now = System.currentTimeMillis();
+ synchronized (mLock) {
+ if (eventType == USER_LIFECYCLE_EVENT_TYPE_SWITCHING) {
+ mLastSwitchedUser = to.getUserIdentifier();
+ } else if (eventType == USER_LIFECYCLE_EVENT_TYPE_STOPPING
+ || eventType == USER_LIFECYCLE_EVENT_TYPE_STOPPED) {
+ mLastUserLifecycle.delete(to.getUserIdentifier());
+ } else {
+ mLastUserLifecycle.put(to.getUserIdentifier(), eventType);
+ }
+ if (mCarService == null) {
+ if (DBG) Slog.d(TAG, "Queuing lifecycle event " + eventType + " for user " + to);
+ queueOperationLocked(() -> sendUserLifecycleEvent(eventType, now, from, to));
+ return;
+ }
+ }
+ TimingsTraceAndSlog t = newTimingsTraceAndSlog();
+ t.traceBegin("send-lifecycle-" + eventType + "-" + to.getUserIdentifier());
+ sendUserLifecycleEvent(eventType, now, from, to);
+ t.traceEnd();
}
- private void sendSetUserLockStatusBinderCall(int userHandle, boolean unlocked) {
+ private void sendUserLifecycleEvent(int eventType, long timestamp, @Nullable TargetUser from,
+ @NonNull TargetUser to) {
+ int fromId = from == null ? UserHandle.USER_NULL : from.getUserIdentifier();
+ int toId = to.getUserIdentifier();
+ sendUserLifecycleEvent(eventType, timestamp, fromId, toId);
+ }
+
+ private void sendUserLifecycleEvent(int eventType, long timestamp, @UserIdInt int fromId,
+ @UserIdInt int toId) {
Parcel data = Parcel.obtain();
- data.writeInterfaceToken(CAR_SERVICE_INTERFACE);
- data.writeInt(userHandle);
- data.writeInt(unlocked ? 1 : 0);
- // void setUserLockStatus(in int userHandle, in int unlocked)
- sendBinderCallToCarService(data, ICAR_CALL_SET_USER_UNLOCK_STATUS);
+ data.writeInterfaceToken(ICarConstants.CAR_SERVICE_INTERFACE);
+ data.writeInt(eventType);
+ data.writeLong(timestamp);
+ data.writeInt(fromId);
+ data.writeInt(toId);
+ // void onUserLifecycleEvent(int eventType, long timestamp, int from, int to)
+ sendBinderCallToCarService(data, ICarConstants.ICAR_CALL_ON_USER_LIFECYCLE);
}
- private void sendSwitchUserBindercall(int userHandle) {
+ private void sendOrQueueGetInitialUserInfo(int requestType, @NonNull IResultReceiver receiver) {
+ synchronized (mLock) {
+ if (mCarService == null) {
+ if (DBG) Slog.d(TAG, "Queuing GetInitialUserInfo call for type " + requestType);
+ queueOperationLocked(() -> sendGetInitialUserInfo(requestType, receiver));
+ return;
+ }
+ }
+ sendGetInitialUserInfo(requestType, receiver);
+ }
+
+ private void sendGetInitialUserInfo(int requestType, @NonNull IResultReceiver receiver) {
Parcel data = Parcel.obtain();
- data.writeInterfaceToken(CAR_SERVICE_INTERFACE);
- data.writeInt(userHandle);
- // void onSwitchUser(in int userHandle)
- sendBinderCallToCarService(data, ICAR_CALL_SET_SWITCH_USER);
+ data.writeInterfaceToken(ICarConstants.CAR_SERVICE_INTERFACE);
+ data.writeInt(requestType);
+ data.writeInt(mHalTimeoutMs);
+ data.writeStrongBinder(receiver.asBinder());
+ // void getInitialUserInfo(int requestType, int timeoutMs, in IResultReceiver receiver)
+ sendBinderCallToCarService(data, ICarConstants.ICAR_CALL_GET_INITIAL_USER_INFO);
+ }
+
+ @VisibleForTesting
+ void setInitialUser(@Nullable UserInfo user) {
+ synchronized (mLock) {
+ if (mCarService == null) {
+ if (DBG) Slog.d(TAG, "Queuing setInitialUser() call");
+ queueOperationLocked(() -> sendSetInitialUser(user));
+ return;
+ }
+ }
+ sendSetInitialUser(user);
+ }
+
+ private void sendSetInitialUser(@Nullable UserInfo user) {
+ if (DBG) Slog.d(TAG, "sendSetInitialUser(): " + user);
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(ICarConstants.CAR_SERVICE_INTERFACE);
+ data.writeInt(user != null ? user.id : UserHandle.USER_NULL);
+ // void setInitialUser(int userId)
+ sendBinderCallToCarService(data, ICarConstants.ICAR_CALL_SET_INITIAL_USER);
+ }
+
+ private void sendFirstUserUnlocked(@NonNull TargetUser user) {
+ long now = System.currentTimeMillis();
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(ICarConstants.CAR_SERVICE_INTERFACE);
+ data.writeInt(user.getUserIdentifier());
+ data.writeLong(now);
+ data.writeLong(mFirstUnlockedUserDuration);
+ data.writeInt(mHalResponseTime);
+ // void onFirstUserUnlocked(int userId, long timestamp, long duration, int halResponseTime)
+ sendBinderCallToCarService(data, ICarConstants.ICAR_CALL_FIRST_USER_UNLOCKED);
}
private void sendBinderCallToCarService(Parcel data, int callNumber) {
@@ -515,12 +1074,23 @@ public class CarServiceHelperService extends SystemService {
synchronized (mLock) {
carService = mCarService;
}
+ if (carService == null) {
+ Slog.w(TAG, "Not calling txn " + callNumber + " because service is not bound yet",
+ new Exception());
+ return;
+ }
+ int code = IBinder.FIRST_CALL_TRANSACTION + callNumber;
try {
- carService.transact(IBinder.FIRST_CALL_TRANSACTION + callNumber,
- data, null, Binder.FLAG_ONEWAY);
+ if (VERBOSE) Slog.v(TAG, "calling one-way binder transaction with code " + code);
+ carService.transact(code, data, null, Binder.FLAG_ONEWAY);
+ if (VERBOSE) Slog.v(TAG, "finished one-way binder transaction with code " + code);
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException from car service", e);
handleCarServiceCrash();
+ } catch (RuntimeException e) {
+ Slog.wtf(TAG, "Exception calling binder transaction " + callNumber + " (real code: "
+ + code + ")", e);
+ throw e;
} finally {
data.recycle();
}
@@ -576,10 +1146,11 @@ public class CarServiceHelperService extends SystemService {
pids.add(Process.myPid());
ActivityManagerService.dumpStackTraces(
- pids, null, null, getInterestingNativePids());
+ pids, null, null, getInterestingNativePids(), null);
}
- private void handleCarServiceCrash() {
+ @VisibleForTesting
+ void handleCarServiceCrash() {
// Recovery behavior. Kill the system server and reset
// everything if enabled by the property.
boolean restartOnServiceCrash = SystemProperties.getBoolean(PROP_RESTART_RUNTIME, false);
@@ -593,6 +1164,48 @@ public class CarServiceHelperService extends SystemService {
} else {
Slog.w(TAG, "*** CARHELPER ignoring: " + "CarService crash");
}
+ synchronized (mLock) {
+ mCarServiceHasCrashed = true;
+ }
+ }
+
+ private void handleClientsNotResponding(@NonNull int[] pids) {
+ mProcessTerminator.requestTerminateProcess(pids);
+ }
+
+ private void registerMonitorToWatchdogDaemon() {
+ try {
+ mCarWatchdogDaemonHelper.registerMonitor(mCarWatchdogMonitor);
+ } catch (RemoteException | RuntimeException e) {
+ Slog.w(TAG, "Cannot register to car watchdog daemon: " + e);
+ }
+ }
+
+ private void killProcessAndReportToMonitor(int pid) {
+ String processName = getProcessName(pid);
+ Process.killProcess(pid);
+ Slog.w(TAG, "carwatchdog killed " + processName + " (pid: " + pid + ")");
+ try {
+ mCarWatchdogDaemonHelper.tellDumpFinished(mCarWatchdogMonitor, pid);
+ } catch (RemoteException | RuntimeException e) {
+ Slog.w(TAG, "Cannot report monitor result to car watchdog daemon: " + e);
+ }
+ }
+
+ private static String getProcessName(int pid) {
+ String unknownProcessName = "unknown process";
+ String filename = "/proc/" + pid + "/cmdline";
+ try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
+ String line = reader.readLine().replace('\0', ' ').trim();
+ int index = line.indexOf(' ');
+ if (index != -1) {
+ line = line.substring(0, index);
+ }
+ return Paths.get(line).getFileName().toString();
+ } catch (IOException e) {
+ Slog.w(TAG, "Cannot read " + filename);
+ return unknownProcessName;
+ }
}
private static native int nativeForceSuspend(int timeoutMs);
@@ -613,5 +1226,129 @@ public class CarServiceHelperService extends SystemService {
}
return retVal;
}
+
+ @Override
+ public void setDisplayWhitelistForUser(@UserIdInt int userId, int[] displayIds) {
+ mCarLaunchParamsModifier.setDisplayWhitelistForUser(userId, displayIds);
+ }
+
+ @Override
+ public void setPassengerDisplays(int[] displayIdsForPassenger) {
+ mCarLaunchParamsModifier.setPassengerDisplays(displayIdsForPassenger);
+ }
+
+ @Override
+ public void setSourcePreferredComponents(boolean enableSourcePreferred,
+ @Nullable List<ComponentName> sourcePreferredComponents) {
+ mCarLaunchParamsModifier.setSourcePreferredComponents(
+ enableSourcePreferred, sourcePreferredComponents);
+ }
+ }
+
+ private class ICarWatchdogMonitorImpl extends ICarWatchdogMonitor.Stub {
+ private final WeakReference<CarServiceHelperService> mService;
+
+ private ICarWatchdogMonitorImpl(CarServiceHelperService service) {
+ mService = new WeakReference<>(service);
+ }
+
+ @Override
+ public void onClientsNotResponding(int[] pids) {
+ CarServiceHelperService service = mService.get();
+ if (service == null || pids == null || pids.length == 0) {
+ return;
+ }
+ service.handleClientsNotResponding(pids);
+ }
+
+ @Override
+ public int getInterfaceVersion() {
+ return this.VERSION;
+ }
+
+ @Override
+ public String getInterfaceHash() {
+ return this.HASH;
+ }
+ }
+
+ private final class ProcessTerminator {
+
+ private static final long ONE_SECOND_MS = 1_000L;
+
+ private final Object mProcessLock = new Object();
+ private ExecutorService mExecutor;
+ @GuardedBy("mProcessLock")
+ private int mQueuedTask;
+
+ public void requestTerminateProcess(@NonNull int[] pids) {
+ synchronized (mProcessLock) {
+ // If there is a running thread, we re-use it instead of starting a new thread.
+ if (mExecutor == null) {
+ mExecutor = Executors.newSingleThreadExecutor();
+ }
+ mQueuedTask++;
+ }
+ mExecutor.execute(() -> {
+ for (int pid : pids) {
+ dumpAndKillProcess(pid);
+ }
+ // mExecutor will be stopped from the main thread, if there is no queued task.
+ mHandler.sendMessage(obtainMessage(ProcessTerminator::postProcessing, this)
+ .setWhat(WHAT_POST_PROCESS_DUMPING));
+ });
+ }
+
+ private void postProcessing() {
+ synchronized (mProcessLock) {
+ mQueuedTask--;
+ if (mQueuedTask == 0) {
+ mExecutor.shutdown();
+ mExecutor = null;
+ }
+ }
+ }
+
+ private void dumpAndKillProcess(int pid) {
+ if (DBG) {
+ Slog.d(TAG, "Dumping and killing process(pid: " + pid + ")");
+ }
+ ArrayList<Integer> javaPids = new ArrayList<>(1);
+ ArrayList<Integer> nativePids = new ArrayList<>();
+ try {
+ if (isJavaApp(pid)) {
+ javaPids.add(pid);
+ } else {
+ nativePids.add(pid);
+ }
+ } catch (IOException e) {
+ Slog.w(TAG, "Cannot get process information: " + e);
+ return;
+ }
+ nativePids.addAll(getInterestingNativePids());
+ long startDumpTime = SystemClock.uptimeMillis();
+ ActivityManagerService.dumpStackTraces(javaPids, null, null, nativePids, null);
+ long dumpTime = SystemClock.uptimeMillis() - startDumpTime;
+ if (DBG) {
+ Slog.d(TAG, "Dumping process took " + dumpTime + "ms");
+ }
+ // To give clients a chance of wrapping up before the termination.
+ if (dumpTime < ONE_SECOND_MS) {
+ mHandler.sendMessageDelayed(obtainMessage(
+ CarServiceHelperService::killProcessAndReportToMonitor,
+ CarServiceHelperService.this, pid).setWhat(WHAT_PROCESS_KILL),
+ ONE_SECOND_MS - dumpTime);
+ } else {
+ killProcessAndReportToMonitor(pid);
+ }
+ }
+
+ private boolean isJavaApp(int pid) throws IOException {
+ Path exePath = new File("/proc/" + pid + "/exe").toPath();
+ String target = Files.readSymbolicLink(exePath).toString();
+ // Zygote's target exe is also /system/bin/app_process32 or /system/bin/app_process64.
+ // But, we can be very sure that Zygote will not be the client of car watchdog daemon.
+ return target == "/system/bin/app_process32" || target == "/system/bin/app_process64";
+ }
}
}
diff --git a/src/com/android/internal/car/ExternalConstants.java b/src/com/android/internal/car/ExternalConstants.java
new file mode 100644
index 0000000..d9d6345
--- /dev/null
+++ b/src/com/android/internal/car/ExternalConstants.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2020 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.internal.car;
+
+/**
+ * Provides constants that are defined somewhere else and must be cloned here
+ */
+final class ExternalConstants {
+
+ private ExternalConstants() {
+ throw new UnsupportedOperationException("contains only static constants");
+ }
+
+ // TODO(b/149797595): remove once ICar.aidl is split in 2
+ static final class ICarConstants {
+ static final String CAR_SERVICE_INTERFACE = "android.car.ICar";
+
+ // These numbers should match with binder call order of
+ // packages/services/Car/car-lib/src/android/car/ICar.aidl
+ static final int ICAR_CALL_SET_CAR_SERVICE_HELPER = 0;
+ static final int ICAR_CALL_ON_USER_LIFECYCLE = 1;
+ static final int ICAR_CALL_FIRST_USER_UNLOCKED = 2;
+ static final int ICAR_CALL_GET_INITIAL_USER_INFO = 3;
+ static final int ICAR_CALL_SET_INITIAL_USER = 4;
+
+ private ICarConstants() {
+ throw new UnsupportedOperationException("contains only static constants");
+ }
+ }
+
+ /**
+ * Constants used by {@link android.user.user.CarUserManager} - they cannot be defined on
+ * {@link android.car.userlib.CommonConstants} to avoid an extra dependency in the
+ * {@code android.car} project
+ */
+ static final class CarUserManagerConstants {
+
+ static final int USER_LIFECYCLE_EVENT_TYPE_STARTING = 1;
+ static final int USER_LIFECYCLE_EVENT_TYPE_SWITCHING = 2;
+ static final int USER_LIFECYCLE_EVENT_TYPE_UNLOCKING = 3;
+ static final int USER_LIFECYCLE_EVENT_TYPE_UNLOCKED = 4;
+ static final int USER_LIFECYCLE_EVENT_TYPE_STOPPING = 5;
+ static final int USER_LIFECYCLE_EVENT_TYPE_STOPPED = 6;
+
+ private CarUserManagerConstants() {
+ throw new UnsupportedOperationException("contains only static constants");
+ }
+ }
+}
diff --git a/src/com/android/internal/car/ICarServiceHelper.aidl b/src/com/android/internal/car/ICarServiceHelper.aidl
index aecee08..8145229 100644
--- a/src/com/android/internal/car/ICarServiceHelper.aidl
+++ b/src/com/android/internal/car/ICarServiceHelper.aidl
@@ -16,10 +16,33 @@
package com.android.internal.car;
+import android.content.ComponentName;
+
+import java.util.List;
+
/**
- * Helper API for car service. Only for itneraction between system server and car service.
+ * Helper API for car service. Only for interaction between system server and car service.
* @hide
*/
interface ICarServiceHelper {
int forceSuspend(int timeoutMs);
+ /**
+ * Check
+ * {@link com.android.server.wm.CarLaunchParamsModifier#setDisplayWhitelistForUser(int, int[]).
+ */
+ void setDisplayWhitelistForUser(in int userId, in int[] displayIds);
+
+ /**
+ * Check
+ * {@link com.android.server.wm.CarLaunchParamsModifier#setPassengerDisplays(int[])}.
+ */
+ void setPassengerDisplays(in int[] displayIds);
+
+ /**
+ * Check
+ * {@link com.android.server.wm.CarLaunchParamsModifier#setSourcePreferredComponents(
+ * boolean, List<ComponentName>)}.
+ */
+ void setSourcePreferredComponents(
+ in boolean enableSourcePreferred, in List<ComponentName> sourcePreferredComponents);
}
diff --git a/src/com/android/server/wm/CarLaunchParamsModifier.java b/src/com/android/server/wm/CarLaunchParamsModifier.java
new file mode 100644
index 0000000..9e86c63
--- /dev/null
+++ b/src/com/android/server/wm/CarLaunchParamsModifier.java
@@ -0,0 +1,393 @@
+/*
+ * 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.server.wm;
+
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.app.ActivityTaskManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.util.Slog;
+import android.util.SparseIntArray;
+import android.view.Display;
+import android.window.WindowContainerToken;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Class to control the assignment of a display for Car while launching a Activity.
+ *
+ * <p>This one controls which displays users are allowed to launch.
+ * The policy should be passed from car service through
+ * {@link com.android.internal.car.ICarServiceHelper} binder interfaces. If no policy is set,
+ * this module will not change anything for launch process.</p>
+ *
+ * <p> The policy can only affect which display passenger users can use. Current user, assumed
+ * to be a driver user, is allowed to launch any display always.</p>
+ */
+public final class CarLaunchParamsModifier implements LaunchParamsController.LaunchParamsModifier {
+
+ private static final String TAG = "CAR.LAUNCH";
+ private static final boolean DBG = false;
+
+ private final Context mContext;
+
+ private DisplayManager mDisplayManager; // set only from init()
+ private ActivityTaskManagerService mAtm; // set only from init()
+
+ private final Object mLock = new Object();
+
+ // Always start with USER_SYSTEM as the timing of handleCurrentUserSwitching(USER_SYSTEM) is not
+ // guaranteed to be earler than 1st Activity launch.
+ @GuardedBy("mLock")
+ private int mCurrentDriverUser = UserHandle.USER_SYSTEM;
+
+ // TODO: Switch from tracking displays to tracking display areas instead
+ /**
+ * This one is for holding all passenger (=profile user) displays which are mostly static unless
+ * displays are added / removed. Note that {@link #mDisplayToProfileUserMapping} can be empty
+ * while user is assigned and that cannot always tell if specific display is for driver or not.
+ */
+ @GuardedBy("mLock")
+ private final ArrayList<Integer> mPassengerDisplays = new ArrayList<>();
+
+ /** key: display id, value: profile user id */
+ @GuardedBy("mLock")
+ private final SparseIntArray mDisplayToProfileUserMapping = new SparseIntArray();
+
+ /** key: profile user id, value: display id */
+ @GuardedBy("mLock")
+ private final SparseIntArray mDefaultDisplayForProfileUser = new SparseIntArray();
+
+ @GuardedBy("mLock")
+ private boolean mIsSourcePreferred;
+
+ @GuardedBy("mLock")
+ private List<ComponentName> mSourcePreferredComponents;
+
+
+ @VisibleForTesting
+ final DisplayManager.DisplayListener mDisplayListener =
+ new DisplayManager.DisplayListener() {
+ @Override
+ public void onDisplayAdded(int displayId) {
+ // ignore. car service should update whiltelist.
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ synchronized (mLock) {
+ mPassengerDisplays.remove(Integer.valueOf(displayId));
+ updateProfileUserConfigForDisplayRemovalLocked(displayId);
+ }
+ }
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ // ignore
+ }
+ };
+
+ private void updateProfileUserConfigForDisplayRemovalLocked(int displayId) {
+ mDisplayToProfileUserMapping.delete(displayId);
+ int i = mDefaultDisplayForProfileUser.indexOfValue(displayId);
+ if (i >= 0) {
+ mDefaultDisplayForProfileUser.removeAt(i);
+ }
+ }
+
+ /** Constructor. Can be constructed any time. */
+ public CarLaunchParamsModifier(Context context) {
+ // This can be very early stage. So postpone interaction with other system until init.
+ mContext = context;
+ }
+
+ /**
+ * Initializes all internal stuffs. This should be called only after ATMS, DisplayManagerService
+ * are ready.
+ */
+ public void init() {
+ mAtm = (ActivityTaskManagerService) ActivityTaskManager.getService();
+ LaunchParamsController controller = mAtm.mStackSupervisor.getLaunchParamsController();
+ controller.registerModifier(this);
+ mDisplayManager = mContext.getSystemService(DisplayManager.class);
+ mDisplayManager.registerDisplayListener(mDisplayListener,
+ new Handler(Looper.getMainLooper()));
+ }
+
+ /**
+ * Sets sourcePreferred configuration. When sourcePreferred is enabled and there is no pre-
+ * assigned display for the Activity, CarLauncherParamsModifier will launch the Activity in
+ * the display of the source. When sourcePreferredComponents isn't null the sourcePreferred
+ * is applied for the sourcePreferredComponents only.
+ *
+ * @param enableSourcePreferred whether to enable sourcePreferred mode
+ * @param sourcePreferredComponents null for all components, or the list of components to apply
+ */
+ public void setSourcePreferredComponents(boolean enableSourcePreferred,
+ @Nullable List<ComponentName> sourcePreferredComponents) {
+ synchronized (mLock) {
+ mIsSourcePreferred = enableSourcePreferred;
+ mSourcePreferredComponents = sourcePreferredComponents;
+ if (mSourcePreferredComponents != null) {
+ Collections.sort(mSourcePreferredComponents);
+ }
+ }
+ }
+
+ /** Notifies user switching. */
+ public void handleCurrentUserSwitching(int newUserId) {
+ synchronized (mLock) {
+ mCurrentDriverUser = newUserId;
+ mDefaultDisplayForProfileUser.clear();
+ mDisplayToProfileUserMapping.clear();
+ }
+ }
+
+ private void removeUserFromWhitelistsLocked(int userId) {
+ for (int i = mDisplayToProfileUserMapping.size() - 1; i >= 0; i--) {
+ if (mDisplayToProfileUserMapping.valueAt(i) == userId) {
+ mDisplayToProfileUserMapping.removeAt(i);
+ }
+ }
+ mDefaultDisplayForProfileUser.delete(userId);
+ }
+
+ /** Notifies user stopped. */
+ public void handleUserStopped(int stoppedUser) {
+ // Note that the current user is never stopped. It always takes switching into
+ // non-current user before stopping the user.
+ synchronized (mLock) {
+ removeUserFromWhitelistsLocked(stoppedUser);
+ }
+ }
+
+ /**
+ * Sets display whiltelist for the userId. For passenger user, activity will be always launched
+ * to a display in the whitelist. If requested display is not in the whitelist, the 1st display
+ * in the whitelist will be selected as target display.
+ *
+ * <p>The whitelist is kept only for profile user. Assigning the current user unassigns users
+ * for the given displays.
+ */
+ public void setDisplayWhitelistForUser(int userId, int[] displayIds) {
+ if (DBG) {
+ Slog.d(TAG, "setDisplayWhitelistForUser userId:" + userId
+ + " displays:" + displayIds);
+ }
+ synchronized (mLock) {
+ for (int displayId : displayIds) {
+ if (!mPassengerDisplays.contains(displayId)) {
+ Slog.w(TAG, "setDisplayWhitelistForUser called with display:" + displayId
+ + " not in passenger display list:" + mPassengerDisplays);
+ continue;
+ }
+ if (userId == mCurrentDriverUser) {
+ mDisplayToProfileUserMapping.delete(displayId);
+ } else {
+ mDisplayToProfileUserMapping.put(displayId, userId);
+ }
+ // now the display cannot be a default display for other user
+ int i = mDefaultDisplayForProfileUser.indexOfValue(displayId);
+ if (i >= 0) {
+ mDefaultDisplayForProfileUser.removeAt(i);
+ }
+ }
+ if (displayIds.length > 0) {
+ mDefaultDisplayForProfileUser.put(userId, displayIds[0]);
+ } else {
+ removeUserFromWhitelistsLocked(userId);
+ }
+ }
+ }
+
+ /**
+ * Sets displays assigned to passenger. All other displays will be treated as assigned to
+ * driver.
+ *
+ * <p>The 1st display in the array will be considered as a default display to assign
+ * for any non-driver user if there is no display assigned for the user. </p>
+ */
+ public void setPassengerDisplays(int[] displayIdsForPassenger) {
+ if (DBG) {
+ Slog.d(TAG, "setPassengerDisplays displays:" + displayIdsForPassenger);
+ }
+ synchronized (mLock) {
+ for (int id : displayIdsForPassenger) {
+ mPassengerDisplays.remove(Integer.valueOf(id));
+ }
+ // handle removed displays
+ for (int i = 0; i < mPassengerDisplays.size(); i++) {
+ int displayId = mPassengerDisplays.get(i);
+ updateProfileUserConfigForDisplayRemovalLocked(displayId);
+ }
+ mPassengerDisplays.clear();
+ mPassengerDisplays.ensureCapacity(displayIdsForPassenger.length);
+ for (int id : displayIdsForPassenger) {
+ mPassengerDisplays.add(id);
+ }
+ }
+ }
+
+ /**
+ * Decides display to assign while an Activity is launched.
+ *
+ * <p>For current user (=driver), launching to any display is allowed as long as system
+ * allows it.</p>
+ *
+ * <p>For private display, do not change anything as private display has its own logic.</p>
+ *
+ * <p>For passenger displays, only run in allowed displays. If requested display is not
+ * allowed, change to the 1st allowed display.</p>
+ */
+ @Override
+ public int onCalculate(Task task, ActivityInfo.WindowLayout layout, ActivityRecord activity,
+ ActivityRecord source, ActivityOptions options, int phase,
+ LaunchParamsController.LaunchParams currentParams,
+ LaunchParamsController.LaunchParams outParams) {
+ int userId;
+ if (task != null) {
+ userId = task.mUserId;
+ } else if (activity != null) {
+ userId = activity.mUserId;
+ } else {
+ Slog.w(TAG, "onCalculate, cannot decide user");
+ return RESULT_SKIP;
+ }
+ // DisplayArea where user wants to launch the Activity.
+ TaskDisplayArea originalDisplayArea = currentParams.mPreferredTaskDisplayArea;
+ // DisplayArea where CarLaunchParamsModifier targets to launch the Activity.
+ TaskDisplayArea targetDisplayArea = null;
+ if (DBG) {
+ Slog.d(TAG, "onCalculate, userId:" + userId
+ + " original displayArea:" + originalDisplayArea
+ + " ActivityOptions:" + options);
+ }
+ // If originalDisplayArea is set, respect that before ActivityOptions check.
+ if (originalDisplayArea == null) {
+ if (options != null) {
+ WindowContainerToken daToken = options.getLaunchTaskDisplayArea();
+ if (daToken != null) {
+ originalDisplayArea = (TaskDisplayArea) WindowContainer.fromBinder(
+ daToken.asBinder());
+ } else {
+ int originalDisplayId = options.getLaunchDisplayId();
+ if (originalDisplayId != Display.INVALID_DISPLAY) {
+ originalDisplayArea = getDefaultTaskDisplayAreaOnDisplay(originalDisplayId);
+ }
+ }
+ }
+ }
+ decision:
+ synchronized (mLock) {
+ if (originalDisplayArea == null // No specified DisplayArea to launch the Activity
+ && mIsSourcePreferred && source != null
+ && (mSourcePreferredComponents == null || Collections.binarySearch(
+ mSourcePreferredComponents, activity.info.getComponentName()) >= 0)) {
+ targetDisplayArea = source.noDisplay ? source.mHandoverTaskDisplayArea
+ : source.getDisplayArea();
+ }
+ if (userId == mCurrentDriverUser) {
+ // Respect the existing DisplayArea.
+ break decision;
+ }
+ if (userId == UserHandle.USER_SYSTEM) {
+ // This will be only allowed if it has FLAG_SHOW_FOR_ALL_USERS.
+ // The flag is not immediately accessible here so skip the check.
+ // But other WM policy will enforce it.
+ break decision;
+ }
+ // Now user is a passenger.
+ if (mPassengerDisplays.isEmpty()) {
+ // No displays for passengers. This could be old user and do not do anything.
+ break decision;
+ }
+ if (targetDisplayArea == null) {
+ if (originalDisplayArea != null) {
+ targetDisplayArea = originalDisplayArea;
+ } else {
+ targetDisplayArea = getDefaultTaskDisplayAreaOnDisplay(Display.DEFAULT_DISPLAY);
+ }
+ }
+ Display display = targetDisplayArea.mDisplayContent.getDisplay();
+ if ((display.getFlags() & Display.FLAG_PRIVATE) != 0) {
+ // private display should follow its own restriction rule.
+ break decision;
+ }
+ if (display.getType() == Display.TYPE_VIRTUAL) {
+ // TODO(b/132903422) : We need to update this after the bug is resolved.
+ // For now, don't change anything.
+ break decision;
+ }
+ int userForDisplay = mDisplayToProfileUserMapping.get(display.getDisplayId(),
+ UserHandle.USER_NULL);
+ if (userForDisplay == userId) {
+ break decision;
+ }
+ targetDisplayArea = getAlternativeDisplayAreaForPassengerLocked(
+ userId, targetDisplayArea);
+ }
+ if (targetDisplayArea != null && originalDisplayArea != targetDisplayArea) {
+ Slog.i(TAG, "Changed launching display, user:" + userId
+ + " requested display area:" + originalDisplayArea
+ + " target display area:" + targetDisplayArea);
+ outParams.mPreferredTaskDisplayArea = targetDisplayArea;
+ return RESULT_DONE;
+ } else {
+ return RESULT_SKIP;
+ }
+ }
+
+ @Nullable
+ private TaskDisplayArea getAlternativeDisplayAreaForPassengerLocked(int userId,
+ TaskDisplayArea originalDisplayArea) {
+ int displayId = mDefaultDisplayForProfileUser.get(userId, Display.INVALID_DISPLAY);
+ if (displayId != Display.INVALID_DISPLAY) {
+ return getDefaultTaskDisplayAreaOnDisplay(displayId);
+ }
+ // return the 1st passenger display area if it exists
+ if (!mPassengerDisplays.isEmpty()) {
+ Slog.w(TAG, "No default display area for user:" + userId
+ + " reassign to 1st passenger display area");
+ return getDefaultTaskDisplayAreaOnDisplay(mPassengerDisplays.get(0));
+ }
+ Slog.w(TAG, "No default display for user:" + userId
+ + " and no passenger display, keep the requested display area:"
+ + originalDisplayArea);
+ return originalDisplayArea;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ TaskDisplayArea getDefaultTaskDisplayAreaOnDisplay(int displayId) {
+ DisplayContent dc = mAtm.mRootWindowContainer.getDisplayContentOrCreate(displayId);
+ if (dc == null) {
+ return null;
+ }
+ return dc.getDefaultTaskDisplayArea();
+ }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
index bb5e149..a609dd7 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -5,8 +5,8 @@ include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := tests
LOCAL_SRC_FILES := $(call all-java-files-under, src) \
- $(call all-java-files-under, ../src/com/android/internal/car) \
- $(call all-Iaidl-files-under, ../src/com/android/internal/car)
+ $(call all-java-files-under, ../src) \
+ $(call all-Iaidl-files-under, ../src)
LOCAL_PACKAGE_NAME := CarServicesTest
LOCAL_PRIVATE_PLATFORM_APIS := true
@@ -26,11 +26,19 @@ LOCAL_JAVA_LIBRARIES += \
services
LOCAL_STATIC_JAVA_LIBRARIES := \
- androidx.test.rules \
+ android.car.internal.event-log-tags \
+ android.car.test.utils \
android.car.userlib \
- junit \
- mockito-target-minus-junit4 \
+ android.car.watchdoglib \
+ androidx.test.ext.junit \
+ androidx.test.rules \
+ mockito-target-extended-minus-junit4 \
services \
truth-prebuilt
+# mockito-target-extended dependencies
+LOCAL_JNI_SHARED_LIBRARIES := \
+ libdexmakerjvmtiagent \
+ libstaticjvmtiagent \
+
include $(BUILD_PACKAGE) \ No newline at end of file
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 008655a..ff050df 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -22,7 +22,8 @@
android:targetPackage="com.android.internal.car"
android:label="Unit Tests for Car Framework Services"/>
- <application android:label="CarServicesTest">
+ <application android:label="CarServicesTest"
+ android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>
</manifest>
diff --git a/tests/src/com/android/internal/car/CarHelperServiceTest.java b/tests/src/com/android/internal/car/CarHelperServiceTest.java
index 3f95366..e20009d 100644
--- a/tests/src/com/android/internal/car/CarHelperServiceTest.java
+++ b/tests/src/com/android/internal/car/CarHelperServiceTest.java
@@ -16,116 +16,1217 @@
package com.android.internal.car;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
+import static android.car.test.util.UserTestingHelper.getDefaultUserType;
+import static android.car.test.util.UserTestingHelper.newGuestUser;
+import static android.car.test.util.UserTestingHelper.newSecondaryUser;
+import static android.car.test.util.UserTestingHelper.UserInfoBuilder;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+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.AdditionalAnswers.answerVoid;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.ArgumentMatchers.nullable;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.car.test.mocks.AbstractExtendedMockitoTestCase;
+import android.car.test.mocks.SyncAnswer;
import android.car.userlib.CarUserManagerHelper;
+import android.car.userlib.HalCallback;
+import android.car.userlib.CommonConstants.CarUserServiceConstants;
+import android.car.userlib.InitialUserSetter;
+import android.car.userlib.UserHalHelper;
+import android.car.watchdoglib.CarWatchdogDaemonHelper;
import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.hardware.automotive.vehicle.V2_0.UserFlags;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoRequestType;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponseAction;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+import android.sysprop.CarProperties;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.car.ExternalConstants.CarUserManagerConstants;
+import com.android.internal.car.ExternalConstants.ICarConstants;
+import com.android.internal.os.IResultReceiver;
import com.android.server.SystemService;
+import com.android.server.SystemService.TargetUser;
+import com.android.server.wm.CarLaunchParamsModifier;
+import com.android.server.utils.TimingsTraceAndSlog;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Captor;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.quality.Strictness;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Optional;
/**
* This class contains unit tests for the {@link CarServiceHelperService}.
- *
- * The following mocks are used:
- * <ol>
- * <li> {@link Context} provides system services and resources.
- * <li> {@link CarUserManagerHelper} provides user info and actions.
- * <ol/>
*/
@RunWith(AndroidJUnit4.class)
-public class CarHelperServiceTest {
- private static final String DEFAULT_NAME = "Driver";
- private CarServiceHelperService mCarServiceHelperService;
+public class CarHelperServiceTest extends AbstractExtendedMockitoTestCase {
+
+ private static final String TAG = CarHelperServiceTest.class.getSimpleName();
+
+ private static final int PRE_CREATED_USER_ID = 24;
+ private static final int PRE_CREATED_GUEST_ID = 25;
+ private static final int USER_MANAGER_TIMEOUT_MS = 100;
+
+ private static final String HAL_USER_NAME = "HAL 9000";
+ private static final int HAL_USER_ID = 42;
+ private static final int HAL_USER_FLAGS = 108;
+
+ private static final String USER_LOCALES = "LOL";
+
+ private static final int HAL_TIMEOUT_MS = 500;
+
+ private static final int ADDITIONAL_TIME_MS = 200;
+
+ private static final int HAL_NOT_REPLYING_TIMEOUT_MS = HAL_TIMEOUT_MS + ADDITIONAL_TIME_MS;
+
+ private static final long POST_HAL_NOT_REPLYING_TIMEOUT_MS = HAL_NOT_REPLYING_TIMEOUT_MS
+ + ADDITIONAL_TIME_MS;
+
+
+ // Spy used in tests that need to verify folloing method:
+ // managePreCreatedUsers, postAsyncPreCreatedUser, preCreateUsers
+ private CarServiceHelperService mHelper;
+
@Mock
private Context mMockContext;
-
+ @Mock
+ private PackageManager mPackageManager;
@Mock
private Context mApplicationContext;
-
@Mock
- private CarUserManagerHelper mCarUserManagerHelper;
+ private CarUserManagerHelper mUserManagerHelper;
+ @Mock
+ private UserManager mUserManager;
+ @Mock
+ private CarLaunchParamsModifier mCarLaunchParamsModifier;
+ @Mock
+ private CarWatchdogDaemonHelper mCarWatchdogDaemonHelper;
+ @Mock
+ private IBinder mICarBinder;
+ @Mock
+ private InitialUserSetter mInitialUserSetter;
+
+ @Captor
+ private ArgumentCaptor<Parcel> mBinderCallData;
+
+ private Exception mBinderCallException;
/**
* Initialize objects and setup testing environment.
*/
+ @Override
+ protected void onSessionBuilder(CustomMockitoSessionBuilder session) {
+ session
+ .spyStatic(CarProperties.class)
+ .spyStatic(UserManager.class);
+ }
+
@Before
- public void setUpMocks() throws Exception {
- MockitoAnnotations.initMocks(this);
- doReturn(mApplicationContext).when(mMockContext).getApplicationContext();
+ public void setUpMocks() {
+ mHelper = spy(new CarServiceHelperService(
+ mMockContext,
+ mUserManagerHelper,
+ mInitialUserSetter,
+ mUserManager,
+ mCarLaunchParamsModifier,
+ mCarWatchdogDaemonHelper,
+ /* halEnabled= */ true,
+ HAL_TIMEOUT_MS));
- mCarServiceHelperService = new CarServiceHelperService(mMockContext, mCarUserManagerHelper);
+ when(mMockContext.getPackageManager()).thenReturn(mPackageManager);
+ }
+
+ @Test
+ public void testCarServiceLaunched() throws Exception {
+ mockRegisterReceiver();
+ mockBindService();
+ mockLoadLibrary();
+
+ mHelper.onStart();
+
+ verifyBindService();
}
- /**
- * Test that the {@link CarServiceHelperService} starts up a secondary admin user
- * upon first run.
- */
@Test
- public void testStartsSecondaryAdminUserOnFirstRun() {
- UserInfo admin = mockAdminWithDefaultName(/* adminId= */ 10);
+ public void testHandleCarServiceCrash() throws Exception {
+ mockHandleCarServiceCrash();
+ mockCarServiceException();
- doReturn(new ArrayList<>()).when(mCarUserManagerHelper).getAllUsers();
- mCarServiceHelperService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+ mHelper.handleCarServiceConnection(mICarBinder);
- verify(mCarUserManagerHelper).createNewAdminUser();
- verify(mCarUserManagerHelper).switchToUserId(admin.id);
+ verify(mHelper).handleCarServiceCrash();
}
/**
- * Test that the {@link CarServiceHelperService} updates last active user to the first
- * admin user on first run.
+ * Test that the {@link CarServiceHelperService} starts up a secondary admin user upon first
+ * run.
*/
@Test
- public void testUpdateLastActiveUserOnFirstRun() {
- UserInfo admin = mockAdminWithDefaultName(/* adminId= */ 10);
+ public void testInitialInfo_noHal() throws Exception {
+ CarServiceHelperService halLessHelper = new CarServiceHelperService(
+ mMockContext,
+ mUserManagerHelper,
+ mInitialUserSetter,
+ mUserManager,
+ mCarLaunchParamsModifier,
+ mCarWatchdogDaemonHelper,
+ /* halEnabled= */ false,
+ HAL_TIMEOUT_MS);
+
+ halLessHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+ verifyDefaultBootBehavior();
+ }
+
+ @Test
+ public void testInitialInfo_halReturnedDefault() throws Exception {
+ bindMockICar();
+
+ expectICarGetInitialUserInfo(InitialUserInfoAction.DEFAULT);
+
+ mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+ assertNoICarCallExceptions();
+ verifyICarGetInitialUserInfoCalled();
+ assertThat(mHelper.getHalResponseTime()).isGreaterThan(0);
+
+ verifyDefaultBootBehavior();
+ }
+
+ @Test
+ public void testInitialInfo_halReturnedDefault_withLocale() throws Exception {
+ bindMockICar();
+
+ expectICarGetInitialUserInfo(InitialUserInfoAction.DEFAULT_WITH_LOCALE);
+
+ mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+ assertNoICarCallExceptions();
+ verifyICarGetInitialUserInfoCalled();
+ assertThat(mHelper.getHalResponseTime()).isGreaterThan(0);
+
+ verifyDefaultBootBehaviorWithLocale();
+ }
+
+ @Test
+ public void testInitialInfo_halServiceNeverReturned() throws Exception {
+ bindMockICar();
+
+ expectICarGetInitialUserInfo(InitialUserInfoAction.DO_NOT_REPLY);
+
+ mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+ sleep("before asserting DEFAULT behavior", POST_HAL_NOT_REPLYING_TIMEOUT_MS);
+
+ assertNoICarCallExceptions();
+ verifyICarGetInitialUserInfoCalled();
+ assertThat(mHelper.getHalResponseTime()).isLessThan(0);
+
+ verifyDefaultBootBehavior();
+ }
+
+ @Test
+ public void testInitialInfo_halServiceReturnedTooLate() throws Exception {
+ bindMockICar();
+
+ expectICarGetInitialUserInfo(InitialUserInfoAction.DELAYED_REPLY);
+
+ mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+ sleep("before asserting DEFAULT behavior", POST_HAL_NOT_REPLYING_TIMEOUT_MS);
+
+ assertNoICarCallExceptions();
+ verifyICarGetInitialUserInfoCalled();
+ assertThat(mHelper.getHalResponseTime()).isGreaterThan(0);
+
+ sleep("to make sure not called again", POST_HAL_NOT_REPLYING_TIMEOUT_MS);
+
+ verifyDefaultBootBehavior();
+ }
+
+ @Test
+ public void testInitialInfo_halReturnedNonOkResultCode() throws Exception {
+ bindMockICar();
+
+ expectICarGetInitialUserInfo(InitialUserInfoAction.NON_OK_RESULT_CODE);
+
+ mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+ assertNoICarCallExceptions();
+ verifyICarGetInitialUserInfoCalled();
+ assertThat(mHelper.getHalResponseTime()).isGreaterThan(0);
+
+ verifyDefaultBootBehavior();
+ }
+
+ @Test
+ public void testInitialInfo_halReturnedOkWithNoBundle() throws Exception {
+ bindMockICar();
+
+ expectICarGetInitialUserInfo(InitialUserInfoAction.NULL_BUNDLE);
+
+ mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+ assertNoICarCallExceptions();
+ verifyICarGetInitialUserInfoCalled();
+ assertThat(mHelper.getHalResponseTime()).isGreaterThan(0);
+
+ verifyDefaultBootBehavior();
+ }
+
+ @Test
+ public void testInitialInfo_halReturnedSwitch_ok() throws Exception {
+ bindMockICar();
+
+ expectICarGetInitialUserInfo(InitialUserInfoAction.SWITCH_OK);
+ mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+ assertNoICarCallExceptions();
+ verifyICarGetInitialUserInfoCalled();
+ assertThat(mHelper.getHalResponseTime()).isGreaterThan(0);
+
+ verifyUserSwitchedByHal();
+ }
+
+ @Test
+ public void testInitialInfo_halReturnedSwitch_ok_withLocale() throws Exception {
+ bindMockICar();
+
+ expectICarGetInitialUserInfo(InitialUserInfoAction.SWITCH_OK_WITH_LOCALE);
+ mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
- mCarServiceHelperService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+ assertNoICarCallExceptions();
+ verifyICarGetInitialUserInfoCalled();
+ assertThat(mHelper.getHalResponseTime()).isGreaterThan(0);
- verify(mCarUserManagerHelper).setLastActiveUser(admin.id);
+ verifyUserSwitchedByHalWithLocale();
+ }
+
+ @Test
+ public void testInitialInfo_halReturnedSwitch_switchMissingUserId() throws Exception {
+ bindMockICar();
+
+ expectICarGetInitialUserInfo(InitialUserInfoAction.SWITCH_MISSING_USER_ID);
+
+ mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+ assertNoICarCallExceptions();
+ verifyICarGetInitialUserInfoCalled();
+ assertThat(mHelper.getHalResponseTime()).isGreaterThan(0);
+
+ verifyUserNotSwitchedByHal();
+ verifyDefaultBootBehavior();
+ }
+
+ @Test
+ public void testInitialInfo_halReturnedCreateOk() throws Exception {
+ bindMockICar();
+
+ expectICarGetInitialUserInfo((r) -> sendCreateDefaultHalUserAction(r));
+
+ mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+ assertNoICarCallExceptions();
+ verifyICarGetInitialUserInfoCalled();
+ assertThat(mHelper.getHalResponseTime()).isGreaterThan(0);
+
+ verifyUserCreatedByHal();
+ }
+
+ @Test
+ public void testInitialInfo_halReturnedCreateOk_withLocale() throws Exception {
+ bindMockICar();
+
+ expectICarGetInitialUserInfo(
+ (r) -> sendCreateAction(r, HAL_USER_NAME, HAL_USER_FLAGS, USER_LOCALES));
+
+ mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+ assertNoICarCallExceptions();
+ verifyICarGetInitialUserInfoCalled();
+ assertThat(mHelper.getHalResponseTime()).isGreaterThan(0);
+
+ verifyUserCreatedByHalWithLocale();
+ }
+
+ @Test
+ public void testOnUserStarting_notifiesICar() throws Exception {
+ bindMockICar();
+
+ int userId = 10;
+ expectICarOnUserLifecycleEvent(CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_STARTING,
+ userId);
+
+ mHelper.onUserStarting(newTargetUser(userId));
+
+ assertNoICarCallExceptions();
+ verifyICarOnUserLifecycleEventCalled();
+ }
+
+ @Test
+ public void testOnUserStarting_preCreatedDoesntNotifyICar() throws Exception {
+ bindMockICar();
+
+ mHelper.onUserStarting(newTargetUser(10, /* preCreated= */ true));
+
+ verifyICarOnUserLifecycleEventNeverCalled();
+ }
+
+ @Test
+ public void testOnUserSwitching_notifiesICar() throws Exception {
+ bindMockICar();
+
+ int currentUserId = 10;
+ int targetUserId = 11;
+ expectICarOnUserLifecycleEvent(CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_SWITCHING,
+ currentUserId, targetUserId);
+
+ mHelper.onUserSwitching(newTargetUser(currentUserId),
+ newTargetUser(targetUserId));
+
+ assertNoICarCallExceptions();
+ }
+
+ @Test
+ public void testOnUserSwitching_preCreatedDoesntNotifyICar() throws Exception {
+ bindMockICar();
+
+ mHelper.onUserSwitching(newTargetUser(10), newTargetUser(11, /* preCreated= */ true));
+
+ verifyICarOnUserLifecycleEventNeverCalled();
+ }
+
+ @Test
+ public void testOnUserUnlocking_notifiesICar() throws Exception {
+ bindMockICar();
+
+ int userId = 10;
+ expectICarOnUserLifecycleEvent(CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKING,
+ userId);
+
+ mHelper.onUserUnlocking(newTargetUser(userId));
+
+ assertNoICarCallExceptions();
+ }
+
+ @Test
+ public void testOnUserUnlocking_preCreatedDoesntNotifyICar() throws Exception {
+ bindMockICar();
+
+ mHelper.onUserUnlocking(newTargetUser(10, /* preCreated= */ true));
+
+ verifyICarOnUserLifecycleEventNeverCalled();
+ }
+
+ @Test
+ public void testOnUserUnlocked_notifiesICar_systemUserFirst() throws Exception {
+ bindMockICar();
+
+ int systemUserId = UserHandle.USER_SYSTEM;
+ expectICarOnUserLifecycleEvent(CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED,
+ systemUserId);
+
+ int firstUserId = 10;
+ expectICarFirstUserUnlocked(firstUserId);
+
+ setHalResponseTime();
+ mHelper.onUserUnlocked(newTargetUser(systemUserId));
+ mHelper.onUserUnlocked(newTargetUser(firstUserId));
+
+ assertNoICarCallExceptions();
+
+ verifyICarOnUserLifecycleEventCalled(); // system user
+ verifyICarFirstUserUnlockedCalled(); // first user
+ }
+
+ @Test
+ public void testOnUserUnlocked_notifiesICar_firstUserReportedJustOnce() throws Exception {
+ bindMockICar();
+
+ int firstUserId = 10;
+ expectICarFirstUserUnlocked(firstUserId);
+
+ int secondUserId = 11;
+ expectICarOnUserLifecycleEvent(CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED,
+ secondUserId);
+
+ setHalResponseTime();
+ mHelper.onUserUnlocked(newTargetUser(firstUserId));
+ mHelper.onUserUnlocked(newTargetUser(secondUserId));
+
+ assertNoICarCallExceptions();
+
+ verifyICarFirstUserUnlockedCalled(); // first user
+ verifyICarOnUserLifecycleEventCalled(); // second user
+ }
+
+ @Test
+ public void testOnUserStopping_notifiesICar() throws Exception {
+ bindMockICar();
+
+ int userId = 10;
+ expectICarOnUserLifecycleEvent(CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPING,
+ userId);
+
+ mHelper.onUserStopping(newTargetUser(userId));
+
+ assertNoICarCallExceptions();
+ }
+
+ @Test
+ public void testOnUserStopping_preCreatedDoesntNotifyICar() throws Exception {
+ bindMockICar();
+
+ mHelper.onUserStopping(newTargetUser(10, /* preCreated= */ true));
+
+ verifyICarOnUserLifecycleEventNeverCalled();
+ }
+
+ @Test
+ public void testOnUserStopped_notifiesICar() throws Exception {
+ bindMockICar();
+
+ int userId = 10;
+ expectICarOnUserLifecycleEvent(CarUserManagerConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPED,
+ userId);
+
+ mHelper.onUserStopped(newTargetUser(userId));
+
+ assertNoICarCallExceptions();
+ }
+
+ @Test
+ public void testOnUserStopped_preCreatedDoesntNotifyICar() throws Exception {
+ bindMockICar();
+
+ mHelper.onUserStopped(newTargetUser(10, /* preCreated= */ true));
+
+ verifyICarOnUserLifecycleEventNeverCalled();
+ }
+
+ @Test
+ public void testSendSetInitialUserInfoNotifiesICar() throws Exception {
+ bindMockICar();
+
+ UserInfo user = new UserInfo(42, "Dude", UserInfo.FLAG_ADMIN);
+ expectICarSetInitialUserInfo(user);
+
+ mHelper.setInitialUser(user);
+
+ verifyICarSetInitialUserCalled();
+ assertNoICarCallExceptions();
+ }
+
+ @Test
+ public void testInitialUserInfoRequestType_FirstBoot() throws Exception {
+ when(mUserManagerHelper.hasInitialUser()).thenReturn(false);
+ when(mPackageManager.isDeviceUpgrading()).thenReturn(true);
+ assertThat(mHelper.getInitialUserInfoRequestType())
+ .isEqualTo(InitialUserInfoRequestType.FIRST_BOOT);
+ }
+
+ @Test
+ public void testInitialUserInfoRequestType_FirstBootAfterOTA() throws Exception {
+ when(mUserManagerHelper.hasInitialUser()).thenReturn(true);
+ when(mPackageManager.isDeviceUpgrading()).thenReturn(true);
+ assertThat(mHelper.getInitialUserInfoRequestType())
+ .isEqualTo(InitialUserInfoRequestType.FIRST_BOOT_AFTER_OTA);
+ }
+
+ @Test
+ public void testInitialUserInfoRequestType_ColdBoot() throws Exception {
+ when(mUserManagerHelper.hasInitialUser()).thenReturn(true);
+ when(mPackageManager.isDeviceUpgrading()).thenReturn(false);
+ assertThat(mHelper.getInitialUserInfoRequestType())
+ .isEqualTo(InitialUserInfoRequestType.COLD_BOOT);
+ }
+
+ @Test
+ public void testPreCreatedUsersLessThanRequested() throws Exception {
+ // Set existing user
+ expectNoPreCreatedUser();
+ // Set number of requested user
+ setNumberRequestedUsersProperty(1);
+ setNumberRequestedGuestsProperty(0);
+ mockRunAsync();
+ SyncAnswer syncUserInfo = mockPreCreateUser(/* isGuest= */ false);
+
+ mHelper.managePreCreatedUsers();
+ syncUserInfo.await(USER_MANAGER_TIMEOUT_MS);
+
+ verifyUserCreated(/* isGuest= */ false);
+ }
+
+ @Test
+ public void testPreCreatedGuestsLessThanRequested() throws Exception {
+ // Set existing user
+ expectNoPreCreatedUser();
+ // Set number of requested user
+ setNumberRequestedUsersProperty(0);
+ setNumberRequestedGuestsProperty(1);
+ mockRunAsync();
+ SyncAnswer syncUserInfo = mockPreCreateUser(/* isGuest= */ true);
+
+ mHelper.managePreCreatedUsers();
+ syncUserInfo.await(USER_MANAGER_TIMEOUT_MS);
+
+ verifyUserCreated(/* isGuest= */ true);
+ }
+
+ @Test
+ public void testRemovePreCreatedUser() throws Exception {
+ UserInfo user = expectPreCreatedUser(/* isGuest= */ false,
+ /* isInitialized= */ true);
+ setNumberRequestedUsersProperty(0);
+ setNumberRequestedGuestsProperty(0);
+ mockRunAsync();
+
+ SyncAnswer syncRemoveStatus = mockRemoveUser(PRE_CREATED_USER_ID);
+
+ mHelper.managePreCreatedUsers();
+ syncRemoveStatus.await(USER_MANAGER_TIMEOUT_MS);
+
+ verifyUserRemoved(user);
+ }
+
+ @Test
+ public void testRemovePreCreatedGuest() throws Exception {
+ UserInfo user = expectPreCreatedUser(/* isGuest= */ true,
+ /* isInitialized= */ true);
+ setNumberRequestedUsersProperty(0);
+ setNumberRequestedGuestsProperty(0);
+ mockRunAsync();
+ SyncAnswer syncRemoveStatus = mockRemoveUser(PRE_CREATED_GUEST_ID);
+
+ mHelper.managePreCreatedUsers();
+ syncRemoveStatus.await(USER_MANAGER_TIMEOUT_MS);
+
+ verifyUserRemoved(user);
+ }
+
+ @Test
+ public void testRemoveInvalidPreCreatedUser() throws Exception {
+ UserInfo user = expectPreCreatedUser(/* isGuest= */ false,
+ /* isInitialized= */ false);
+ setNumberRequestedUsersProperty(0);
+ setNumberRequestedGuestsProperty(0);
+ mockRunAsync();
+ SyncAnswer syncRemoveStatus = mockRemoveUser(PRE_CREATED_USER_ID);
+
+ mHelper.managePreCreatedUsers();
+ syncRemoveStatus.await(ADDITIONAL_TIME_MS);
+
+ verifyUserRemoved(user);
+ }
+
+ @Test
+ public void testManagePreCreatedUsersDoNothing() throws Exception {
+ UserInfo user = expectPreCreatedUser(/* isGuest= */ false,
+ /* isInitialized= */ true);
+ setNumberRequestedUsersProperty(1);
+ setNumberRequestedGuestsProperty(0);
+ mockPreCreateUser(/* isGuest= */ false);
+ mockRemoveUser(PRE_CREATED_USER_ID);
+
+ mHelper.managePreCreatedUsers();
+
+ verifyPostPreCreatedUserSkipped();
+ }
+
+ @Test
+ public void testManagePreCreatedUsersOnBootCompleted() throws Exception {
+ mockRunAsync();
+
+ mHelper.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+
+ verifyManagePreCreatedUsers();
+ }
+
+ @Test
+ public void testPreCreateUserExceptionLogged() throws Exception {
+ SyncAnswer syncException = mockPreCreateUserException();
+ TimingsTraceAndSlog trace = new TimingsTraceAndSlog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
+ mHelper.preCreateUsers(trace, false);
+
+ verifyPostPreCreatedUserException();
+ assertThat(trace.getUnfinishedTracesForDebug()).isEmpty();
+ }
+
+ private void setHalResponseTime() {
+ mHelper.setInitialHalResponseTime();
+ SystemClock.sleep(1); // must sleep at least 1ms so it's not 0
+ mHelper.setFinalHalResponseTime();
}
/**
- * Test that the {@link CarServiceHelperService} starts up the last active user on reboot.
+ * Used in cases where the result of calling HAL for the initial info should be the same as
+ * not using HAL.
*/
- @Test
- public void testStartsLastActiveUserOnReboot() {
- List<UserInfo> users = new ArrayList<>();
+ private void verifyDefaultBootBehavior() throws Exception {
+ verify(mInitialUserSetter).set(argThat((info) -> {
+ return info.type == InitialUserSetter.TYPE_DEFAULT_BEHAVIOR && info.userLocales == null;
+ }));
+ }
+
+ private void verifyDefaultBootBehaviorWithLocale() {
+ verify(mInitialUserSetter).set(argThat((info) -> {
+ return info.type == InitialUserSetter.TYPE_DEFAULT_BEHAVIOR
+ && USER_LOCALES.equals(info.userLocales);
+ }));
+ }
+
+ private TargetUser newTargetUser(int userId) {
+ return newTargetUser(userId, /* preCreated= */ false);
+ }
+
+ private TargetUser newTargetUser(int userId, boolean preCreated) {
+ TargetUser targetUser = mock(TargetUser.class);
+ when(targetUser.getUserIdentifier()).thenReturn(userId);
+ UserInfo userInfo = new UserInfo();
+ userInfo.id = userId;
+ userInfo.preCreated = preCreated;
+ when(targetUser.getUserInfo()).thenReturn(userInfo);
+ return targetUser;
+ }
+
+ private void bindMockICar() throws Exception {
+ // Must set the binder expectation, otherwise checks for other transactions would fail
+ expectICarSetCarServiceHelper();
+ mHelper.handleCarServiceConnection(mICarBinder);
+ }
+
+ private void verifyUserCreatedByHal() throws Exception {
+ verify(mInitialUserSetter).set(argThat((info) -> {
+ return info.type == InitialUserSetter.TYPE_CREATE
+ && info.newUserName == HAL_USER_NAME
+ && info.newUserFlags == HAL_USER_FLAGS
+ && info.userLocales == null;
+ }));
+ }
+
+ private void verifyUserCreatedByHalWithLocale() throws Exception {
+ verify(mInitialUserSetter).set(argThat((info) -> {
+ return info.type == InitialUserSetter.TYPE_CREATE
+ && info.newUserName == HAL_USER_NAME
+ && info.newUserFlags == HAL_USER_FLAGS
+ && info.userLocales == USER_LOCALES;
+ }));
+ }
+
+ private void verifyUserSwitchedByHal() {
+ verify(mInitialUserSetter).set(argThat((info) -> {
+ return info.type == InitialUserSetter.TYPE_SWITCH
+ && info.switchUserId == HAL_USER_ID
+ && info.userLocales == null;
+ }));
+ }
+
+ private void verifyUserSwitchedByHalWithLocale() {
+ verify(mInitialUserSetter).set(argThat((info) -> {
+ return info.type == InitialUserSetter.TYPE_SWITCH
+ && info.switchUserId == HAL_USER_ID
+ && info.userLocales == USER_LOCALES;
+ }));
+ }
+
+ private void verifyUserNotSwitchedByHal() {
+ verify(mInitialUserSetter, never()).set(argThat((info) -> {
+ return info.type == InitialUserSetter.TYPE_SWITCH;
+ }));
+ }
+
+ private void verifyBindService () throws Exception {
+ verify(mMockContext).bindServiceAsUser(
+ argThat(intent -> intent.getAction().equals(ICarConstants.CAR_SERVICE_INTERFACE)),
+ any(), eq(Context.BIND_AUTO_CREATE), eq(UserHandle.SYSTEM));
+ }
+
+ private void verifyHandleCarServiceCrash() throws Exception {
+ verify(mHelper).handleCarServiceCrash();
+ }
+
+ private void mockRegisterReceiver() {
+ when(mMockContext.registerReceiverForAllUsers(any(), any(), any(), any()))
+ .thenReturn(new Intent());
+ }
+
+ private void mockBindService() {
+ when(mMockContext.bindServiceAsUser(any(), any(),
+ eq(Context.BIND_AUTO_CREATE), eq(UserHandle.SYSTEM)))
+ .thenReturn(true);
+ }
+
+ private void mockLoadLibrary() {
+ doNothing().when(mHelper).loadNativeLibrary();
+ }
+
+ private void mockCarServiceException() throws Exception {
+ when(mICarBinder.transact(anyInt(), notNull(), isNull(), eq(Binder.FLAG_ONEWAY)))
+ .thenThrow(new RemoteException("mock car service Crash"));
+ }
+
+ private void mockHandleCarServiceCrash() throws Exception {
+ doNothing().when(mHelper).handleCarServiceCrash();
+ }
+
+ // TODO: create a custom matcher / verifier for binder calls
+ private void expectICarOnUserLifecycleEvent(int eventType, int expectedUserId)
+ throws Exception {
+ expectICarOnUserLifecycleEvent(eventType, UserHandle.USER_NULL, expectedUserId);
+ }
+
+ private void expectICarSetCarServiceHelper() throws Exception {
+ int txn = IBinder.FIRST_CALL_TRANSACTION
+ + ICarConstants.ICAR_CALL_SET_CAR_SERVICE_HELPER;
+ when(mICarBinder.transact(eq(txn), notNull(), isNull(), eq(Binder.FLAG_ONEWAY)))
+ .thenReturn(true);
+ }
- int adminUserId = 10;
- UserInfo admin =
- new UserInfo(adminUserId, DEFAULT_NAME, UserInfo.FLAG_ADMIN);
+ private void expectICarOnUserLifecycleEvent(int expectedEventType, int expectedFromUserId,
+ int expectedToUserId) throws Exception {
+ int txn = IBinder.FIRST_CALL_TRANSACTION + ICarConstants.ICAR_CALL_ON_USER_LIFECYCLE;
+ long before = System.currentTimeMillis();
- int secUserId = 11;
- UserInfo secUser =
- new UserInfo(secUserId, DEFAULT_NAME, UserInfo.FLAG_ADMIN);
+ when(mICarBinder.transact(eq(txn), notNull(), isNull(),
+ eq(Binder.FLAG_ONEWAY))).thenAnswer((invocation) -> {
+ try {
+ long after = System.currentTimeMillis();
+ Log.d(TAG, "Answering txn " + txn);
+ Parcel data = (Parcel) invocation.getArguments()[1];
+ data.setDataPosition(0);
+ data.enforceInterface(ICarConstants.CAR_SERVICE_INTERFACE);
+ int actualEventType = data.readInt();
+ long actualTimestamp = data.readLong();
+ int actualFromUserId = data.readInt();
+ int actualToUserId = data.readInt();
+ Log.d(TAG, "Unmarshalled data: eventType=" + actualEventType
+ + ", timestamp= " + actualTimestamp
+ + ", fromUserId= " + actualFromUserId
+ + ", toUserId= " + actualToUserId);
+ List<String> errors = new ArrayList<>();
+ assertParcelValueInRange(errors, "timestamp", before, actualTimestamp, after);
+ assertParcelValue(errors, "eventType", expectedEventType, actualEventType);
+ assertParcelValue(errors, "fromUserId", expectedFromUserId,
+ actualFromUserId);
+ assertParcelValue(errors, "toUserId", expectedToUserId, actualToUserId);
+ assertNoParcelErrors(errors);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Exception answering binder call", e);
+ mBinderCallException = e;
+ return false;
+ }
+ });
+ }
+
+ private void expectICarFirstUserUnlocked(int expectedUserId) throws Exception {
+ int txn = IBinder.FIRST_CALL_TRANSACTION + ICarConstants.ICAR_CALL_FIRST_USER_UNLOCKED;
+ long before = System.currentTimeMillis();
+ long minDuration = SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime();
- users.add(admin);
- users.add(secUser);
+ when(mICarBinder.transact(eq(txn), notNull(), isNull(),
+ eq(Binder.FLAG_ONEWAY))).thenAnswer((invocation) -> {
+ try {
+ long after = System.currentTimeMillis();
+ Log.d(TAG, "Answering txn " + txn);
+ Parcel data = (Parcel) invocation.getArguments()[1];
+ data.setDataPosition(0);
+ data.enforceInterface(ICarConstants.CAR_SERVICE_INTERFACE);
+ int actualUserId = data.readInt();
+ long actualTimestamp = data.readLong();
+ long actualDuration = data.readLong();
+ int actualHalResponseTime = data.readInt();
+ Log.d(TAG, "Unmarshalled data: userId= " + actualUserId
+ + ", timestamp= " + actualTimestamp
+ + ", duration=" + actualDuration
+ + ", halResponseTime=" + actualHalResponseTime);
+ List<String> errors = new ArrayList<>();
+ assertParcelValue(errors, "userId", expectedUserId, actualUserId);
+ assertParcelValueInRange(errors, "timestamp", before, actualTimestamp,
+ after);
+ assertMinimumParcelValue(errors, "duration", minDuration, actualDuration);
+ assertMinimumParcelValue(errors, "halResponseTime", 1, actualHalResponseTime);
+ assertNoParcelErrors(errors);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Exception answering binder call", e);
+ mBinderCallException = e;
+ return false;
+ }
+ });
- doReturn(users).when(mCarUserManagerHelper).getAllUsers();
- doReturn(secUserId).when(mCarUserManagerHelper).getInitialUser();
+ }
+
+ enum InitialUserInfoAction {
+ DEFAULT,
+ DEFAULT_WITH_LOCALE,
+ DO_NOT_REPLY,
+ DELAYED_REPLY,
+ NON_OK_RESULT_CODE,
+ NULL_BUNDLE,
+ SWITCH_OK,
+ SWITCH_OK_WITH_LOCALE,
+ SWITCH_MISSING_USER_ID
+ }
+
+ private void expectICarGetInitialUserInfo(InitialUserInfoAction action) throws Exception {
+ expectICarGetInitialUserInfo((receiver) ->{
+ switch (action) {
+ case DEFAULT:
+ sendDefaultAction(receiver);
+ break;
+ case DEFAULT_WITH_LOCALE:
+ sendDefaultAction(receiver, USER_LOCALES);
+ break;
+ case DO_NOT_REPLY:
+ Log.d(TAG, "NOT replying to bind call");
+ break;
+ case DELAYED_REPLY:
+ sleep("before sending result", HAL_NOT_REPLYING_TIMEOUT_MS);
+ sendDefaultAction(receiver);
+ break;
+ case NON_OK_RESULT_CODE:
+ Log.d(TAG, "sending bad result code");
+ receiver.send(-1, null);
+ break;
+ case NULL_BUNDLE:
+ Log.d(TAG, "sending OK without bundle");
+ receiver.send(HalCallback.STATUS_OK, null);
+ break;
+ case SWITCH_OK:
+ sendValidSwitchAction(receiver, /* userLocales= */ null);
+ break;
+ case SWITCH_OK_WITH_LOCALE:
+ sendValidSwitchAction(receiver, USER_LOCALES);
+ break;
+ case SWITCH_MISSING_USER_ID:
+ Log.d(TAG, "sending Switch without user Id");
+ sendSwitchAction(receiver, /* id= */ null, /* userLocales= */ null);
+ break;
+ default:
+ throw new IllegalArgumentException("invalid action: " + action);
+ }
+ });
+ }
+
+ private void expectICarGetInitialUserInfo(GetInitialUserInfoAction action) throws Exception {
+ int txn = IBinder.FIRST_CALL_TRANSACTION + ICarConstants.ICAR_CALL_GET_INITIAL_USER_INFO;
+ when(mICarBinder.transact(eq(txn), notNull(), isNull(),
+ eq(Binder.FLAG_ONEWAY))).thenAnswer((invocation) -> {
+ try {
+ Log.d(TAG, "Answering txn " + txn);
+ Parcel data = (Parcel) invocation.getArguments()[1];
+ data.setDataPosition(0);
+ data.enforceInterface(ICarConstants.CAR_SERVICE_INTERFACE);
+ int actualRequestType = data.readInt();
+ int actualTimeoutMs = data.readInt();
+ IResultReceiver receiver = IResultReceiver.Stub
+ .asInterface(data.readStrongBinder());
+
+ Log.d(TAG, "Unmarshalled data: requestType= " + actualRequestType
+ + ", timeout=" + actualTimeoutMs
+ + ", receiver =" + receiver);
+ action.onReceiver(receiver);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Exception answering binder call", e);
+ mBinderCallException = e;
+ return false;
+ }
+ });
+ }
+
+ private void expectICarSetInitialUserInfo(UserInfo user) throws RemoteException {
+ int txn = IBinder.FIRST_CALL_TRANSACTION + ICarConstants.ICAR_CALL_SET_INITIAL_USER;
+ when(mICarBinder.transact(eq(txn), notNull(), isNull(),
+ eq(Binder.FLAG_ONEWAY))).thenAnswer((invocation) -> {
+ try {
+ Log.d(TAG, "Answering txn " + txn);
+ Parcel data = (Parcel) invocation.getArguments()[1];
+ data.setDataPosition(0);
+ data.enforceInterface(ICarConstants.CAR_SERVICE_INTERFACE);
+ int actualUserId = data.readInt();
+ Log.d(TAG, "Unmarshalled data: user= " + actualUserId);
+ assertThat(actualUserId).isEqualTo(user.id);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Exception answering binder call", e);
+ mBinderCallException = e;
+ return false;
+ }
+ });
+ }
+
+ private void expectNoPreCreatedUser() throws Exception {
+ when(mUserManager.getUsers(/* excludePartial= */ true,
+ /* excludeDying= */ true, /* excludePreCreated= */ false))
+ .thenReturn(new ArrayList<UserInfo> ());
+ }
- mCarServiceHelperService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+ private UserInfo expectPreCreatedUser(boolean isGuest, boolean isInitialized)
+ throws Exception {
+ int userId = isGuest ? PRE_CREATED_GUEST_ID : PRE_CREATED_USER_ID;
+ UserInfo user = new UserInfoBuilder(userId)
+ .setGuest(isGuest)
+ .setPreCreated(true)
+ .setInitialized(isInitialized)
+ .build();
+
+ when(mUserManager.getUsers(/* excludePartial= */ true,
+ /* excludeDying= */ true, /* excludePreCreated= */ false))
+ .thenReturn(Arrays.asList(user));
+ return user;
+ }
+
+ private interface GetInitialUserInfoAction {
+ void onReceiver(IResultReceiver receiver) throws Exception;
+ }
+
+ private void sendDefaultAction(IResultReceiver receiver) throws Exception {
+ sendDefaultAction(receiver, /* userLocales= */ null);
+ }
+
+ private void sendDefaultAction(IResultReceiver receiver, String userLocales) throws Exception {
+ Log.d(TAG, "Sending DEFAULT action to receiver " + receiver);
+ Bundle data = new Bundle();
+ data.putInt(CarUserServiceConstants.BUNDLE_INITIAL_INFO_ACTION,
+ InitialUserInfoResponseAction.DEFAULT);
+ if (userLocales != null) {
+ data.putString(CarUserServiceConstants.BUNDLE_USER_LOCALES, userLocales);
+ }
+ receiver.send(HalCallback.STATUS_OK, data);
+ }
+
+ private void sendValidSwitchAction(IResultReceiver receiver, String userLocales)
+ throws Exception {
+ Log.d(TAG, "Sending SWITCH (" + HAL_USER_ID + ") action to receiver " + receiver);
+ sendSwitchAction(receiver, HAL_USER_ID, userLocales);
+ }
+
+ private void sendSwitchAction(IResultReceiver receiver, Integer id, String userLocales)
+ throws Exception {
+ Bundle data = new Bundle();
+ data.putInt(CarUserServiceConstants.BUNDLE_INITIAL_INFO_ACTION,
+ InitialUserInfoResponseAction.SWITCH);
+ if (id != null) {
+ data.putInt(CarUserServiceConstants.BUNDLE_USER_ID, id);
+ }
+ if (userLocales != null) {
+ data.putString(CarUserServiceConstants.BUNDLE_USER_LOCALES, userLocales);
+ }
+ receiver.send(HalCallback.STATUS_OK, data);
+ }
+
+ private void sendCreateDefaultHalUserAction(IResultReceiver receiver) throws Exception {
+ sendCreateAction(receiver, HAL_USER_NAME, HAL_USER_FLAGS, /* userLocales= */ null);
+ }
+
+ private void sendCreateAction(IResultReceiver receiver, String name, Integer flags,
+ String userLocales) throws Exception {
+ Bundle data = new Bundle();
+ data.putInt(CarUserServiceConstants.BUNDLE_INITIAL_INFO_ACTION,
+ InitialUserInfoResponseAction.CREATE);
+ if (name != null) {
+ data.putString(CarUserServiceConstants.BUNDLE_USER_NAME, name);
+ }
+ if (flags != null) {
+ data.putInt(CarUserServiceConstants.BUNDLE_USER_FLAGS, flags);
+ }
+ if (userLocales != null) {
+ data.putString(CarUserServiceConstants.BUNDLE_USER_LOCALES, userLocales);
+ }
+ receiver.send(HalCallback.STATUS_OK, data);
+ }
+
+ private void sleep(String reason, long napTimeMs) {
+ Log.d(TAG, "Sleeping for " + napTimeMs + "ms: " + reason);
+ SystemClock.sleep(napTimeMs);
+ Log.d(TAG, "Woke up (from '" + reason + "')");
+ }
+
+ private void verifyICarOnUserLifecycleEventCalled() throws Exception {
+ verifyICarTxnCalled(ICarConstants.ICAR_CALL_ON_USER_LIFECYCLE);
+ }
+
+ private void verifyICarOnUserLifecycleEventNeverCalled() throws Exception {
+ verifyICarTxnNeverCalled(ICarConstants.ICAR_CALL_ON_USER_LIFECYCLE);
+ }
- verify(mCarUserManagerHelper).switchToUserId(secUserId);
+ private void verifyICarFirstUserUnlockedCalled() throws Exception {
+ verifyICarTxnCalled(ICarConstants.ICAR_CALL_FIRST_USER_UNLOCKED);
}
- private UserInfo mockAdminWithDefaultName(int adminId) {
- UserInfo admin = new UserInfo(adminId, DEFAULT_NAME, UserInfo.FLAG_ADMIN);
- doReturn(admin).when(mCarUserManagerHelper).createNewAdminUser();
- return admin;
+ private void verifyICarGetInitialUserInfoCalled() throws Exception {
+ verifyICarTxnCalled(ICarConstants.ICAR_CALL_GET_INITIAL_USER_INFO);
+ }
+
+ private void verifyICarSetInitialUserCalled() throws Exception {
+ verifyICarTxnCalled(ICarConstants.ICAR_CALL_SET_INITIAL_USER);
+ }
+
+ private void verifyICarTxnCalled(int txnId) throws Exception {
+ int txn = IBinder.FIRST_CALL_TRANSACTION + txnId;
+ verify(mICarBinder).transact(eq(txn), notNull(), isNull(), eq(Binder.FLAG_ONEWAY));
+ }
+
+ private void verifyICarTxnNeverCalled(int txnId) throws Exception {
+ int txn = IBinder.FIRST_CALL_TRANSACTION + txnId;
+ verify(mICarBinder, never()).transact(eq(txn), notNull(), isNull(), eq(Binder.FLAG_ONEWAY));
+ }
+
+ private void setNumberRequestedUsersProperty(int numberUser) {
+ doReturn(Optional.of(numberUser)).when(() -> CarProperties.number_pre_created_users());
+ }
+
+ private void setNumberRequestedGuestsProperty(int numberGuest) {
+ doReturn(Optional.of(numberGuest)).when(() -> CarProperties.number_pre_created_guests());
+ }
+
+ private void mockRunAsync() {
+ doAnswer(answerVoid(Runnable::run)).when(mHelper).runAsync(any(Runnable.class));
+ }
+
+ private SyncAnswer mockPreCreateUser(boolean isGuest) {
+ UserInfo newUser = isGuest ? newGuestUser(PRE_CREATED_GUEST_ID) :
+ newSecondaryUser(PRE_CREATED_USER_ID);
+ SyncAnswer<UserInfo> syncUserInfo = SyncAnswer.forReturn(newUser);
+ when(mUserManager.preCreateUser(getDefaultUserType(isGuest)))
+ .thenAnswer(syncUserInfo);
+
+ return syncUserInfo;
+ }
+
+ private SyncAnswer mockRemoveUser(@UserIdInt int userId) {
+ SyncAnswer<Boolean> syncRemoveStatus = SyncAnswer.forReturn(true);
+ when(mUserManager.removeUser(userId)).thenAnswer(syncRemoveStatus);
+
+ return syncRemoveStatus;
+ }
+
+ private SyncAnswer mockPreCreateUserException() {
+ SyncAnswer<UserInfo> syncException = SyncAnswer.forException(new Exception());
+ when(mUserManager.preCreateUser(anyString()))
+ .thenAnswer(syncException);
+ return syncException;
+ }
+
+ private void verifyUserCreated(boolean isGuest) throws Exception {
+ String userType =
+ isGuest ? UserManager.USER_TYPE_FULL_GUEST : UserManager.USER_TYPE_FULL_SECONDARY;
+ verify(mUserManager).preCreateUser(eq(userType));
+ }
+
+ private void verifyUserRemoved(UserInfo user) throws Exception {
+ verify(mUserManager).removeUser(user.id);
+ }
+
+ private void verifyPostPreCreatedUserSkipped() throws Exception {
+ verify(mHelper, never()).runAsync(any());
+ }
+
+ private void verifyPostPreCreatedUserException() throws Exception {
+ verify(mHelper).logPrecreationFailure(anyString(), any());
+ }
+
+ private void verifyManagePreCreatedUsers() throws Exception {
+ verify(mHelper).managePreCreatedUsers();
+ }
+
+ private void assertParcelValue(List<String> errors, String field, int expected,
+ int actual) {
+ if (expected != actual) {
+ errors.add(String.format("%s mismatch: expected=%d, actual=%d",
+ field, expected, actual));
+ }
+ }
+
+ private void assertParcelValueInRange(List<String> errors, String field, long before,
+ long actual, long after) {
+ if (actual < before || actual> after) {
+ errors.add(field + " (" + actual+ ") not in range [" + before + ", " + after + "]");
+ }
+ }
+
+ private void assertMinimumParcelValue(List<String> errors, String field, long min,
+ long actual) {
+ if (actual < min) {
+ errors.add("Minimum " + field + " should be " + min + " (was " + actual + ")");
+ }
+ }
+
+ private void assertNoParcelErrors(List<String> errors) {
+ int size = errors.size();
+ if (size == 0) return;
+
+ StringBuilder msg = new StringBuilder().append(size).append(" errors on parcel: ");
+ for (String error : errors) {
+ msg.append("\n\t").append(error);
+ }
+ msg.append('\n');
+ throw new IllegalArgumentException(msg.toString());
+ }
+
+ /**
+ * Asserts that no exception was thrown when answering to a mocked {@code ICar} binder call.
+ * <p>
+ * This method should be called before asserting the expected results of a test, so it makes
+ * clear why the test failed when the call was not made as expected.
+ */
+ private void assertNoICarCallExceptions() throws Exception {
+ if (mBinderCallException != null)
+ throw mBinderCallException;
+
}
}
diff --git a/tests/src/com/android/server/wm/CarLaunchParamsModifierTest.java b/tests/src/com/android/server/wm/CarLaunchParamsModifierTest.java
new file mode 100644
index 0000000..5d9f8ba
--- /dev/null
+++ b/tests/src/com/android/server/wm/CarLaunchParamsModifierTest.java
@@ -0,0 +1,495 @@
+/*
+ * 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.server.wm;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.ActivityTaskManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Configuration;
+import android.hardware.display.DisplayManager;
+import android.os.UserHandle;
+import android.view.Display;
+import android.view.SurfaceControl;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.AttributeCache;
+import com.android.server.LocalServices;
+import com.android.server.display.color.ColorDisplayService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class CarLaunchParamsModifierTest {
+ private static final int PASSENGER_DISPLAY_ID_10 = 10;
+ private static final int PASSENGER_DISPLAY_ID_11 = 11;
+
+ private MockitoSession mMockingSession;
+
+ private CarLaunchParamsModifier mModifier;
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private DisplayManager mDisplayManager;
+ @Mock
+ private ActivityTaskManagerService mActivityTaskManagerService;
+ @Mock
+ private ActivityStackSupervisor mActivityStackSupervisor;
+ @Mock
+ private RecentTasks mRecentTasks;
+ @Mock
+ private WindowManagerService mWindowManagerService;
+ @Mock
+ private ColorDisplayService.ColorDisplayServiceInternal mColorDisplayServiceInternal;
+ @Mock
+ private RootWindowContainer mRootWindowContainer;
+ @Mock
+ private LaunchParamsController mLaunchParamsController;
+
+ @Mock
+ private Display mDisplay0ForDriver;
+ @Mock
+ private TaskDisplayArea mDisplayArea0ForDriver;
+ @Mock
+ private Display mDisplay1Private;
+ @Mock
+ private TaskDisplayArea mDisplayArea1Private;
+ @Mock
+ private Display mDisplay10ForPassenger;
+ @Mock
+ private TaskDisplayArea mDisplayArea10ForPassenger;
+ @Mock
+ private Display mDisplay11ForPassenger;
+ @Mock
+ private TaskDisplayArea mDisplayArea11ForPassenger;
+
+ // All mocks from here before CarLaunchParamsModifier are arguments for
+ // LaunchParamsModifier.onCalculate() call.
+ @Mock
+ private Task mTask;
+ @Mock
+ private ActivityInfo.WindowLayout mWindowLayout;
+ @Mock
+ private ActivityRecord mActivityRecordActivity;
+ @Mock
+ private ActivityRecord mActivityRecordSource;
+ @Mock
+ private ActivityOptions mActivityOptions;
+ @Mock
+ private LaunchParamsController.LaunchParams mCurrentParams;
+ @Mock
+ private LaunchParamsController.LaunchParams mOutParams;
+
+ private void mockDisplay(Display display, TaskDisplayArea defaultTaskDisplayArea,
+ int displayId, int flags, int type) {
+ when(mDisplayManager.getDisplay(displayId)).thenReturn(display);
+ when(display.getDisplayId()).thenReturn(displayId);
+ when(display.getFlags()).thenReturn(flags);
+ when(display.getType()).thenReturn(type);
+
+ // Return the same id as the display for simplicity
+ DisplayContent dc = mock(DisplayContent.class);
+ defaultTaskDisplayArea.mDisplayContent = dc;
+ when(mRootWindowContainer.getDisplayContentOrCreate(displayId)).thenReturn(dc);
+ when(dc.getDisplay()).thenReturn(display);
+ when(dc.getDefaultTaskDisplayArea()).thenReturn(defaultTaskDisplayArea);
+ }
+
+ @Before
+ public void setUp() {
+ mMockingSession = mockitoSession()
+ .initMocks(this)
+ .mockStatic(ActivityTaskManager.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+ when(mContext.getSystemService(DisplayManager.class)).thenReturn(mDisplayManager);
+ doReturn(mActivityTaskManagerService).when(() -> ActivityTaskManager.getService());
+ mActivityTaskManagerService.mStackSupervisor = mActivityStackSupervisor;
+ when(mActivityStackSupervisor.getLaunchParamsController()).thenReturn(
+ mLaunchParamsController);
+ mActivityTaskManagerService.mRootWindowContainer = mRootWindowContainer;
+ mActivityTaskManagerService.mWindowManager = mWindowManagerService;
+ when(mActivityTaskManagerService.getRecentTasks()).thenReturn(mRecentTasks);
+ mWindowManagerService.mTransactionFactory = () -> new SurfaceControl.Transaction();
+ AttributeCache.init(getInstrumentation().getTargetContext());
+ LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class,
+ mColorDisplayServiceInternal);
+ when(mActivityOptions.getLaunchDisplayId()).thenReturn(Display.INVALID_DISPLAY);
+ mockDisplay(mDisplay0ForDriver, mDisplayArea0ForDriver, 0, 0, 0);
+ mockDisplay(mDisplay10ForPassenger, mDisplayArea10ForPassenger, PASSENGER_DISPLAY_ID_10,
+ /* flags */ 0, /* type */ 0);
+ mockDisplay(mDisplay11ForPassenger, mDisplayArea11ForPassenger, PASSENGER_DISPLAY_ID_11,
+ /* flags */ 0, /* type */ 0);
+ mockDisplay(mDisplay1Private, mDisplayArea1Private, 1, Display.FLAG_PRIVATE, 0);
+
+ mModifier = new CarLaunchParamsModifier(mContext);
+ mModifier.init();
+ }
+
+ @After
+ public void tearDown() {
+ LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
+ mMockingSession.finishMocking();
+ }
+
+ private void assertDisplayIsAllowed(@UserIdInt int userId, Display display) {
+ mTask.mUserId = userId;
+ mCurrentParams.mPreferredTaskDisplayArea = mModifier
+ .getDefaultTaskDisplayAreaOnDisplay(display.getDisplayId());
+ assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
+ mActivityRecordSource, mActivityOptions, 0, mCurrentParams, mOutParams))
+ .isEqualTo(LaunchParamsController.LaunchParamsModifier.RESULT_SKIP);
+ }
+
+ private void assertDisplayIsReassigned(@UserIdInt int userId, Display displayRequested,
+ Display displayAssigned) {
+ assertThat(displayRequested.getDisplayId()).isNotEqualTo(displayAssigned.getDisplayId());
+ mTask.mUserId = userId;
+ TaskDisplayArea requestedTaskDisplayArea = mModifier
+ .getDefaultTaskDisplayAreaOnDisplay(displayRequested.getDisplayId());
+ TaskDisplayArea assignedTaskDisplayArea = mModifier
+ .getDefaultTaskDisplayAreaOnDisplay(displayAssigned.getDisplayId());
+ mCurrentParams.mPreferredTaskDisplayArea = requestedTaskDisplayArea;
+ assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
+ mActivityRecordSource, mActivityOptions, 0, mCurrentParams, mOutParams))
+ .isEqualTo(LaunchParamsController.LaunchParamsModifier.RESULT_DONE);
+ assertThat(mOutParams.mPreferredTaskDisplayArea).isEqualTo(assignedTaskDisplayArea);
+ }
+
+ private void assertDisplayIsAssigned(
+ @UserIdInt int userId, TaskDisplayArea expectedDisplayArea) {
+ mTask.mUserId = userId;
+ mCurrentParams.mPreferredTaskDisplayArea = null;
+ assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
+ mActivityRecordSource, mActivityOptions, 0, mCurrentParams, mOutParams))
+ .isEqualTo(LaunchParamsController.LaunchParamsModifier.RESULT_DONE);
+ assertThat(mOutParams.mPreferredTaskDisplayArea).isEqualTo(expectedDisplayArea);
+ }
+
+ private void assertNoDisplayIsAssigned(@UserIdInt int userId) {
+ mTask.mUserId = userId;
+ mCurrentParams.mPreferredTaskDisplayArea = null;
+ assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
+ mActivityRecordSource, mActivityOptions, 0, mCurrentParams, mOutParams))
+ .isEqualTo(LaunchParamsController.LaunchParamsModifier.RESULT_SKIP);
+ assertThat(mOutParams.mPreferredTaskDisplayArea).isNull();
+ }
+
+ private ActivityRecord buildActivityRecord(String packageName, String className) {
+ ActivityInfo info = new ActivityInfo();
+ info.packageName = packageName;
+ info.name = className;
+ info.applicationInfo = new ApplicationInfo();
+ info.applicationInfo.packageName = packageName;
+ Intent intent = new Intent();
+ intent.setClassName(packageName, className);
+ return new ActivityRecord(mActivityTaskManagerService, /* caller */null,
+ /* launchedFromPid */ 0, /* launchedFromUid */ 0, /* launchedFromPackage */ null,
+ /* launchedFromFeature */ null, intent, /* resolvedType */ null, info,
+ new Configuration(), /* resultTo */ null, /* resultWho */ null, /* reqCode */ 0,
+ /*componentSpecified*/ false, /* rootVoiceInteraction */ false,
+ mActivityStackSupervisor, /* options */ null, /* sourceRecord */ null);
+ }
+
+ @Test
+ public void testNoPolicySet() {
+ final int randomUserId = 1000;
+ // policy not set set, so do not apply any enforcement.
+ assertDisplayIsAllowed(randomUserId, mDisplay0ForDriver);
+ assertDisplayIsAllowed(randomUserId, mDisplay10ForPassenger);
+
+ assertDisplayIsAllowed(UserHandle.USER_SYSTEM, mDisplay0ForDriver);
+ assertDisplayIsAllowed(UserHandle.USER_SYSTEM, mDisplay10ForPassenger);
+
+ assertDisplayIsAllowed(ActivityManager.getCurrentUser(), mDisplay0ForDriver);
+ assertDisplayIsAllowed(ActivityManager.getCurrentUser(), mDisplay10ForPassenger);
+ }
+
+ private void assertAllDisplaysAllowedForUser(int userId) {
+ assertDisplayIsAllowed(userId, mDisplay0ForDriver);
+ assertDisplayIsAllowed(userId, mDisplay10ForPassenger);
+ assertDisplayIsAllowed(userId, mDisplay11ForPassenger);
+ }
+
+ @Test
+ public void testAllowAllForDriverDuringBoot() {
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay10ForPassenger.getDisplayId()});
+
+ // USER_SYSTEM should be allowed always
+ assertAllDisplaysAllowedForUser(UserHandle.USER_SYSTEM);
+ }
+
+ @Test
+ public void testAllowAllForDriverAfterUserSwitching() {
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay10ForPassenger.getDisplayId()});
+
+ final int driver1 = 10;
+ mModifier.handleCurrentUserSwitching(driver1);
+ assertAllDisplaysAllowedForUser(driver1);
+ assertAllDisplaysAllowedForUser(UserHandle.USER_SYSTEM);
+
+ final int driver2 = 10;
+ mModifier.handleCurrentUserSwitching(driver2);
+ assertAllDisplaysAllowedForUser(driver2);
+ assertAllDisplaysAllowedForUser(UserHandle.USER_SYSTEM);
+ }
+
+ @Test
+ public void testPassengerAllowed() {
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ final int passengerUserId = 100;
+ mModifier.setDisplayWhitelistForUser(passengerUserId,
+ new int[]{mDisplay10ForPassenger.getDisplayId()});
+
+ assertDisplayIsAllowed(passengerUserId, mDisplay10ForPassenger);
+ }
+
+ @Test
+ public void testPassengerChange() {
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ int passengerUserId1 = 100;
+ mModifier.setDisplayWhitelistForUser(passengerUserId1,
+ new int[]{mDisplay11ForPassenger.getDisplayId()});
+
+ assertDisplayIsAllowed(passengerUserId1, mDisplay11ForPassenger);
+
+ int passengerUserId2 = 101;
+ mModifier.setDisplayWhitelistForUser(passengerUserId2,
+ new int[]{mDisplay11ForPassenger.getDisplayId()});
+
+ assertDisplayIsAllowed(passengerUserId2, mDisplay11ForPassenger);
+ // 11 not allowed, so reassigned to the 1st passenger display
+ assertDisplayIsReassigned(passengerUserId1, mDisplay11ForPassenger, mDisplay10ForPassenger);
+ }
+
+ @Test
+ public void testPassengerNotAllowed() {
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ final int passengerUserId = 100;
+ mModifier.setDisplayWhitelistForUser(
+ passengerUserId, new int[]{mDisplay10ForPassenger.getDisplayId()});
+
+ assertDisplayIsReassigned(passengerUserId, mDisplay0ForDriver, mDisplay10ForPassenger);
+ assertDisplayIsReassigned(passengerUserId, mDisplay11ForPassenger, mDisplay10ForPassenger);
+ }
+
+ @Test
+ public void testPassengerNotAllowedAfterUserSwitch() {
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ int passengerUserId = 100;
+ mModifier.setDisplayWhitelistForUser(
+ passengerUserId, new int[]{mDisplay11ForPassenger.getDisplayId()});
+ assertDisplayIsAllowed(passengerUserId, mDisplay11ForPassenger);
+
+ mModifier.handleCurrentUserSwitching(2);
+
+ assertDisplayIsReassigned(passengerUserId, mDisplay0ForDriver, mDisplay10ForPassenger);
+ assertDisplayIsReassigned(passengerUserId, mDisplay11ForPassenger, mDisplay10ForPassenger);
+ }
+
+ @Test
+ public void testPassengerNotAllowedAfterAssigningCurrentUser() {
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ int passengerUserId = 100;
+ mModifier.setDisplayWhitelistForUser(
+ passengerUserId, new int[]{mDisplay11ForPassenger.getDisplayId()});
+ assertDisplayIsAllowed(passengerUserId, mDisplay11ForPassenger);
+
+ mModifier.setDisplayWhitelistForUser(
+ UserHandle.USER_SYSTEM, new int[]{mDisplay11ForPassenger.getDisplayId()});
+
+ assertDisplayIsReassigned(passengerUserId, mDisplay0ForDriver, mDisplay10ForPassenger);
+ assertDisplayIsReassigned(passengerUserId, mDisplay11ForPassenger, mDisplay10ForPassenger);
+ }
+
+ @Test
+ public void testPassengerDisplayRemoved() {
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ final int passengerUserId = 100;
+ mModifier.setDisplayWhitelistForUser(passengerUserId,
+ new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ assertDisplayIsAllowed(passengerUserId, mDisplay10ForPassenger);
+ assertDisplayIsAllowed(passengerUserId, mDisplay11ForPassenger);
+
+ mModifier.mDisplayListener.onDisplayRemoved(mDisplay11ForPassenger.getDisplayId());
+
+ assertDisplayIsAllowed(passengerUserId, mDisplay10ForPassenger);
+ assertDisplayIsReassigned(passengerUserId, mDisplay11ForPassenger, mDisplay10ForPassenger);
+ }
+
+ @Test
+ public void testPassengerDisplayRemovedFromSetPassengerDisplays() {
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ final int passengerUserId = 100;
+ mModifier.setDisplayWhitelistForUser(passengerUserId,
+ new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ assertDisplayIsAllowed(passengerUserId, mDisplay10ForPassenger);
+ assertDisplayIsAllowed(passengerUserId, mDisplay11ForPassenger);
+
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId()});
+
+ assertDisplayIsAllowed(passengerUserId, mDisplay10ForPassenger);
+ assertDisplayIsReassigned(passengerUserId, mDisplay11ForPassenger, mDisplay10ForPassenger);
+ }
+
+ @Test
+ public void testIgnorePrivateDisplay() {
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ final int passengerUserId = 100;
+ mModifier.setDisplayWhitelistForUser(passengerUserId,
+ new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay10ForPassenger.getDisplayId()});
+
+ assertDisplayIsAllowed(passengerUserId, mDisplay1Private);
+ }
+
+ @Test
+ public void testDriverPassengerSwap() {
+ mModifier.setPassengerDisplays(new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ final int wasDriver = 10;
+ final int wasPassenger = 11;
+ mModifier.handleCurrentUserSwitching(wasDriver);
+ mModifier.setDisplayWhitelistForUser(wasPassenger,
+ new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ assertDisplayIsAllowed(wasDriver, mDisplay0ForDriver);
+ assertDisplayIsAllowed(wasDriver, mDisplay10ForPassenger);
+ assertDisplayIsAllowed(wasDriver, mDisplay11ForPassenger);
+ assertDisplayIsReassigned(wasPassenger, mDisplay0ForDriver, mDisplay10ForPassenger);
+ assertDisplayIsAllowed(wasPassenger, mDisplay10ForPassenger);
+ assertDisplayIsAllowed(wasPassenger, mDisplay11ForPassenger);
+
+ final int driver = wasPassenger;
+ final int passenger = wasDriver;
+ mModifier.handleCurrentUserSwitching(driver);
+ mModifier.setDisplayWhitelistForUser(passenger,
+ new int[]{mDisplay10ForPassenger.getDisplayId(),
+ mDisplay11ForPassenger.getDisplayId()});
+
+ assertDisplayIsAllowed(driver, mDisplay0ForDriver);
+ assertDisplayIsAllowed(driver, mDisplay10ForPassenger);
+ assertDisplayIsAllowed(driver, mDisplay11ForPassenger);
+ assertDisplayIsReassigned(passenger, mDisplay0ForDriver, mDisplay10ForPassenger);
+ assertDisplayIsAllowed(passenger, mDisplay10ForPassenger);
+ assertDisplayIsAllowed(passenger, mDisplay11ForPassenger);
+ }
+
+ @Test
+ public void testPreferSourceForDriver() {
+ when(mActivityRecordSource.getDisplayArea()).thenReturn(mDisplayArea0ForDriver);
+
+ // When no sourcePreferredComponents is set, it doesn't set the display for system user.
+ assertNoDisplayIsAssigned(UserHandle.USER_SYSTEM);
+
+ mModifier.setSourcePreferredComponents(true, null);
+ assertDisplayIsAssigned(UserHandle.USER_SYSTEM, mDisplayArea0ForDriver);
+ }
+
+ @Test
+ public void testPreferSourceForPassenger() {
+ mModifier.setPassengerDisplays(new int[]{PASSENGER_DISPLAY_ID_10, PASSENGER_DISPLAY_ID_11});
+ int passengerUserId = 100;
+ mModifier.setDisplayWhitelistForUser(passengerUserId,
+ new int[]{PASSENGER_DISPLAY_ID_10, PASSENGER_DISPLAY_ID_11});
+ when(mActivityRecordSource.getDisplayArea()).thenReturn(mDisplayArea11ForPassenger);
+
+ // When no sourcePreferredComponents is set, it returns the default passenger display.
+ assertDisplayIsAssigned(passengerUserId, mDisplayArea10ForPassenger);
+
+ mModifier.setSourcePreferredComponents(true, null);
+ assertDisplayIsAssigned(passengerUserId, mDisplayArea11ForPassenger);
+ }
+
+ @Test
+ public void testPreferSourceDoNotOverrideActivityOptions() {
+ when(mActivityOptions.getLaunchDisplayId()).thenReturn(PASSENGER_DISPLAY_ID_10);
+ when(mActivityRecordSource.getDisplayArea()).thenReturn(mDisplayArea0ForDriver);
+
+ mModifier.setSourcePreferredComponents(true, null);
+ assertNoDisplayIsAssigned(UserHandle.USER_SYSTEM);
+ }
+
+ @Test
+ public void testPreferSourceForSpecifiedActivity() {
+ when(mActivityRecordSource.getDisplayArea()).thenReturn(mDisplayArea0ForDriver);
+ mActivityRecordActivity = buildActivityRecord("testPackage", "testActivity");
+ mModifier.setSourcePreferredComponents(true,
+ Arrays.asList(new ComponentName("testPackage", "testActivity")));
+
+ assertDisplayIsAssigned(UserHandle.USER_SYSTEM, mDisplayArea0ForDriver);
+ }
+
+ @Test
+ public void testPreferSourceDoNotAssignDisplayForNonSpecifiedActivity() {
+ when(mActivityRecordSource.getDisplayArea()).thenReturn(mDisplayArea0ForDriver);
+ mActivityRecordActivity = buildActivityRecord("dummyPackage", "dummyActivity");
+ mModifier.setSourcePreferredComponents(true,
+ Arrays.asList(new ComponentName("testPackage", "testActivity")));
+
+ assertNoDisplayIsAssigned(UserHandle.USER_SYSTEM);
+ }
+}