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

import android.annotation.CallbackExecutor;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.PackageInfoFlags;
import android.devicelock.DeviceLockManager;
import android.devicelock.IDeviceLockService;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.OutcomeReceiver;
import android.os.RemoteCallback;
import android.os.RemoteException;

import androidx.annotation.NonNull;

import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * Implementation for SystemDeviceLockManager.
 */
public final class SystemDeviceLockManagerImpl implements SystemDeviceLockManager {
    private static final String TAG = "SystemDeviceLockManagerImpl";

    private final IDeviceLockService mIDeviceLockService;
    private final Context mContext;

    private SystemDeviceLockManagerImpl(Context context) {
        mContext = context;
        final DeviceLockManager deviceLockManager = context.getSystemService(
                DeviceLockManager.class);

        mIDeviceLockService = deviceLockManager.getService();
    }

    private SystemDeviceLockManagerImpl() {
        this(DeviceLockControllerApplication.getAppContext());
    }

    // Initialization-on-demand holder.
    private static final class SystemDeviceLockManagerHolder {
        static final SystemDeviceLockManagerImpl sSystemDeviceLockManager =
                new SystemDeviceLockManagerImpl();
    }

    /**
     * Returns the lazily initialized singleton instance of the SystemDeviceLockManager.
     */
    public static SystemDeviceLockManager getInstance() {
        return SystemDeviceLockManagerHolder.sSystemDeviceLockManager;
    }

    @Override
    public void addFinancedDeviceKioskRole(@NonNull String packageName,
            @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mIDeviceLockService.addFinancedDeviceKioskRole(packageName,
                    new RemoteCallback(result -> executor.execute(() -> {
                        processResult(result, callback,
                                "Failed to add financed role to: " + packageName);
                    }), new Handler(Looper.getMainLooper())));
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    @Override
    public void removeFinancedDeviceKioskRole(@NonNull String packageName,
            @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mIDeviceLockService.removeFinancedDeviceKioskRole(packageName,
                    new RemoteCallback(result -> executor.execute(() -> {
                        processResult(result, callback,
                                "Failed to remove financed role from: " + packageName);
                    }), new Handler(Looper.getMainLooper())));
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    @Override
    public void setDlcExemptFromActivityBgStartRestrictionState(boolean exempt,
            @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mIDeviceLockService.setCallerExemptFromActivityBgStartRestrictionState(exempt,
                    new RemoteCallback(result -> executor.execute(() -> {
                        processResult(result, callback,
                                "Failed to change exempt from activity background start to: "
                                        + (exempt ? "exempt" : "non exempt"));
                    }), new Handler(Looper.getMainLooper())));
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    @Override
    public void setDlcAllowedToSendUndismissibleNotifications(boolean allowed,
            @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mIDeviceLockService.setCallerAllowedToSendUndismissibleNotifications(allowed,
                    new RemoteCallback(result -> executor.execute(() -> {
                        processResult(result, callback,
                                "Failed to change undismissible notifs allowed to: "
                                        + (allowed ? "allowed" : "not allowed"));
                    }), new Handler(Looper.getMainLooper())));
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    @Override
    public void setKioskAppExemptFromRestrictionsState(String packageName, boolean exempt,
            Executor executor, @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            int kioskUid = mContext.getPackageManager().getPackageUid(packageName,
                    PackageInfoFlags.of(0));
            mIDeviceLockService.setUidExemptFromRestrictionsState(kioskUid, exempt,
                    new RemoteCallback(result -> executor.execute(() -> {
                        processResult(result, callback, "Failed to exempt for UID: " + kioskUid);
                    }), new Handler(Looper.getMainLooper())));
        } catch (RemoteException | PackageManager.NameNotFoundException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    @Override
    public void enableKioskKeepalive(String packageName, Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(packageName);
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mIDeviceLockService.enableKioskKeepalive(packageName,
                    new RemoteCallback(result -> executor.execute(() -> {
                        processResult(result, callback,
                                "Failed to enable kiosk keep-alive for package: "
                                        + packageName);
                    }), new Handler(Looper.getMainLooper())));
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    @Override
    public void disableKioskKeepalive(Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mIDeviceLockService.disableKioskKeepalive(
                    new RemoteCallback(result -> executor.execute(() -> {
                        processResult(result, callback, "Failed to disable kiosk keep-alive");
                    }), new Handler(Looper.getMainLooper())));
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    @Override
    public void enableControllerKeepalive(Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mIDeviceLockService.enableControllerKeepalive(
                    new RemoteCallback(result -> executor.execute(() -> {
                        processResult(result, callback, "Failed to enable controller keep-alive");
                    }), new Handler(Looper.getMainLooper())));
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    @Override
    public void disableControllerKeepalive(Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mIDeviceLockService.disableControllerKeepalive(
                    new RemoteCallback(result -> executor.execute(() -> {
                        processResult(result, callback, "Failed to disable controller keep-alive");
                    }), new Handler(Looper.getMainLooper())));
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    @Override
    public void setDeviceFinalized(boolean finalized, Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mIDeviceLockService.setDeviceFinalized(finalized,
                    new RemoteCallback(result -> executor.execute(() -> {
                        processResult(result, callback, "Failed to set device finalized");
                    }), new Handler(Looper.getMainLooper())));
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    @Override
    public void setPostNotificationsSystemFixed(boolean systemFixed, Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);

        try {
            mIDeviceLockService.setPostNotificationsSystemFixed(systemFixed,
                    new RemoteCallback(result -> executor.execute(() -> {
                        processResult(result, callback, "Failed to change POST_NOTIFICATIONS "
                                + "SYSTEM_FIXED flag to: " + systemFixed);
                    }), new Handler(Looper.getMainLooper())));
        } catch (RemoteException e) {
            executor.execute(() -> callback.onError(new RuntimeException(e)));
        }
    }

    private static void processResult(Bundle result,
            @NonNull OutcomeReceiver<Void, Exception> callback, String message) {
        final boolean remoteCallbackResult = result.getBoolean(
                IDeviceLockService.KEY_REMOTE_CALLBACK_RESULT);
        if (remoteCallbackResult) {
            callback.onResult(null /* result */);
        } else {
            callback.onError(new Exception(message));
        }
    }
}
