/*
 * 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 static com.android.car.internal.SystemConstants.ICAR_SYSTEM_SERVER_CLIENT;
import static com.android.car.internal.common.CommonConstants.CAR_SERVICE_INTERFACE;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STARTING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPED;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
import static com.android.car.internal.common.CommonConstants.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.annotation.UserIdInt;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyManager.DevicePolicyOperation;
import android.app.admin.DevicePolicyManager.OperationSafetyReason;
import android.app.admin.DevicePolicySafetyChecker;
import android.automotive.watchdog.internal.ICarWatchdogMonitor;
import android.automotive.watchdog.internal.PowerCycle;
import android.automotive.watchdog.internal.StateType;
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.hidl.manager.V1_0.IServiceManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.EventLog;
import android.util.IndentingPrintWriter;
import android.util.TimeUtils;

import com.android.car.internal.ICarServiceHelper;
import com.android.car.internal.ICarSystemServerClient;
import com.android.car.internal.common.CommonConstants.UserLifecycleEventType;
import com.android.car.internal.common.EventLogTags;
import com.android.car.internal.common.UserHelperLite;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;
import com.android.server.Dumpable;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.Watchdog;
import com.android.server.am.ActivityManagerService;
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.UserManagerInternal.UserLifecycleListener;
import com.android.server.utils.Slogf;
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.HashSet;
import java.util.List;
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.
 */
