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

package android.car.admin;

import static android.os.Process.myUid;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.car.Car;
import android.car.CarManagerBase;
import android.car.SyncResultCallback;
import android.car.builtin.util.EventLogHelper;
import android.car.user.UserCreationResult;
import android.car.user.UserRemovalResult;
import android.car.user.UserStartResult;
import android.car.user.UserStopResult;
import android.car.util.concurrent.AndroidFuture;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Slog;

import com.android.car.internal.ResultCallbackImpl;
import com.android.car.internal.common.UserHelperLite;
import com.android.car.internal.os.CarSystemProperties;
import com.android.internal.annotations.VisibleForTesting;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Public interface for managing policies enforced on a device.
 *
 * <p>This is a sub-set of {@link android.app.admin.DevicePolicyManager}, but with the following
 * differences:
 *
 * <ol>
 *   <li>Its methods take in consideration driver-safety restrictions.
 *   <li>Callers don't need to be a {@code DPC}, but rather have the proper permissions.
 * </ol>
 *
 * @hide
 */
@SystemApi
public final class CarDevicePolicyManager extends CarManagerBase {

    /**
     * @hide
     */
    @VisibleForTesting
    public static final String TAG = CarDevicePolicyManager.class.getSimpleName();

    private final ICarDevicePolicyService mService;

    private static final String PREFIX_USER_TYPE = "USER_TYPE_";

    /**
     * Type used to indicate the user is a regular user.
     */
    public static final int USER_TYPE_REGULAR = 0;

    /**
     * Type used to indicate the user is an admin user.
     */
    public static final int USER_TYPE_ADMIN = 1;

    /**
     * Type used to indicate the user is a guest user.
     */
    public static final int USER_TYPE_GUEST = 2;

    /** @hide - Used on test cases only */
    public static final int FIRST_USER_TYPE = USER_TYPE_REGULAR;
    /** @hide - Used on test cases only */
    public static final int LAST_USER_TYPE = USER_TYPE_GUEST;

    private static final int DEVICE_POLICY_MANAGER_TIMEOUT_MS =
            CarSystemProperties.getDevicePolicyManagerTimeout().orElse(60_000);
    private static final int REMOVE_USER_CALL_TIMEOUT_MS = 60_000;

