/*
 * Copyright (C) 2022 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.devicelock;

import android.Manifest.permission;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresFeature;
import android.annotation.RequiresNoPermission;
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
import android.text.TextUtils;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * Manager used to interact with the system device lock service.
 * The device lock feature is used by special applications ('kiosk apps', downloaded and installed
 * by the device lock solution) to lock and unlock a device.
 * A typical use case is a financed device, where the financing entity has the capability to lock
 * the device in case of a missed payment.
 * When a device is locked, only a limited set of interactions with the device is allowed (for
 * example, placing emergency calls).
 * <p>
 * Use {@link android.content.Context#getSystemService(java.lang.String)}
 * with {@link Context#DEVICE_LOCK_SERVICE} to create a {@link DeviceLockManager}.
 * </p>
 *
 */
@SystemService(Context.DEVICE_LOCK_SERVICE)
@RequiresFeature(PackageManager.FEATURE_DEVICE_LOCK)
public final class DeviceLockManager {
    private static final String TAG = "DeviceLockManager";
    private final IDeviceLockService mService;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = "DEVICE_LOCK_ROLE_", value = {
        DEVICE_LOCK_ROLE_FINANCING,
    })
    public @interface DeviceLockRole {}

    /**
     * Constant representing a financed device role, returned by {@link #getKioskApps}.
     */
    public static final int DEVICE_LOCK_ROLE_FINANCING = 0;

    /**
     * @hide
     */
    public DeviceLockManager(Context context, IDeviceLockService service) {
        mService = service;
    }

    /**
     * Return the underlying service interface.
     * This is used to implement private APIs between the Device Lock Controller and the
     * Device Lock System Service.
     *
     * @hide
     */
    @NonNull
    public IDeviceLockService getService() {
        return mService;
    }

    /**
     * Lock the device.
     *
     * @param executor the {@link Executor} on which to invoke the callback.
     * @param callback this returns either success or an exception.
     */
    @RequiresPermission(permission.MANAGE_DEVICE_LOCK_STATE)
    public void lockDevice(@NonNull @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mService.lockDevice(
                    new ILockUnlockDeviceCallback.Stub() {
                        @Override
                        public void onDeviceLockedUnlocked() {
                            executor.execute(() -> callback.onResult(null));
                        }

                        @Override
                        public void onError(ParcelableException parcelableException) {
                            callback.onError(parcelableException.getException());
                        }
                    });
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    /**
     * Unlock the device.
     *
     * @param executor the {@link Executor} on which to invoke the callback.
     * @param callback this returns either success or an exception.
     */
    @RequiresPermission(permission.MANAGE_DEVICE_LOCK_STATE)
    public void unlockDevice(@NonNull @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mService.unlockDevice(
                    new ILockUnlockDeviceCallback.Stub() {
                        @Override
                        public void onDeviceLockedUnlocked() {
                            executor.execute(() -> callback.onResult(null));
                        }

                        @Override
                        public void onError(ParcelableException parcelableException) {
                            callback.onError(parcelableException.getException());
                        }
                    });
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    /**
     * Check if the device is locked or not.
     *
     * @param executor the {@link Executor} on which to invoke the callback.
     * @param callback this returns either the lock status or an exception.
     */
    @RequiresPermission(permission.MANAGE_DEVICE_LOCK_STATE)
    public void isDeviceLocked(@NonNull @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<Boolean, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mService.isDeviceLocked(
                    new IIsDeviceLockedCallback.Stub() {
                        @Override
                        public void onIsDeviceLocked(boolean locked) {
                            executor.execute(() -> callback.onResult(locked));
                        }

                        @Override
                        public void onError(ParcelableException parcelableException) {
                            executor.execute(() ->
                                    callback.onError(parcelableException.getException()));
                        }
                    });
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    /**
     * Get the device id.
     *
     * @param executor the {@link Executor} on which to invoke the callback.
     * @param callback this returns either the {@link DeviceId} or an exception.
     */
    @RequiresPermission(permission.MANAGE_DEVICE_LOCK_STATE)
    public void getDeviceId(@NonNull @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<DeviceId, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mService.getDeviceId(
                    new IGetDeviceIdCallback.Stub() {
                        @Override
                        public void onDeviceIdReceived(int type, String id) {
                            if (TextUtils.isEmpty(id)) {
                                executor.execute(() -> {
                                    callback.onError(new Exception("Cannot get device id (empty)"));
                                });
                            } else {
                                executor.execute(() -> {
                                    callback.onResult(new DeviceId(type, id));
                                });
                            }
                        }

                        @Override
                        public void onError(ParcelableException parcelableException) {
                            callback.onError(parcelableException.getException());
                        }
                    }
            );
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    /**
     * Get the kiosk app roles and packages.
     *
     * @param executor the {@link Executor} on which to invoke the callback.
     * @param callback this returns either a {@link Map} of device roles/package names,
     *                 or an exception. The Integer in the map represent the device lock role
     *                 (at this moment, the only supported role is
     *                 {@value #DEVICE_LOCK_ROLE_FINANCING}. The String represents tha package
     *                 name of the kiosk app for that role.
     */
    @RequiresNoPermission
    public void getKioskApps(@NonNull @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<Map<Integer, String>, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mService.getKioskApps(
                    new IGetKioskAppsCallback.Stub() {
                        @Override
                        public void onKioskAppsReceived(Map kioskApps) {
                            executor.execute(() -> callback.onResult(kioskApps));
                        }

                        @Override
                        public void onError(ParcelableException parcelableException) {
                            callback.onError(parcelableException.getException());
                        }
                    }
            );
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }
}
