diff options
| author | Xin Li <delphij@google.com> | 2020-09-08 16:55:24 -0700 |
|---|---|---|
| committer | Xin Li <delphij@google.com> | 2020-09-08 16:55:24 -0700 |
| commit | b68ac114f832c9a5691fc98fd97cf6307d9d3e30 (patch) | |
| tree | e67c2f26c6603f0ee5e8762a5a624a9ab089e94c | |
| parent | 57450c2252059c0a61962ac8ba5959e0f3bb4d98 (diff) | |
| parent | f48ff0ad05662c6cb44b6f3c8d48da1f21997f19 (diff) | |
| download | platform_frameworks_opt_car_services-master.tar.gz platform_frameworks_opt_car_services-master.tar.bz2 platform_frameworks_opt_car_services-master.zip | |
Bug: 168057903
Merged-In: I11c4aae11395a7bf8f92574af746f7af3687a216
Change-Id: I33d97b2c5c48451d34a158ecb29d5db43fc0eefa
| -rw-r--r-- | Android.bp | 9 | ||||
| -rw-r--r-- | OWNERS | 4 | ||||
| -rw-r--r-- | PREUPLOAD.cfg | 6 | ||||
| -rw-r--r-- | src/com/android/internal/car/CarServiceHelperService.java | 1165 | ||||
| -rw-r--r-- | src/com/android/internal/car/ExternalConstants.java | 62 | ||||
| -rw-r--r-- | src/com/android/internal/car/ICarServiceHelper.aidl | 25 | ||||
| -rw-r--r-- | src/com/android/server/wm/CarLaunchParamsModifier.java | 393 | ||||
| -rw-r--r-- | tests/Android.mk | 18 | ||||
| -rw-r--r-- | tests/AndroidManifest.xml | 3 | ||||
| -rw-r--r-- | tests/src/com/android/internal/car/CarHelperServiceTest.java | 1213 | ||||
| -rw-r--r-- | tests/src/com/android/server/wm/CarLaunchParamsModifierTest.java | 495 |
11 files changed, 3113 insertions, 280 deletions
@@ -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 { @@ -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); + } +} |
