content: /*
   * Copyright (C) 2017 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;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.ActivityManager;
  import android.app.IActivityManager;
  import android.app.admin.DevicePolicyManager;
  import android.car.userlib.CarUserManagerHelper;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.ServiceConnection;
  import android.content.pm.UserInfo;
  import android.hidl.manager.V1_0.IServiceManager;
  import android.os.Binder;
  import android.os.IBinder;
  import android.os.Parcel;
  import android.os.Process;
  import android.os.RemoteException;
  import android.os.ServiceManager;
  import android.os.SystemProperties;
  import android.os.Trace;
  import android.os.UserHandle;
  import android.os.UserManager;
  import android.sysprop.CarProperties;
  import android.util.Slog;
  import android.util.TimingsTraceLog;
  import android.util.SparseBooleanArray;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.server.SystemService;
  import com.android.server.Watchdog;
  import com.android.server.am.ActivityManagerService;
  
  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;
  
  /**
   * 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";
      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 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"
      );
  
      @GuardedBy("mLock")
      private int mLastSwitchedUser = UserHandle.USER_NULL;
  
      private final ICarServiceHelperImpl mHelper = new ICarServiceHelperImpl();
      private final Context mContext;
      private final Object mLock = new Object();
      @GuardedBy("mLock")
      private IBinder mCarService;
      @GuardedBy("mLock")
      private boolean mSystemBootCompleted;
  
      @GuardedBy("mLock")
      private final HashMap<Integer, Boolean> mUserUnlockedStatus = new HashMap<>();
      private final CarUserManagerHelper mCarUserManagerHelper;
      private final UserManager mUserManager;
      private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
  
          @Override
          public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
              if (DBG) {
                  Slog.d(TAG, "onServiceConnected:" + iBinder);
              }
              handleCarServiceConnection(iBinder);
          }
  
          @Override
          public void onServiceDisconnected(ComponentName componentName) {
              handleCarServiceCrash();
          }
      };
  
      public CarServiceHelperService(Context context) {
          this(context, new CarUserManagerHelper(context));
      }
  
      @VisibleForTesting
      CarServiceHelperService(Context context, CarUserManagerHelper carUserManagerHelper) {
          super(context);
          mContext = context;
          mCarUserManagerHelper = carUserManagerHelper;
          mUserManager = UserManager.get(context);
      }
  
      @Override
      public void onBootPhase(int phase) {
          if (DBG) {
              Slog.d(TAG, "onBootPhase:" + phase);
          }
          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();
          } 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();
              }
              t.traceEnd();
          }
      }
  
      @Override
      public void onStart() {
          Intent intent = new Intent();
          intent.setPackage("com.android.car");
          intent.setAction(CAR_SERVICE_INTERFACE);
          if (!getContext().bindServiceAsUser(intent, mCarServiceConnection, Context.BIND_AUTO_CREATE,
                  UserHandle.SYSTEM)) {
              Slog.wtf(TAG, "cannot start car service");
          }
          System.loadLibrary("car-framework-service-jni");
      }
  
  
      @Override
      public void onUnlockUser(int userHandle) {
          handleUserLockStatusChange(userHandle, true);
          if (DBG) {
              Slog.d(TAG, "User" + userHandle + " unlocked");
          }
      }
  
      @Override
      public void onStopUser(int userHandle) {
          handleUserLockStatusChange(userHandle, false);
      }
  
      @Override
      public void onCleanupUser(int userHandle) {
          handleUserLockStatusChange(userHandle, false);
      }
  
      @Override
      public void onSwitchUser(int userHandle) {
          synchronized (mLock) {
              mLastSwitchedUser = userHandle;
              if (mCarService == null) {
                  return;  // The event will be delivered upon CarService connection.
              }
          }
          sendSwitchUserBindercall(userHandle);
      }
  
      // 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() {
          synchronized (mLock) {
              if (mCarService != null) {
                  return;
              }
          }
          IBinder iBinder = ServiceManager.checkService("car_service");
          if (iBinder != null) {
              if (DBG) {
                  Slog.d(TAG, "Car service found through ServiceManager:" + iBinder);
              }
              handleCarServiceConnection(iBinder);
          }
      }
  
      private void handleCarServiceConnection(IBinder iBinder) {
          int lastSwitchedUser;
          boolean systemBootCompleted;
          synchronized (mLock) {
              if (mCarService == iBinder) {
                  return; // already connected.
              }
              if (mCarService != null) {
                  Slog.i(TAG, "car service binder changed, was:" + mCarService
                          + " new:" + iBinder);
              }
              mCarService = iBinder;
              lastSwitchedUser = mLastSwitchedUser;
              systemBootCompleted = mSystemBootCompleted;
          }
          Slog.i(TAG, "**CarService connected**");
          sendSetCarServiceHelperBinderCall();
          if (systemBootCompleted) {
              notifyAllUnlockedUsers();
          }
          if (lastSwitchedUser != UserHandle.USER_NULL) {
              sendSwitchUserBindercall(lastSwitchedUser);
          }
      }
  
      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;
                  }
              }
          }
          if (shouldNotify) {
              sendSetUserLockStatusBinderCall(userHandle, unlocked);
          }
      }
  
      private void setupAndStartUsers() {
          DevicePolicyManager devicePolicyManager =
                  mContext.getSystemService(DevicePolicyManager.class);
          if (devicePolicyManager != null && devicePolicyManager.getUserProvisioningState()
                  != DevicePolicyManager.STATE_USER_UNMANAGED) {
              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;
          } else {
              targetUserId = mCarUserManagerHelper.getInitialUser();
              Slog.i(TAG, "Switching to user " + targetUserId + " on boot");
          }
  
          IActivityManager am = ActivityManager.getService();
          if (am == null) {
              Slog.wtf(TAG, "cannot get ActivityManagerService");
              return;
          }
  
          // If system user is the only user to unlock, handle it when system completes the boot.
          if (targetUserId == UserHandle.USER_SYSTEM) {
              return;
          }
  
          TimingsTraceLog t = new TimingsTraceLog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
          unlockSystemUser(t, am);
  
          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();
      }
  
      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");
                      return;
                  }
              }
          } catch (RemoteException e) {
              // should not happen for local call.
              Slog.wtf("RemoteException from AMS", e);
          } finally {
              t.traceEnd();
          }
      }
  
      private 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");
          if (numberRequestedGuests < 0 || numberRequestedUsers < 0) {
              Slog.w(TAG, "preCreateUsers(): invalid values provided by OEM; "
                      + "number_pre_created_guests=" + numberRequestedGuests
                      + ", number_pre_created_users=" + numberRequestedUsers);
              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);
  
          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();
  
          for (int i = 0; i < allUsersSize; i++) {
              UserInfo user = allUsers.get(i);
              if (!user.preCreated) continue;
              if (!user.isInitialized()) {
                  Slog.w(TAG, "Found invalid pre-created user that needs to be removed: "
                          + user.toFullString());
                  invalidUsers.append(user.id, /* notUsed=*/ true);
                  continue;
              }
              if (user.isGuest()) {
                  numberExistingGuests++;
              } else {
                  numberExistingUsers++;
              }
          }
          if (DBG) {
              Slog.i(TAG, "managePreCreatedUsers(): system already has " + numberExistingGuests
                      + " pre-created guests," + numberExistingUsers + " pre-created users, and these"
                      + " invalid users: " + invalidUsers );
          }
  
          int numberGuests = numberRequestedGuests - numberExistingGuests;
          int numberUsers = numberRequestedUsers - numberExistingUsers;
          int numberInvalidUsers = invalidUsers.size();
  
          if (numberGuests <= 0 && numberUsers <= 0 && numberInvalidUsers == 0) {
              Slog.i(TAG, "managePreCreatedUsers(): all pre-created and no invalid ones");
              return;
          }
  
          // Finally, manage them....
  
          // In theory, we could submit multiple user pre-creations in parallel, but we're
          // 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);
  
              t.traceBegin("preCreateUsers");
              if (numberUsers > 0) {
                  preCreateUsers(t, numberUsers, /* isGuest= */ false);
              }
              if (numberGuests > 0) {
                  preCreateUsers(t, numberGuests, /* isGuest= */ true);
              }
              t.traceEnd();
  
              if (numberInvalidUsers > 0) {
                  t.traceBegin("removeInvalidPreCreatedUsers");
                  for (int i = 0; i < numberInvalidUsers; i++) {
                      int userId = invalidUsers.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) {
          String msg = isGuest ? "preCreateGuests-" + size : "preCreateUsers-" + size;
          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 " : "")
                          + " user #" + i);
                  continue;
              }
          }
          t.traceEnd();
      }
  
      // TODO(b/111451156): add unit test?
      @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";
          }
          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);
          try {
              if (user == null) {
                  // Couldn't create user, most likely because there are too many.
                  Slog.w(TAG, "couldn't " + traceMsg);
                  return null;
              }
          } 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);
          }
          for (Integer i : users) {
              sendSetUserLockStatusBinderCall(i, true);
          }
      }
  
      private void sendSetCarServiceHelperBinderCall() {
          Parcel data = Parcel.obtain();
          data.writeInterfaceToken(CAR_SERVICE_INTERFACE);
          data.writeStrongBinder(mHelper.asBinder());
          // void setCarServiceHelper(in IBinder helper)
          sendBinderCallToCarService(data, ICAR_CALL_SET_CAR_SERVICE_HELPER);
      }
  
      private void sendSetUserLockStatusBinderCall(int userHandle, boolean unlocked) {
          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);
      }
  
      private void sendSwitchUserBindercall(int userHandle) {
          Parcel data = Parcel.obtain();
          data.writeInterfaceToken(CAR_SERVICE_INTERFACE);
          data.writeInt(userHandle);
          // void onSwitchUser(in int userHandle)
          sendBinderCallToCarService(data, ICAR_CALL_SET_SWITCH_USER);
      }
  
      private void sendBinderCallToCarService(Parcel data, int callNumber) {
          // Cannot depend on ICar which is defined in CarService, so handle binder call directly
          // instead.
          IBinder carService;
          synchronized (mLock) {
              carService = mCarService;
          }
          try {
              carService.transact(IBinder.FIRST_CALL_TRANSACTION + callNumber,
                      data, null, Binder.FLAG_ONEWAY);
          } catch (RemoteException e) {
              Slog.w(TAG, "RemoteException from car service", e);
              handleCarServiceCrash();
          } finally {
              data.recycle();
          }
      }
  
      // Adapted from frameworks/base/services/core/java/com/android/server/Watchdog.java
      // TODO(b/131861630) use implementation common with Watchdog.java
      //
      private static ArrayList<Integer> getInterestingHalPids() {
          try {
              IServiceManager serviceManager = IServiceManager.getService();
              ArrayList<IServiceManager.InstanceDebugInfo> dump =
                      serviceManager.debugDump();
              HashSet<Integer> pids = new HashSet<>();
              for (IServiceManager.InstanceDebugInfo info : dump) {
                  if (info.pid == IServiceManager.PidConstant.NO_PID) {
                      continue;
                  }
  
                  if (Watchdog.HAL_INTERFACES_OF_INTEREST.contains(info.interfaceName) ||
                          CAR_HAL_INTERFACES_OF_INTEREST.contains(info.interfaceName)) {
                      pids.add(info.pid);
                  }
              }
  
              return new ArrayList<Integer>(pids);
          } catch (RemoteException e) {
              return new ArrayList<Integer>();
          }
      }
  
      // Adapted from frameworks/base/services/core/java/com/android/server/Watchdog.java
      // TODO(b/131861630) use implementation common with Watchdog.java
      //
      private static ArrayList<Integer> getInterestingNativePids() {
          ArrayList<Integer> pids = getInterestingHalPids();
  
          int[] nativePids = Process.getPidsForCommands(Watchdog.NATIVE_STACKS_OF_INTEREST);
          if (nativePids != null) {
              pids.ensureCapacity(pids.size() + nativePids.length);
              for (int i : nativePids) {
                  pids.add(i);
              }
          }
  
          return pids;
      }
  
      // Borrowed from Watchdog.java.  Create an ANR file from the call stacks.
      //
      private static void dumpServiceStacks() {
          ArrayList<Integer> pids = new ArrayList<>();
          pids.add(Process.myPid());
  
          ActivityManagerService.dumpStackTraces(
                  pids, null, null, getInterestingNativePids());
      }
  
      private void handleCarServiceCrash() {
          // Recovery behavior.  Kill the system server and reset
          // everything if enabled by the property.
          boolean restartOnServiceCrash = SystemProperties.getBoolean(PROP_RESTART_RUNTIME, false);
  
          dumpServiceStacks();
          if (restartOnServiceCrash) {
              Slog.w(TAG, "*** CARHELPER KILLING SYSTEM PROCESS: " + "CarService crash");
              Slog.w(TAG, "*** GOODBYE!");
              Process.killProcess(Process.myPid());
              System.exit(10);
          } else {
              Slog.w(TAG, "*** CARHELPER ignoring: " + "CarService crash");
          }
      }
  
      private static native int nativeForceSuspend(int timeoutMs);
  
      private class ICarServiceHelperImpl extends ICarServiceHelper.Stub {
          /**
           * Force device to suspend
           */
          @Override // Binder call
          public int forceSuspend(int timeoutMs) {
              int retVal;
              mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, null);
              final long ident = Binder.clearCallingIdentity();
              try {
                  retVal = nativeForceSuspend(timeoutMs);
              } finally {
                  Binder.restoreCallingIdentity(ident);
              }
              return retVal;
          }
      }
  }
  