/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.car;

import static android.car.user.CarUserManager.lifecycleEventTypeToString;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.os.Process.INVALID_UID;

import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.car.Car;
import android.car.builtin.content.ContextHelper;
import android.car.builtin.content.pm.PackageManagerHelper;
import android.car.builtin.os.UserManagerHelper;
import android.car.builtin.util.Slogf;
import android.car.user.CarUserManager.UserLifecycleEvent;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.hardware.automotive.vehicle.SubscribeOptions;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Process;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;

import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.common.CommonConstants.UserLifecycleEventType;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.KeyStore;
import java.security.KeyStore.SecretKeyEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

/** Utility class */
public final class CarServiceUtils {

    // https://developer.android.com/reference/java/util/UUID
    private static final int UUID_LENGTH = 16;
    private static final String TAG = CarLog.tagFor(CarServiceUtils.class);
    private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG);

    private static final String COMMON_HANDLER_THREAD_NAME =
            "CarServiceUtils_COMMON_HANDLER_THREAD";
    private static final byte[] CHAR_POOL_FOR_RANDOM_STRING =
            "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes();

    private static final String PACKAGE_NOT_FOUND = "Package not found:";
    private static final String ANDROID_KEYSTORE_NAME = "AndroidKeyStore";
    private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
    private static final int GCM_TAG_LENGTH = 128;

    /** K: class name, V: HandlerThread */
    private static final ArrayMap<String, HandlerThread> sHandlerThreads = new ArrayMap<>();

    @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE,
            details = "private constructor")
    private CarServiceUtils() {
        throw new UnsupportedOperationException("contains only static methods");
    }

    /**
     * Returns a byte buffer corresponding to the passed long argument.
     *
     * @param primitive data to convert format.
     */
    public static byte[] longToBytes(long primitive) {
        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
        buffer.putLong(primitive);
        return buffer.array();
    }

    /**
     * Returns a byte buffer corresponding to the passed long argument.
     *
     * @param array data to convert format.
     */
    public static long bytesToLong(byte[] array) {
        ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
        buffer.put(array);
        buffer.flip();
        long value = buffer.getLong();
        return value;
    }

    /**
     * Returns a String in Hex format that is formed from the bytes in the byte array
     * Useful for debugging
     *
     * @param array the byte array
     * @return the Hex string version of the input byte array
     */
    public static String byteArrayToHexString(byte[] array) {
        StringBuilder sb = new StringBuilder(array.length * 2);
        for (byte b : array) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    /**
     * Convert UUID to Big Endian byte array
     *
     * @param uuid UUID to convert
     * @return the byte array representing the UUID
     */
    @NonNull
    public static byte[] uuidToBytes(@NonNull UUID uuid) {

        return ByteBuffer.allocate(UUID_LENGTH)
                .order(ByteOrder.BIG_ENDIAN)
                .putLong(uuid.getMostSignificantBits())
                .putLong(uuid.getLeastSignificantBits())
                .array();
    }

    /**
     * Convert Big Endian byte array to UUID
     *
     * @param bytes byte array to convert
     * @return the UUID representing the byte array, or null if not a valid UUID
     */
    @Nullable
    public static UUID bytesToUUID(@NonNull byte[] bytes) {
        if (bytes.length != UUID_LENGTH) {
            return null;
        }

        ByteBuffer buffer = ByteBuffer.wrap(bytes);
        return new UUID(buffer.getLong(), buffer.getLong());
    }

    /**
     * Generate a random zero-filled string of given length
     *
     * @param length of string
     * @return generated string
     */
    @SuppressLint("DefaultLocale")  // Should always have the same format regardless of locale
    public static String generateRandomNumberString(int length) {
        return String.format("%0" + length + "d",
                ThreadLocalRandom.current().nextInt((int) Math.pow(10, length)));
    }

    /**
     * Concatentate the given 2 byte arrays
     *
     * @param a input array 1
     * @param b input array 2
     * @return concatenated array of arrays 1 and 2
     */
    @Nullable
    public static byte[] concatByteArrays(@Nullable byte[] a, @Nullable byte[] b) {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            if (a != null) {
                outputStream.write(a);
            }
            if (b != null) {
                outputStream.write(b);
            }
        } catch (IOException e) {
            return null;
        }
        return outputStream.toByteArray();
    }

    /**
     * Returns the content resolver for the given user. This can be used to put/get the
     * user's settings.
     *
     * @param context The context of the package.
     * @param userId The id of the user which the content resolver is being requested for. It also
     * accepts {@link UserHandle#USER_CURRENT}.
     */
    public static ContentResolver getContentResolverForUser(Context context,
            @UserIdInt int userId) {
        if (userId == UserHandle.CURRENT.getIdentifier()) {
            userId = ActivityManager.getCurrentUser();
        }
        return context
                .createContextAsUser(
                        UserHandle.of(userId), /* flags= */ 0)
                .getContentResolver();
    }

    /**
     * Checks if the type of the {@code event} matches {@code expectedType}.
     *
     * @param tag The tag for logging.
     * @param event The event to check the type against {@code expectedType}.
     * @param expectedType The expected event type.
     * @return true if {@code event}'s type matches {@code expectedType}.
     *         Otherwise, log a wtf and return false.
     */
    public static boolean isEventOfType(String tag, UserLifecycleEvent event,
            @UserLifecycleEventType int expectedType) {
        if (event.getEventType() == expectedType) {
            return true;
        }
        Slogf.wtf(tag, "Received an unexpected event: %s. Expected type: %s.", event,
                lifecycleEventTypeToString(expectedType));
        return false;
    }

    /**
     * Checks if the type of the {@code event} is one of the types in {@code expectedTypes}.
     *
     * @param tag The tag for logging.
     * @param event The event to check the type against {@code expectedTypes}.
     * @param expectedTypes The expected event types. Must not be empty.
     * @return true if {@code event}'s type can be found in {@code expectedTypes}.
     *         Otherwise, log a wtf and return false.
     */
    public static boolean isEventAnyOfTypes(String tag, UserLifecycleEvent event,
            @UserLifecycleEventType int... expectedTypes) {
        for (int i = 0; i < expectedTypes.length; i++) {
            if (event.getEventType() == expectedTypes[i]) {
                return true;
            }
        }
        StringJoiner expectedTyepsStringJoiner = new StringJoiner(",");
        for (int index = 0; index < expectedTypes.length; index++) {
            expectedTyepsStringJoiner.add(lifecycleEventTypeToString(expectedTypes[index]));
        }
        Slogf.wtf(tag, "Received an unexpected event: %s. Expected types: [%s]", event,
                expectedTyepsStringJoiner.toString());
        return false;
    }

    /**
     * Checks if the calling UID owns the give package.
     *
     * @throws SecurityException if the calling UID doesn't own the given package.
     */
    public static void checkCalledByPackage(Context context, String packageName) {
        int callingUid = Binder.getCallingUid();
        PackageManager pm = context.getPackageManager();
        int uidFromPm = INVALID_UID;
        try {
            uidFromPm = PackageManagerHelper.getPackageUidAsUser(pm, packageName,
                    UserManagerHelper.getUserId(callingUid));
        } catch (PackageManager.NameNotFoundException e) {
            String msg = PACKAGE_NOT_FOUND + packageName;
            throw new SecurityException(msg, e);
        }

        if (uidFromPm != callingUid) {
            throw new SecurityException(
                    "Package " + packageName + " is not associated to UID " + callingUid);
        }
    }

    /**
     * Execute a runnable on the main thread
     *
     * @param action The code to run on the main thread.
     */
    public static void runOnMain(Runnable action) {
        runOnLooper(Looper.getMainLooper(), action);
    }

    /**
     * Execute a runnable in the given looper
     * @param looper Looper to run the action.
     * @param action The code to run.
     */
    public static void runOnLooper(Looper looper, Runnable action) {
        new Handler(looper).post(action);
    }

    /**
     * Execute an empty runnable in the looper of the handler thread
     * specified by the name.
     *
     * @param name Name of the handler thread in which to run the empty
     *             runnable.
     */
    public static void runEmptyRunnableOnLooperSync(String name) {
        runOnLooperSync(getHandlerThread(name).getLooper(), () -> {});
    }

    /**
     * Execute a call on the application's main thread, blocking until it is
     * complete.  Useful for doing things that are not thread-safe, such as
     * looking at or modifying the view hierarchy.
     *
     * @param action The code to run on the main thread.
     */
    public static void runOnMainSync(Runnable action) {
        runOnLooperSync(Looper.getMainLooper(), action);
    }

    /**
     * Execute a delayed call on the application's main thread, blocking until it is
     * complete. See {@link #runOnMainSync(Runnable)}
     *
     * @param action The code to run on the main thread.
     * @param delayMillis The delay (in milliseconds) until the Runnable will be executed.
     */
    public static void runOnMainSyncDelayed(Runnable action, long delayMillis) {
        runOnLooperSyncDelayed(Looper.getMainLooper(), action, delayMillis);
    }

    /**
     * Execute a call on the given Looper thread, blocking until it is
     * complete.
     *
     * @param looper Looper to run the action.
     * @param action The code to run on the looper thread.
     */
    public static void runOnLooperSync(Looper looper, Runnable action) {
        runOnLooperSyncDelayed(looper, action, /* delayMillis */ 0L);
    }

    /**
     * Executes a delayed call on the given Looper thread, blocking until it is complete.
     *
     * @param looper Looper to run the action.
     * @param action The code to run on the looper thread.
     * @param delayMillis The delay (in milliseconds) until the Runnable will be executed.
     */
    public static void runOnLooperSyncDelayed(Looper looper, Runnable action, long delayMillis) {
        if (Looper.myLooper() == looper) {
            // requested thread is the same as the current thread. call directly.
            action.run();
        } else {
            Handler handler = new Handler(looper);
            SyncRunnable sr = new SyncRunnable(action);
            handler.postDelayed(sr, delayMillis);
            sr.waitForComplete();
        }
    }

    /**
     * Executes a runnable on the common thread. Useful for doing any kind of asynchronous work
     * across the car related code that doesn't need to be on the main thread.
     *
     * @param action The code to run on the common thread.
     */
    public static void runOnCommon(Runnable action) {
        runOnLooper(getCommonHandlerThread().getLooper(), action);
    }

    private static final class SyncRunnable implements Runnable {
        private final Runnable mTarget;
        private volatile boolean mComplete = false;

        public SyncRunnable(Runnable target) {
            mTarget = target;
        }

        @Override
        public void run() {
            mTarget.run();
            synchronized (this) {
                mComplete = true;
                notifyAll();
            }
        }

        public void waitForComplete() {
            synchronized (this) {
                while (!mComplete) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                    }
                }
            }
        }
    }

    public static float[] toFloatArray(List<Float> list) {
        int size = list.size();
        float[] array = new float[size];
        for (int i = 0; i < size; ++i) {
            array[i] = list.get(i);
        }
        return array;
    }

    public static long[] toLongArray(List<Long> list) {
        int size = list.size();
        long[] array = new long[size];
        for (int i = 0; i < size; ++i) {
            array[i] = list.get(i);
        }
        return array;
    }

    public static int[] toIntArray(List<Integer> list) {
        int size = list.size();
        int[] array = new int[size];
        for (int i = 0; i < size; ++i) {
            array[i] = list.get(i);
        }
        return array;
    }

    /**
     * Converts array to an array list
     */
    public static ArrayList<Integer> asList(int[] array) {
        Preconditions.checkArgument(array != null, "Array to convert to list can not be null");
        int size = array.length;
        ArrayList<Integer> results = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            results.add(array[i]);
        }
        return results;
    }

    public static byte[] toByteArray(List<Byte> list) {
        int size = list.size();
        byte[] array = new byte[size];
        for (int i = 0; i < size; ++i) {
            array[i] = list.get(i);
        }
        return array;
    }

    /**
     * Converts values array to array set
     */
    public static ArraySet<Integer> toIntArraySet(int[] values) {
        Preconditions.checkArgument(values != null,
                "Values to convert to array set must not be null");
        ArraySet<Integer> set = new ArraySet<>(values.length);
        for (int c = 0; c < values.length; c++) {
            set.add(values[c]);
        }

        return set;
    }

    /**
     * Converts int-value array set to values array
     */
    public static int[] toIntArray(ArraySet<Integer> set) {
        Preconditions.checkArgument(set != null,
                "Int array set to converted to array must not be null");
        int size = set.size();
        int[] array = new int[size];
        for (int i = 0; i < size; ++i) {
            array[i] = set.valueAt(i);
        }
        return array;
    }

    /**
     * Returns delta between elapsed time to uptime = {@link SystemClock#elapsedRealtime()} -
     * {@link SystemClock#uptimeMillis()}. Note that this value will be always >= 0.
     */
    public static long getUptimeToElapsedTimeDeltaInMillis() {
        int retry = 0;
        int max_retry = 2; // try only up to twice
        while (true) {
            long elapsed1 = SystemClock.elapsedRealtime();
            long uptime = SystemClock.uptimeMillis();
            long elapsed2 = SystemClock.elapsedRealtime();
            if (elapsed1 == elapsed2) { // avoid possible 1 ms fluctuation.
                return elapsed1 - uptime;
            }
            retry++;
            if (retry >= max_retry) {
                return elapsed1 - uptime;
            }
        }
    }

    /**
     * Gets a static instance of {@code HandlerThread} for the given {@code name}. If the thread
     * does not exist, create one and start it before returning.
     */
    public static HandlerThread getHandlerThread(String name) {
        synchronized (sHandlerThreads) {
            HandlerThread thread = sHandlerThreads.get(name);
            if (thread == null || !thread.isAlive()) {
                Slogf.i(TAG, "Starting HandlerThread:" + name);
                thread = new HandlerThread(name);
                thread.start();
                sHandlerThreads.put(name, thread);
            }
            return thread;
        }
    }

    /**
     * Gets the static instance of the common {@code HandlerThread} meant to be used across
     * CarService.
     */
    public static HandlerThread getCommonHandlerThread() {
        return getHandlerThread(COMMON_HANDLER_THREAD_NAME);
    }

    /**
     * Quits all the {@code HandlerThread} created via
     * {@link#getHandlerThread(String)}. This is useful only for testing.
     */
    @VisibleForTesting
    public static void quitHandlerThreads() throws InterruptedException {
        ArrayList<HandlerThread> threads;
        synchronized (sHandlerThreads) {
            threads = new ArrayList<>(sHandlerThreads.values());
        }
        for (int i = 0; i < threads.size(); i++) {
            var thread = threads.get(i);
            if (!thread.isAlive()) {
                continue;
            }
            if (thread.quitSafely()) {
                thread.join();
            }
        }
        synchronized (sHandlerThreads) {
            for (int i = 0; i < sHandlerThreads.size(); i++) {
                if (sHandlerThreads.valueAt(i).isAlive()) {
                    throw new IllegalStateException(
                            "Thread: " + sHandlerThreads.keyAt(i) + " is still alive after "
                            + "finishing all the tasks in the handler threads, maybe one of the "
                            + " pending task is creating a new handler thread?");
                }
            }
        }
    }

    /**
     * Assert if binder call is coming from system process like system server or if it is called
     * from its own process even if it is not system. The latter can happen in test environment.
     * Note that car service runs as system user but test like car service test will not.
     */
    public static void assertCallingFromSystemProcessOrSelf() {
        if (isCallingFromSystemProcessOrSelf()) {
            throw new SecurityException("Only allowed from system or self");
        }
    }

    /**
     * @return true if binder call is coming from system process like system server or if it is
     * called from its own process even if it is not system.
     */
    public static boolean isCallingFromSystemProcessOrSelf() {
        int uid = Binder.getCallingUid();
        int pid = Binder.getCallingPid();
        return uid != Process.SYSTEM_UID && pid != Process.myPid();
    }


    /** Utility for checking permission */
    public static void assertVehicleHalMockPermission(Context context) {
        assertPermission(context, Car.PERMISSION_MOCK_VEHICLE_HAL);
    }

    /** Utility for checking permission */
    public static void assertNavigationManagerPermission(Context context) {
        assertPermission(context, Car.PERMISSION_CAR_NAVIGATION_MANAGER);
    }

    /** Utility for checking permission */
    public static void assertClusterManagerPermission(Context context) {
        assertPermission(context, Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
    }

    /** Utility for checking permission */
    public static void assertPowerPermission(Context context) {
        assertPermission(context, Car.PERMISSION_CAR_POWER);
    }

    /** Utility for checking permission */
    public static void assertProjectionPermission(Context context) {
        assertPermission(context, Car.PERMISSION_CAR_PROJECTION);
    }

    /** Verify the calling context has the {@link Car#PERMISSION_CAR_PROJECTION_STATUS} */
    public static void assertProjectionStatusPermission(Context context) {
        assertPermission(context, Car.PERMISSION_CAR_PROJECTION_STATUS);
    }

    /** Utility for checking permission */
    public static void assertAnyDiagnosticPermission(Context context) {
        assertAnyPermission(context,
                Car.PERMISSION_CAR_DIAGNOSTIC_READ_ALL,
                Car.PERMISSION_CAR_DIAGNOSTIC_CLEAR);
    }

    /** Utility for checking permission */
    public static void assertDrivingStatePermission(Context context) {
        assertPermission(context, Car.PERMISSION_CAR_DRIVING_STATE);
    }

    /**
     * Verify the calling context has either {@link Car#PERMISSION_VMS_SUBSCRIBER} or
     * {@link Car#PERMISSION_VMS_PUBLISHER}
     */
    public static void assertAnyVmsPermission(Context context) {
        assertAnyPermission(context,
                Car.PERMISSION_VMS_SUBSCRIBER,
                Car.PERMISSION_VMS_PUBLISHER);
    }

    /** Utility for checking permission */
    public static void assertVmsPublisherPermission(Context context) {
        assertPermission(context, Car.PERMISSION_VMS_PUBLISHER);
    }

    /** Utility for checking permission */
    public static void assertVmsSubscriberPermission(Context context) {
        assertPermission(context, Car.PERMISSION_VMS_SUBSCRIBER);
    }

    /** Utility for checking permission */
    public static void assertPermission(Context context, String permission) {
        if (context.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("requires " + permission);
        }
    }

    /**
     * Checks to see if the caller has a permission.
     *
     * @return boolean TRUE if caller has the permission.
     */
    public static boolean hasPermission(Context context, String permission) {
        return context.checkCallingOrSelfPermission(permission)
                == PackageManager.PERMISSION_GRANTED;
    }

    /** Utility for checking permission */
    public static void assertAnyPermission(Context context, String... permissions) {
        for (String permission : permissions) {
            if (context.checkCallingOrSelfPermission(permission)
                    == PackageManager.PERMISSION_GRANTED) {
                return;
            }
        }
        throw new SecurityException("requires any of " + Arrays.toString(permissions));
    }

    /**
     * Turns a {@code SubscribeOptions} to {@code
     * android.hardware.automotive.vehicle.V2_0.SubscribeOptions}
     */
    public static android.hardware.automotive.vehicle.V2_0.SubscribeOptions subscribeOptionsToHidl(
            SubscribeOptions options) {
        android.hardware.automotive.vehicle.V2_0.SubscribeOptions hidlOptions =
                new android.hardware.automotive.vehicle.V2_0.SubscribeOptions();
        hidlOptions.propId = options.propId;
        hidlOptions.sampleRate = options.sampleRate;
        // HIDL backend requires flags to be set although it is not used any more.
        hidlOptions.flags = android.hardware.automotive.vehicle.V2_0.SubscribeFlags.EVENTS_FROM_CAR;
        // HIDL backend does not support area IDs, so we ignore options.areaId field.
        return hidlOptions;
    }

    /**
     * Returns {@code true} if the current configuration supports multiple users on multiple
     * displays.
     */
    public static boolean isMultipleUsersOnMultipleDisplaysSupported(UserManager userManager) {
        return UserManagerHelper.isVisibleBackgroundUsersSupported(userManager);
    }

    /**
     * Returns {@code true} if the current configuration supports visible background users on
     * default display.
     */
    public static boolean isVisibleBackgroundUsersOnDefaultDisplaySupported(
            UserManager userManager) {
        return UserManagerHelper.isVisibleBackgroundUsersOnDefaultDisplaySupported(userManager);
    }

    /**
     * Starts Activity for the given {@code userId} and {@code displayId}.
     *
     * @return {@code true} when starting activity succeeds. It can fail in situation like secondary
     *         home package not existing.
     */
    public static boolean startHomeForUserAndDisplay(Context context,
            @UserIdInt int userId, int displayId) {
        if (DBG) {
            Slogf.d(TAG, "Starting HOME for user: %d, display:%d", userId, displayId);
        }
        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
                .addCategory(Intent.CATEGORY_HOME);
        ActivityOptions activityOptions = ActivityOptions.makeBasic()
                .setLaunchDisplayId(displayId);
        try {
            ContextHelper.startActivityAsUser(context, homeIntent, activityOptions.toBundle(),
                    UserHandle.of(userId));
            if (DBG) {
                Slogf.d(TAG, "Started HOME for user: %d, display:%d", userId, displayId);
            }
            return true;
        } catch (Exception e) {
            Slogf.w(TAG, e, "Cannot start HOME for user: %d, display:%d", userId, displayId);
            return false;
        }
    }

    /**
     * Starts SystemUI component for a particular user - should be called for non-current user only.
     *
     * @return {@code true} when starting service succeeds. It can fail in situation like the
     * SystemUI service component not being defined.
     */
    public static boolean startSystemUiForUser(Context context, @UserIdInt int userId) {
        if (DBG) Slogf.d(TAG, "Start SystemUI for user: %d", userId);
        Preconditions.checkArgument(userId != UserHandle.SYSTEM.getIdentifier(),
                "Cannot start SystemUI for the system user");
        Preconditions.checkArgument(userId != ActivityManager.getCurrentUser(),
                "Cannot start SystemUI for the current foreground user");

        // TODO (b/261192740): add EventLog for SystemUI starting
        ComponentName sysuiComponent = PackageManagerHelper.getSystemUiServiceComponent(context);
        Intent sysUIIntent = new Intent().setComponent(sysuiComponent);
        try {
            context.bindServiceAsUser(sysUIIntent, sEmptyServiceConnection,
                    Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.of(userId));
            return true;
        } catch (Exception e) {
            Slogf.w(TAG, e, "Cannot start SysUI component %s for user %d", sysuiComponent,
                    userId);
            return false;
        }
    }

    // The callbacks are not called actually, because SystemUI returns null for IBinder.
    private static final ServiceConnection sEmptyServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {}

        @Override
        public void onServiceDisconnected(ComponentName name) {}
    };

    /**
     * Stops the SystemUI component for a particular user - this function should not be called
     * for the system user.
     */
    public static void stopSystemUiForUser(Context context, @UserIdInt int userId) {
        Preconditions.checkArgument(userId != UserHandle.SYSTEM.getIdentifier(),
                "Cannot stop SystemUI for the system user");
        // TODO (b/261192740): add EventLog for SystemUI stopping
        String sysUiPackage = PackageManagerHelper.getSystemUiPackageName(context);
        PackageManagerHelper.forceStopPackageAsUserEvenWhenStopping(context, sysUiPackage, userId);
    }

    /**
     * Starts UserPickerActivity for the given {@code userId} and {@code displayId}.
     *
     * @return {@code true} when starting activity succeeds. It can fail in situation like
     * package not existing.
     */
    public static boolean startUserPickerOnDisplay(Context context,
            int displayId, String userPickerActivityPackage) {
        if (DBG) {
            Slogf.d(TAG, "Starting user picker on display:%d", displayId);
        }
        // FLAG_ACTIVITY_MULTIPLE_TASK ensures the user picker can show up on multiple displays.
        Intent intent = new Intent()
                .setComponent(ComponentName.unflattenFromString(
                    userPickerActivityPackage))
                .addFlags(FLAG_ACTIVITY_NEW_TASK)
                .setData(Uri.parse("data://com.android.car/userpicker/display" + displayId));
        ActivityOptions activityOptions = ActivityOptions.makeBasic()
                .setLaunchDisplayId(displayId);
        try {
            // Start the user picker as user 0.
            ContextHelper.startActivityAsUser(context, intent, activityOptions.toBundle(),
                    UserHandle.SYSTEM);
            return true;
        } catch (Exception e) {
            Slogf.w(TAG, e, "Cannot start user picker as user 0 on display:%d", displayId);
            return false;
        }
    }

    /**
     * Generates a random string which consists of captial letters and numbers.
     */
    @SuppressLint("DefaultLocale")  // Should always have the same format regardless of locale
    public static String generateRandomAlphaNumericString(int length) {
        StringBuilder sb = new StringBuilder();

        int poolSize = CHAR_POOL_FOR_RANDOM_STRING.length;
        for (int i = 0; i < length; i++) {
            sb.append(CHAR_POOL_FOR_RANDOM_STRING[ThreadLocalRandom.current().nextInt(poolSize)]);
        }
        return sb.toString();
    }

    /**
     * Encrypts byte array with the keys stored in {@code keyAlias} using AES.
     *
     * @return Encrypted data and initialization vector in {@link EncryptedData}. {@code null} in
     *         case of errors.
     */
    @Nullable
    public static EncryptedData encryptData(byte[] data, String keyAlias) {
        SecretKey secretKey = getOrCreateSecretKey(keyAlias);
        if (secretKey == null) {
            Slogf.e(TAG, "Failed to encrypt data: cannot get a secret key (keyAlias: %s)",
                    keyAlias);
            return null;
        }
        try {
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            return new EncryptedData(cipher.doFinal(data), cipher.getIV());
        } catch (Exception e) {
            Slogf.e(TAG, e, "Failed to encrypt data: keyAlias=%s", keyAlias);
            return null;
        }
    }

    /**
     * Decrypts byte array with the keys stored in {@code keyAlias} using AES.
     *
     * @return Decrypted data in byte array. {@code null} in case of errors.
     */
    @Nullable
    public static byte[] decryptData(EncryptedData data, String keyAlias) {
        SecretKey secretKey = getOrCreateSecretKey(keyAlias);
        if (secretKey == null) {
            Slogf.e(TAG, "Failed to decrypt data: cannot get a secret key (keyAlias: %s)",
                    keyAlias);
            return null;
        }
        try {
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
            GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, data.getIv());
            cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
            return cipher.doFinal(data.getEncryptedData());
        } catch (Exception e) {
            Slogf.e(TAG, e, "Failed to decrypt data: keyAlias=%s", keyAlias);
            return null;
        }
    }

    /**
     * Class to hold encrypted data and its initialization vector.
     */
    public static final class EncryptedData {
        private final byte[] mEncryptedData;
        private final byte[] mIv;

        public EncryptedData(byte[] encryptedData, byte[] iv) {
            mEncryptedData = encryptedData;
            mIv = iv;
        }

        public byte[] getEncryptedData() {
            return mEncryptedData;
        }

        public byte[] getIv() {
            return mIv;
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) return true;
            if (!(other instanceof EncryptedData)) return false;
            EncryptedData data = (EncryptedData) other;
            return Arrays.equals(mEncryptedData, data.mEncryptedData)
                    && Arrays.equals(mIv, data.mIv);
        }

        @Override
        public int hashCode() {
            return Objects.hash(Arrays.hashCode(mEncryptedData), Arrays.hashCode(mIv));
        }
    }

    @Nullable
    private static SecretKey getOrCreateSecretKey(String keyAlias) {
        try {
            KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE_NAME);
            keyStore.load(/* KeyStore.LoadStoreParameter= */ null);
            if (keyStore.containsAlias(keyAlias)) {
                SecretKeyEntry secretKeyEntry = (SecretKeyEntry) keyStore.getEntry(keyAlias,
                        /* protParam= */ null);
                if (secretKeyEntry != null) {
                    return secretKeyEntry.getSecretKey();
                }
                Slogf.e(TAG, "Android key store contains the alias (%s) but the secret key "
                        + "entry is null", keyAlias);
                return null;
            }
            KeyGenerator keyGenerator = KeyGenerator.getInstance(
                    KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE_NAME);
            KeyGenParameterSpec keyGenParameterSpec =
                    new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_ENCRYPT
                            | KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .build();
            keyGenerator.init(keyGenParameterSpec);
            return keyGenerator.generateKey();
        } catch (Exception e) {
            Slogf.e(TAG, "Failed to get or create a secret key for the alias (%s)", keyAlias);
            return null;
        }
    }
}