    /** @hide */
    @IntDef(prefix = PREFIX_USER_TYPE, value = {
            USER_TYPE_REGULAR,
            USER_TYPE_ADMIN,
            USER_TYPE_GUEST
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface UserType {
    }

    /**
     * @hide
     */
    public CarDevicePolicyManager(@NonNull Car car, @NonNull IBinder service) {
        this(car, ICarDevicePolicyService.Stub.asInterface(service));
    }

    /**
     * @hide
     */
    @VisibleForTesting
    public CarDevicePolicyManager(@NonNull Car car, @NonNull ICarDevicePolicyService service) {
        super(car);
        mService = service;
    }

    /**
     * Removes the given user.
     *
     * <p><b>Note: </b>if the caller user is not an admin, it can only remove itself
     * (otherwise it will fail with {@link RemoveUserResult#STATUS_FAILURE_INVALID_ARGUMENTS}).
     *
     * @param user identification of the user to be removed.
     *
     * @return whether the user was successfully removed.
     *
     * @hide
     */
    @SystemApi
    @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
            android.Manifest.permission.CREATE_USERS})
    @NonNull
    @SuppressLint("VisibleForTests")
    public RemoveUserResult removeUser(@NonNull UserHandle user) {
        Objects.requireNonNull(user, "user cannot be null");

        int userId = user.getIdentifier();
        int uid = myUid();
        EventLogHelper.writeCarDevicePolicyManagerRemoveUserReq(uid, userId);
        UserRemovalResult userRemovalResult = new UserRemovalResult(
                UserRemovalResult.STATUS_ANDROID_FAILURE);
        try {
            SyncResultCallback<UserRemovalResult> userRemovalResultCallback =
                    new SyncResultCallback<>();
            ResultCallbackImpl<UserRemovalResult> resultCallbackImpl = new ResultCallbackImpl<>(
                    Runnable::run, userRemovalResultCallback);
            mService.removeUser(user.getIdentifier(), resultCallbackImpl);

            userRemovalResult = userRemovalResultCallback.get(REMOVE_USER_CALL_TIMEOUT_MS,
                    TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Slog.e(TAG, "CarDevicePolicyManager removeUser(user): ", e);
        } catch (TimeoutException e) {
            Slog.e(TAG, "CarDevicePolicyManager removeUser(user): ", e);
        } catch (RemoteException e) {
            return handleRemoteExceptionFromCarService(e,
                    new RemoveUserResult(UserRemovalResult.STATUS_ANDROID_FAILURE));
        } finally {
            EventLogHelper.writeCarDevicePolicyManagerRemoveUserResp(uid,
                    userRemovalResult.getStatus());
        }
        return new RemoveUserResult(userRemovalResult.getStatus());
    }

    /**
     * Creates a user with the given characteristics.
     *
     * <p><b>Note: </b>if the caller user is not an admin, it can only create non-admin users
     * (otherwise it will fail with {@link CreateUserResult#STATUS_FAILURE_INVALID_ARGUMENTS}).
     *
     * @param name user name.
     * @param type either {@link #USER_TYPE_REGULAR}, {@link #USER_TYPE_ADMIN},
     * or {@link #USER_TYPE_GUEST}.
     *
     * @return whether the user was successfully removed.
     *
     * @hide
     */
    @SystemApi
    @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
            android.Manifest.permission.CREATE_USERS})
    @NonNull
    public CreateUserResult createUser(@Nullable String name, @UserType int type) {
        int uid = myUid();
        EventLogHelper.writeCarDevicePolicyManagerCreateUserReq(uid, UserHelperLite.safeName(name),
                type);
        int status = CreateUserResult.STATUS_FAILURE_GENERIC;
        try {
            SyncResultCallback<UserCreationResult> userCreationResultCallback =
                    new SyncResultCallback<>();

            ResultCallbackImpl<UserCreationResult> resultCallbackImpl = new ResultCallbackImpl(
                    Runnable::run, userCreationResultCallback);

            mService.createUser(name, type, resultCallbackImpl);

            UserCreationResult result = userCreationResultCallback.get(
                    DEVICE_POLICY_MANAGER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
            status = result.getStatus();
            return new CreateUserResult(result);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return CreateUserResult.forGenericError();
        } catch (TimeoutException e) {
            return CreateUserResult.forGenericError();
        } catch (RemoteException e) {
            return handleRemoteExceptionFromCarService(e, CreateUserResult.forGenericError());
        } finally {
            EventLogHelper.writeCarDevicePolicyManagerCreateUserResp(uid, status);
        }
    }

    /**
     * Starts a user in the background.
     *
     * @param user identification of the user to be started.
     *
     * @return whether the user was successfully started.
     *
     * @deprecated Use {@link android.car.user.CarUserManager#startUser(UserStartRequest)} instead.
     * @hide
     */
    @TestApi
    @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
            android.Manifest.permission.CREATE_USERS})
    @NonNull
    @Deprecated
    public StartUserInBackgroundResult startUserInBackground(@NonNull UserHandle user) {
        Objects.requireNonNull(user, "user cannot be null");

        int userId = user.getIdentifier();
        int uid = myUid();
        EventLogHelper.writeCarDevicePolicyManagerStartUserInBackgroundReq(uid, userId);
        int status = StartUserInBackgroundResult.STATUS_FAILURE_GENERIC;
        try {
            AndroidFuture<UserStartResult> future = new AndroidFuture<>();
            mService.startUserInBackground(userId, future);
            UserStartResult result = future.get(DEVICE_POLICY_MANAGER_TIMEOUT_MS,
                    TimeUnit.MILLISECONDS);
            status = result.getStatus();
            return new StartUserInBackgroundResult(status);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return new StartUserInBackgroundResult(status);
        } catch (ExecutionException | TimeoutException e) {
            return new StartUserInBackgroundResult(status);
        } catch (RemoteException e) {
            return handleRemoteExceptionFromCarService(e, new StartUserInBackgroundResult(status));
        } finally {
            EventLogHelper.writeCarDevicePolicyManagerStartUserInBackgroundResp(uid, status);
        }
    }

    /**
     * Stops the given user.
     *
     * @param user identification of the user to stop.
     *
     * @return whether the user was successfully stopped.
     *
     * @hide
     * @deprecated Use {@link android.car.user.CarUserManager#stopUser(UserStopRequest)} instead.
     */
    @TestApi
    @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
            android.Manifest.permission.CREATE_USERS})
    @NonNull
    @Deprecated
    public StopUserResult stopUser(@NonNull UserHandle user) {
        Objects.requireNonNull(user, "user cannot be null");

        int userId = user.getIdentifier();
        int uid = myUid();
        EventLogHelper.writeCarDevicePolicyManagerStopUserReq(uid, userId);
        int status = StopUserResult.STATUS_FAILURE_GENERIC;
        try {
            AndroidFuture<UserStopResult> future = new AndroidFuture<>();
            mService.stopUser(userId, future);
            UserStopResult result =
                    future.get(DEVICE_POLICY_MANAGER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
            status = result.getStatus();
            return new StopUserResult(status);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return new StopUserResult(status);
        } catch (ExecutionException | TimeoutException e) {
            return new StopUserResult(status);
        } catch (RemoteException e) {
            return handleRemoteExceptionFromCarService(e, new StopUserResult(status));
        } finally {
            EventLogHelper.writeCarDevicePolicyManagerStopUserResp(uid, status);
        }
    }

    /** @hide */
    public void setUserDisclaimerShown(@NonNull UserHandle user) {
        Objects.requireNonNull(user, "user cannot be null");
        try {
            mService.setUserDisclaimerShown(user.getIdentifier());
        } catch (RemoteException e) {
            handleRemoteExceptionFromCarService(e, null);
        }
    }

    /** @hide */
    public void setUserDisclaimerAcknowledged(@NonNull UserHandle user) {
        Objects.requireNonNull(user, "user cannot be null");
        try {
            mService.setUserDisclaimerAcknowledged(user.getIdentifier());
        } catch (RemoteException e) {
            handleRemoteExceptionFromCarService(e, null);
        }
    }

    /** @hide */
    @Override
    public void onCarDisconnected() {
        // nothing to do
    }
}