public class CarServiceHelperService extends SystemService
        implements Dumpable, DevicePolicySafetyChecker {

    private static final String TAG = "CarServiceHelper";

    // TODO(b/154033860): STOPSHIP if they're still true
    private static final boolean DBG = true;
    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@2.0::IAudioControl"
    );

    // Message ID representing post-processing of process dumping.
    private static final int WHAT_POST_PROCESS_DUMPING = 1;
    // Message ID representing process killing.
    private static final int WHAT_PROCESS_KILL = 2;
    // Message ID representing service unresponsiveness.
    private static final int WHAT_SERVICE_UNRESPONSIVE = 3;

    private static final long CAR_SERVICE_BINDER_CALL_TIMEOUT = 15_000;

    private static final long LIFECYCLE_TIMESTAMP_IGNORE = 0;

    private final ICarServiceHelperImpl mHelper = new ICarServiceHelperImpl();
    private final Context mContext;
    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private IBinder mCarServiceBinder;
    @GuardedBy("mLock")
    private boolean mSystemBootCompleted;

    private final CarLaunchParamsModifier mCarLaunchParamsModifier;

    private final Handler mHandler;
    private final HandlerThread mHandlerThread = new HandlerThread("CarServiceHelperService");

    private final ProcessTerminator mProcessTerminator = new ProcessTerminator();
    private final CarServiceConnectedCallback mCarServiceConnectedCallback =
            new CarServiceConnectedCallback();
    private final CarServiceProxy mCarServiceProxy;

    /**
     * End-to-end time (from process start) for unlocking the first non-system user.
     */
    private long mFirstUnlockedUserDuration;

    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) {
                Slogf.d(TAG, "onServiceConnected: %s", iBinder);
            }
            handleCarServiceConnection(iBinder);
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            handleCarServiceCrash();
        }
    };

    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_SHUTDOWN_ENTER;
            try {
                mCarWatchdogDaemonHelper.notifySystemStateChange(StateType.POWER_CYCLE,
                        powerCycle, /* arg2= */ 0);
                if (DBG) {
                    Slogf.d(TAG, "Notified car watchdog daemon of power cycle(%d)", powerCycle);
                }
            } catch (RemoteException | RuntimeException e) {
                Slogf.w(TAG, "Notifying power cycle state change failed: %s", e);
            }
        }
    };

    private final CarDevicePolicySafetyChecker mCarDevicePolicySafetyChecker;

    public CarServiceHelperService(Context context) {
        this(context,
                new CarLaunchParamsModifier(context),
                new CarWatchdogDaemonHelper(TAG),
                /* carServiceOperationManager= */ null,
                /* carDevicePolicySafetyChecker= */ null
        );
    }

    @VisibleForTesting
    CarServiceHelperService(
            Context context,
            CarLaunchParamsModifier carLaunchParamsModifier,
            CarWatchdogDaemonHelper carWatchdogDaemonHelper,
            @Nullable CarServiceProxy carServiceOperationManager,
            @Nullable CarDevicePolicySafetyChecker carDevicePolicySafetyChecker) {
        super(context);

        mContext = context;
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper());
        mCarLaunchParamsModifier = carLaunchParamsModifier;
        mCarWatchdogDaemonHelper = carWatchdogDaemonHelper;
        mCarServiceProxy =
                carServiceOperationManager == null ? new CarServiceProxy(this)
                        : carServiceOperationManager;
        UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
        if (umi != null) {
            umi.addUserLifecycleListener(new UserLifecycleListener() {
                @Override
                public void onUserCreated(UserInfo user, Object token) {
                    if (DBG) Slogf.d(TAG, "onUserCreated(): %s", user.toFullString());
                }
                @Override
                public void onUserRemoved(UserInfo user) {
                    if (DBG) Slogf.d(TAG, "onUserRemoved(): $s", user.toFullString());
                    mCarServiceProxy.onUserRemoved(user);
                }
            });
        } else {
            Slogf.e(TAG, "UserManagerInternal not available - should only happen on unit tests");
        }
        mCarDevicePolicySafetyChecker = carDevicePolicySafetyChecker == null
                ? new CarDevicePolicySafetyChecker(this)
                : carDevicePolicySafetyChecker;
    }
    @Override
    public void onBootPhase(int phase) {
        EventLog.writeEvent(EventLogTags.CAR_HELPER_BOOT_PHASE, phase);
        if (DBG) Slogf.d(TAG, "onBootPhase: %d", phase);

        TimingsTraceAndSlog t = newTimingsTraceAndSlog();
        if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
            t.traceBegin("onBootPhase.3pApps");
            mCarLaunchParamsModifier.init();
            setupAndStartUsers(t);
            t.traceEnd();
        } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
            t.traceBegin("onBootPhase.completed");
            synchronized (mLock) {
                mSystemBootCompleted = true;
            }
            try {
                mCarWatchdogDaemonHelper.notifySystemStateChange(
                        StateType.BOOT_PHASE, phase, /* arg2= */ 0);
            } catch (RemoteException | RuntimeException e) {
                Slogf.w(TAG, "Failed to notify boot phase change: %s", e);
            }
            t.traceEnd();
        }
    }

    @Override
    public void onStart() {
        EventLog.writeEvent(EventLogTags.CAR_HELPER_START);

        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 (!mContext.bindServiceAsUser(intent, mCarServiceConnection, Context.BIND_AUTO_CREATE,
                mHandler, UserHandle.SYSTEM)) {
            Slogf.wtf(TAG, "cannot start car service");
        }
        loadNativeLibrary();
    }

    @Override
    public void dump(IndentingPrintWriter pw, String[] args) {
        if (args == null || args.length == 0 || args[0].equals("-a")) {
            pw.printf("System boot completed: %b\n", mSystemBootCompleted);
            pw.print("First unlocked user duration: ");
            TimeUtils.formatDuration(mFirstUnlockedUserDuration, pw); pw.println();
            pw.printf("Queued tasks: %d\n", mProcessTerminator.mQueuedTask);
            mCarServiceProxy.dump(pw);
            mCarDevicePolicySafetyChecker.dump(pw);
            return;
        }

        if ("--user-metrics-only".equals(args[0])) {
            mCarServiceProxy.dumpUserMetrics(pw);
            return;
        }

        if ("--is-operation-safe".equals(args[0]) & args.length > 1) {
            String arg1 = args[1];
            int operation = 0;
            try {
                operation = Integer.parseInt(arg1);
            } catch (Exception e) {
                pw.printf("Invalid operation type: %s\n", arg1);
                return;

            }
            int reason = getUnsafeOperationReason(operation);
            boolean safe = reason == DevicePolicyManager.OPERATION_SAFETY_REASON_NONE;
            pw.printf("Operation %s is %s. Reason: %s\n",
                    DevicePolicyManager.operationToString(operation),
                    safe ? "SAFE" : "UNSAFE",
                    DevicePolicyManager.operationSafetyReasonToString(reason));
            return;
        }
        pw.printf("Invalid args: %s\n", Arrays.toString(args));
    }

    @Override
    public String getDumpableName() {
        return "CarServiceHelper";
    }

    @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) Slogf.d(TAG, "onUserUnlocking(%s)", user);

        sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKING, user);
    }

    @Override
    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) Slogf.d(TAG, "onUserUnlocked(%s)", user);

        if (mFirstUnlockedUserDuration == 0 && !UserHelperLite.isHeadlessSystemUser(userId)) {
            mFirstUnlockedUserDuration = SystemClock.elapsedRealtime()
                    - Process.getStartElapsedRealtime();
            Slogf.i(TAG, "Time to unlock 1st user(%s): %s", user,
                    TimeUtils.formatDuration(mFirstUnlockedUserDuration));
        }
        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) Slogf.d(TAG, "onUserStarting(%s)", user);

        sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING, user);
    }

    @Override
    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) Slogf.d(TAG, "onUserStopping(%s)", user);

        sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPING, user);
        int userId = user.getUserIdentifier();
        mCarLaunchParamsModifier.handleUserStopped(userId);
    }

    @Override
    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) Slogf.d(TAG, "onUserStopped(%s)", user);

        sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPED, user);
    }

    @Override
    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) Slogf.d(TAG, "onUserSwitching(%s>>%s)", from, to);

        mCarServiceProxy.sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING,
                from, to);
        int userId = to.getUserIdentifier();
        mCarLaunchParamsModifier.handleCurrentUserSwitching(userId);
    }

    @Override // from DevicePolicySafetyChecker
    @OperationSafetyReason
    public int getUnsafeOperationReason(@DevicePolicyOperation int operation) {
        return mCarDevicePolicySafetyChecker.isDevicePolicyOperationSafe(operation)
                ? DevicePolicyManager.OPERATION_SAFETY_REASON_NONE
                : DevicePolicyManager.OPERATION_SAFETY_REASON_DRIVING_DISTRACTION;
    }

    @Override // from DevicePolicySafetyChecker
    public boolean isSafeOperation(@OperationSafetyReason int reason) {
        return mCarDevicePolicySafetyChecker.isSafe();
    }

    @Override // from DevicePolicySafetyChecker
    public void onFactoryReset(IResultReceiver callback) {
        if (DBG) Slogf.d(TAG, "onFactoryReset: %s", callback);

        mCarServiceProxy.onFactoryReset(callback);
    }

    @VisibleForTesting
    void loadNativeLibrary() {
        System.loadLibrary("car-framework-service-jni");
    }

    private boolean isPreCreated(@NonNull TargetUser user, @UserLifecycleEventType int eventType) {
        if (!user.isPreCreated()) return false;

        if (DBG) {
            Slogf.d(TAG, "Ignoring event of type %d for pre-created user %s", eventType, user);
        }
        return true;
    }

    @VisibleForTesting
    void handleCarServiceConnection(IBinder iBinder) {
        synchronized (mLock) {
            if (mCarServiceBinder == iBinder) {
                return; // already connected.
            }
            Slogf.i(TAG, "car service binder changed, was %s new: %s", mCarServiceBinder, iBinder);
            mCarServiceBinder = iBinder;
            Slogf.i(TAG, "**CarService connected**");
        }

        sendSetSystemServerConnectionsCall();

        mHandler.removeMessages(WHAT_SERVICE_UNRESPONSIVE);
        mHandler.sendMessageDelayed(
                obtainMessage(CarServiceHelperService::handleCarServiceUnresponsive, this)
                        .setWhat(WHAT_SERVICE_UNRESPONSIVE), CAR_SERVICE_BINDER_CALL_TIMEOUT);
    }

    private TimingsTraceAndSlog newTimingsTraceAndSlog() {
        return new TimingsTraceAndSlog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
    }

    private void setupAndStartUsers(@NonNull TimingsTraceAndSlog t) {
        // TODO(b/156263735): decide if it should return in case the device's on Retail Mode
        t.traceBegin("setupAndStartUsers");
        mCarServiceProxy.initBootUser();
        t.traceEnd();
    }

    private void handleCarServiceUnresponsive() {
        // This should not happen. Calling this method means ICarSystemServerClient binder is not
        // returned after service connection. and CarService has not connected in the given time.
        Slogf.w(TAG, "*** CARHELPER KILLING SYSTEM PROCESS: CarService unresponsive.");
        Slogf.w(TAG, "*** GOODBYE!");
        Process.killProcess(Process.myPid());
        System.exit(10);
    }

    private void sendSetSystemServerConnectionsCall() {
        Parcel data = Parcel.obtain();
        data.writeInterfaceToken(CAR_SERVICE_INTERFACE);
        data.writeStrongBinder(mHelper.asBinder());
        data.writeStrongBinder(mCarServiceConnectedCallback.asBinder());
        IBinder binder;
        synchronized (mLock) {
            binder = mCarServiceBinder;
        }
        int code = IBinder.FIRST_CALL_TRANSACTION;
        try {
            if (VERBOSE) Slogf.v(TAG, "calling one-way binder transaction with code %d", code);
            // oneway void setSystemServerConnections(in IBinder helper, in IBinder receiver) = 0;
            binder.transact(code, data, null, Binder.FLAG_ONEWAY);
            if (VERBOSE) Slogf.v(TAG, "finished one-way binder transaction with code %d", code);
        } catch (RemoteException e) {
            Slogf.w(TAG, "RemoteException from car service", e);
            handleCarServiceCrash();
        } catch (RuntimeException e) {
            Slogf.wtf(TAG, e, "Exception calling binder transaction (real code: %d)", code);
            throw e;
        } finally {
            data.recycle();
        }
    }

    private void sendUserLifecycleEvent(@UserLifecycleEventType int eventType,
            @NonNull TargetUser user) {
        mCarServiceProxy.sendUserLifecycleEvent(eventType, /* from= */ null, user);
    }

    // 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(), null);
    }

    @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);

        mHandler.removeMessages(WHAT_SERVICE_UNRESPONSIVE);

        dumpServiceStacks();
        if (restartOnServiceCrash) {
            Slogf.w(TAG, "*** CARHELPER KILLING SYSTEM PROCESS: CarService crash");
            Slogf.w(TAG, "*** GOODBYE!");
            Process.killProcess(Process.myPid());
            System.exit(10);
        } else {
            Slogf.w(TAG, "*** CARHELPER ignoring: CarService crash");
        }
    }

    private void handleClientsNotResponding(@NonNull int[] pids) {
        mProcessTerminator.requestTerminateProcess(pids);
    }

    private void registerMonitorToWatchdogDaemon() {
        try {
            mCarWatchdogDaemonHelper.registerMonitor(mCarWatchdogMonitor);
            synchronized (mLock) {
                if (!mSystemBootCompleted) {
                    return;
                }
            }
            mCarWatchdogDaemonHelper.notifySystemStateChange(
                    StateType.BOOT_PHASE, SystemService.PHASE_BOOT_COMPLETED, /* arg2= */ 0);
        } catch (RemoteException | RuntimeException e) {
            Slogf.w(TAG, "Cannot register to car watchdog daemon: %s", e);
        }
    }

    private void killProcessAndReportToMonitor(int pid) {
        String processName = getProcessName(pid);
        Process.killProcess(pid);
        Slogf.w(TAG, "carwatchdog killed %s (pid: %d)", processName, pid);
        try {
            mCarWatchdogDaemonHelper.tellDumpFinished(mCarWatchdogMonitor, pid);
        } catch (RemoteException | RuntimeException e) {
            Slogf.w(TAG, "Cannot report monitor result to car watchdog daemon: %s", 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) {
            Slogf.w(TAG, "Cannot read %s", filename);
            return unknownProcessName;
        }
    }

    private static native int nativeForceSuspend(int timeoutMs);

    // TODO(b/173664653): it's missing unit tests (for example, to make sure that
    // when its setSafetyMode() is called, mCarDevicePolicySafetyChecker is updated).
    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;
        }

        @Override
        public void setDisplayAllowlistForUser(@UserIdInt int userId, int[] displayIds) {
            mCarLaunchParamsModifier.setDisplayAllowListForUser(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);
        }

        @Override
        public int setPersistentActivity(ComponentName activity, int displayId, int featureId) {
            return mCarLaunchParamsModifier.setPersistentActivity(activity, displayId, featureId);
        }

        @Override
        public void setSafetyMode(boolean safe) {
            mCarDevicePolicySafetyChecker.setSafe(safe);
        }

        @Override
        public UserInfo createUserEvenWhenDisallowed(String name, String userType, int flags) {
            if (DBG) {
                Slogf.d(TAG, "createUserEvenWhenDisallowed(): name=%s, type=%s, flags=%s",
                        UserHelperLite.safeName(name), userType, UserInfo.flagsToString(flags));
            }
            UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
            try {
                UserInfo user = umi.createUserEvenWhenDisallowed(name, userType, flags,
                        /* disallowedPackages= */ null, /* token= */ null);
                if (DBG) {
                    Slogf.d(TAG, "User created: %s", (user == null ? "null" : user.toFullString()));
                }
                // TODO(b/172691310): decide if user should be affiliated when DeviceOwner is set
                return user;
            } catch (UserManager.CheckedUserOperationException e) {
                Slogf.e(TAG, "Error creating user", e);
                return null;
            }
        }

        @Override
        public void sendInitialUser(UserHandle user) {
            mCarServiceProxy.saveInitialUser(user);
        }
    }

    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);
        }
    }

    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) {
                Slogf.d(TAG, "Dumping and killing process(pid: %d)", 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) {
                Slogf.w(TAG, "Cannot get process information: %s", e);
                return;
            }
            nativePids.addAll(getInterestingNativePids());
            long startDumpTime = SystemClock.uptimeMillis();
            ActivityManagerService.dumpStackTraces(javaPids, null, null, nativePids, null);
            long dumpTime = SystemClock.uptimeMillis() - startDumpTime;
            if (DBG) {
                Slogf.d(TAG, "Dumping process took %dms", dumpTime);
            }
            // 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";
        }
    }

    private final class CarServiceConnectedCallback extends IResultReceiver.Stub {
        @Override
        public void send(int resultCode, Bundle resultData) {
            mHandler.removeMessages(WHAT_SERVICE_UNRESPONSIVE);

            IBinder binder;
            if (resultData == null
                    || (binder = resultData.getBinder(ICAR_SYSTEM_SERVER_CLIENT)) == null) {
                Slogf.wtf(TAG, "setSystemServerConnections return NULL Binder.");
                handleCarServiceUnresponsive();
                return;
            }

            ICarSystemServerClient carService = ICarSystemServerClient.Stub.asInterface(binder);
            mCarServiceProxy.handleCarServiceConnection(carService);
        }
    }
}
