/*
 * 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.server.display;

import static android.hardware.display.DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED;
import static android.os.Temperature.THROTTLING_CRITICAL;
import static android.os.Temperature.THROTTLING_NONE;
import static android.view.Display.TYPE_EXTERNAL;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.display.DisplayManagerGlobal;
import android.hardware.display.DisplayManagerGlobal.DisplayEvent;
import android.os.Build;
import android.os.Handler;
import android.os.IThermalEventListener;
import android.os.IThermalService;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.Temperature;
import android.os.Temperature.ThrottlingStatus;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.display.DisplayManagerService.SyncRoot;
import com.android.server.display.feature.DisplayManagerFlags;
import com.android.server.display.notifications.DisplayNotificationManager;
import com.android.server.display.utils.DebugUtils;

import java.util.HashSet;
import java.util.Set;

/**
 * Listens for Skin thermal sensor events, disables external displays if thermal status becomes
 * equal or above {@link android.os.Temperature#THROTTLING_CRITICAL}, enables external displays if
 * status goes below {@link android.os.Temperature#THROTTLING_CRITICAL}.
 */
class ExternalDisplayPolicy {
    private static final String TAG = "ExternalDisplayPolicy";

    // To enable these logs, run:
    // 'adb shell setprop persist.log.tag.ExternalDisplayPolicy DEBUG && adb reboot'
    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);

    @VisibleForTesting
    static final String ENABLE_ON_CONNECT = "persist.sys.display.enable_on_connect.external";

    static boolean isExternalDisplayLocked(@NonNull final LogicalDisplay logicalDisplay) {
        return logicalDisplay.getDisplayInfoLocked().type == TYPE_EXTERNAL;
    }

    /**
     * Injector interface for {@link ExternalDisplayPolicy}
     */
    interface Injector {
        void sendExternalDisplayEventLocked(@NonNull LogicalDisplay display,
                @DisplayEvent int event);

        @NonNull
        LogicalDisplayMapper getLogicalDisplayMapper();

        @NonNull
        SyncRoot getSyncRoot();

        @Nullable
        IThermalService getThermalService();

        @NonNull
        DisplayManagerFlags getFlags();

        @NonNull
        DisplayNotificationManager getDisplayNotificationManager();

        @NonNull
        Handler getHandler();

        @NonNull
        ExternalDisplayStatsService getExternalDisplayStatsService();
    }

    @NonNull
    private final Injector mInjector;
    @NonNull
    private final LogicalDisplayMapper mLogicalDisplayMapper;
    @NonNull
    private final SyncRoot mSyncRoot;
    @NonNull
    private final DisplayManagerFlags mFlags;
    @NonNull
    private final DisplayNotificationManager mDisplayNotificationManager;
    @NonNull
    private final Handler mHandler;
    @NonNull
    private final ExternalDisplayStatsService mExternalDisplayStatsService;
    @ThrottlingStatus
    private volatile int mStatus = THROTTLING_NONE;
    //@GuardedBy("mSyncRoot")
    private boolean mIsBootCompleted;
    //@GuardedBy("mSyncRoot")
    private final Set<Integer> mDisplayIdsWaitingForBootCompletion = new HashSet<>();

    ExternalDisplayPolicy(@NonNull final Injector injector) {
        mInjector = injector;
        mLogicalDisplayMapper = mInjector.getLogicalDisplayMapper();
        mSyncRoot = mInjector.getSyncRoot();
        mFlags = mInjector.getFlags();
        mDisplayNotificationManager = mInjector.getDisplayNotificationManager();
        mHandler = mInjector.getHandler();
        mExternalDisplayStatsService = mInjector.getExternalDisplayStatsService();
    }

    /**
     * Handles the displays which were connected before the boot completed.
     * Handles content mode for external displays.
     * Starts listening for temperature changes.
     */
    void onBootCompleted() {
        synchronized (mSyncRoot) {
            mIsBootCompleted = true;
            for (var displayId : mDisplayIdsWaitingForBootCompletion) {
                var logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId);
                if (logicalDisplay != null) {
                    handleExternalDisplayConnectedLocked(logicalDisplay);
                }
            }
            if (!mDisplayIdsWaitingForBootCompletion.isEmpty()) {
                mLogicalDisplayMapper.updateLogicalDisplaysLocked();
            }
            mDisplayIdsWaitingForBootCompletion.clear();
        }

        if (mFlags.isDisplayContentModeManagementEnabled()) {
            handleMirrorBuiltInDisplaySettingChangeLocked(/*enableDisplays=*/ false);
        }

        if (!mFlags.isConnectedDisplayErrorHandlingEnabled()) {
            if (DEBUG) {
                Slog.d(TAG, "ConnectedDisplayErrorHandlingEnabled is not enabled on your device:"
                                    + " cannot register thermal listener.");
            }
            return;
        }

        if (!registerThermalServiceListener(new SkinThermalStatusObserver())) {
            Slog.e(TAG, "Failed to register thermal listener");
        }
    }

    /**
     * Handles content mode change for all displays and
     * enables all external displays if needed.
     */
    void handleMirrorBuiltInDisplaySettingChangeLocked(boolean enableDisplays) {
        mLogicalDisplayMapper.forEachLocked(logicalDisplay -> {
            if (!isExternalDisplayLocked(logicalDisplay)) {
                return;
            }
            if (!logicalDisplay.isEnabledLocked() && enableDisplays) {
                setExternalDisplayEnabledLocked(logicalDisplay, true);
            }
            handleLogicalDisplayContentModeChange(logicalDisplay);
        });
    }

    /**
     * Checks the display type is external, and if it is external then enables/disables it.
     */
    void setExternalDisplayEnabledLocked(@NonNull final LogicalDisplay logicalDisplay,
            final boolean enabled) {
        if (!isExternalDisplayLocked(logicalDisplay)) {
            Slog.e(TAG, "setExternalDisplayEnabledLocked called for non external display");
            return;
        }

        if (enabled && !isExternalDisplayAllowed()) {
            Slog.w(TAG, "setExternalDisplayEnabledLocked: External display can not be enabled"
                                + " because it is currently not allowed.");
            mHandler.post(mDisplayNotificationManager::onHighTemperatureExternalDisplayNotAllowed);
            return;
        }

        mLogicalDisplayMapper.setDisplayEnabledLocked(logicalDisplay, enabled);
    }

    /**
     * Upon external display became available check if external displays allowed, this display
     * is disabled and then sends {@link DisplayManagerGlobal#EVENT_DISPLAY_CONNECTED} to allow
     * user to decide how to use this display.
     */
    void handleExternalDisplayConnectedLocked(@NonNull final LogicalDisplay logicalDisplay) {
        if (!isExternalDisplayLocked(logicalDisplay)) {
            Slog.e(TAG, "handleExternalDisplayConnectedLocked called for non-external display");
            return;
        }

        if (!mIsBootCompleted) {
            mDisplayIdsWaitingForBootCompletion.add(logicalDisplay.getDisplayIdLocked());
            return;
        }

        mExternalDisplayStatsService.onDisplayConnected(logicalDisplay);

        if (shouldAutoEnable(logicalDisplay)) {
            Slog.w(TAG, "External display is enabled by default, bypassing user consent.");
            mInjector.sendExternalDisplayEventLocked(logicalDisplay, EVENT_DISPLAY_CONNECTED);
            return;
        } else {
            // As external display is enabled by default, need to disable it now.
            // TODO(b/292196201) Remove when the display can be disabled before DPC is created.
            mLogicalDisplayMapper.setEnabledLocked(logicalDisplay, false);
        }

        if (!isExternalDisplayAllowed()) {
            Slog.w(TAG, "handleExternalDisplayConnectedLocked: External display can not be used"
                                + " because it is currently not allowed.");
            mHandler.post(mDisplayNotificationManager::onHighTemperatureExternalDisplayNotAllowed);
            return;
        }

        mInjector.sendExternalDisplayEventLocked(logicalDisplay, EVENT_DISPLAY_CONNECTED);

        if (DEBUG) {
            Slog.d(TAG, "handleExternalDisplayConnectedLocked complete"
                                + " displayId=" + logicalDisplay.getDisplayIdLocked());
        }
    }

    /**
     * Upon external display become unavailable.
     */
    void handleLogicalDisplayDisconnectedLocked(@NonNull final LogicalDisplay logicalDisplay) {
        // Type of the display here is always UNKNOWN, so we can't verify it is an external display

        var displayId = logicalDisplay.getDisplayIdLocked();
        if (mDisplayIdsWaitingForBootCompletion.remove(displayId)) {
            return;
        }

        mExternalDisplayStatsService.onDisplayDisconnected(displayId);
    }

    /**
     * Upon external display content mode change.
     */
    void handleLogicalDisplayContentModeChange(@NonNull final LogicalDisplay logicalDisplay) {
        if (!isExternalDisplayLocked(logicalDisplay) || !logicalDisplay.isEnabledLocked()) {
            return;
        }

        mExternalDisplayStatsService.onDisplayContentModeChange(
            logicalDisplay.getDisplayIdLocked());
    }

    /**
     * Upon presentation started.
     */
    void onPresentation(int displayId, boolean isShown) {
        synchronized (mSyncRoot) {
            var logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId);
            if (logicalDisplay == null || !isExternalDisplayLocked(logicalDisplay)) {
                return;
            }
        }

        if (isShown) {
            mExternalDisplayStatsService.onPresentationWindowAdded(displayId);
        } else {
            mExternalDisplayStatsService.onPresentationWindowRemoved(displayId);
        }
    }

    private boolean shouldAutoEnable(LogicalDisplay logicalDisplay) {
        if ((Build.IS_ENG || Build.IS_USERDEBUG)
                && SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)) return true;

        // If using the new connection dialog, then don't auto enable displays so the dialog
        // has a reason to show
        if (mFlags.isUpdatedDisplayConnectionDialogEnabled()) return false;

        return mFlags.isDisplayContentModeManagementEnabled()
                && logicalDisplay.canHostTasksLocked();
    }

    @GuardedBy("mSyncRoot")
    private void disableExternalDisplayLocked(@NonNull final LogicalDisplay logicalDisplay) {
        if (!isExternalDisplayLocked(logicalDisplay)) {
            return;
        }

        if (!mFlags.isConnectedDisplayErrorHandlingEnabled()) {
            if (DEBUG) {
                Slog.d(TAG, "disableExternalDisplayLocked shouldn't be called when the"
                                    + " error handling flag is off");
            }
            return;
        }

        if (!logicalDisplay.isEnabledLocked()) {
            if (DEBUG) {
                Slog.d(TAG, "disableExternalDisplayLocked is not allowed:"
                                    + " displayId=" + logicalDisplay.getDisplayIdLocked()
                                    + " isEnabledLocked=false");
            }
            return;
        }

        if (!isExternalDisplayAllowed()) {
            Slog.w(TAG, "External display is currently not allowed and is getting disabled.");
            mHandler.post(mDisplayNotificationManager::onHighTemperatureExternalDisplayNotAllowed);
        }

        mLogicalDisplayMapper.setDisplayEnabledLocked(logicalDisplay, /*enabled=*/ false);

        mExternalDisplayStatsService.onDisplayDisabled(logicalDisplay.getDisplayIdLocked());

        if (DEBUG) {
            Slog.d(TAG, "disableExternalDisplayLocked complete"
                                + " displayId=" + logicalDisplay.getDisplayIdLocked());
        }
    }

    /**
     * @return whether external displays use is currently allowed.
     */
    @VisibleForTesting
    boolean isExternalDisplayAllowed() {
        return mStatus < THROTTLING_CRITICAL;
    }

    private boolean registerThermalServiceListener(
            @NonNull final IThermalEventListener.Stub listener) {
        final var thermalService = mInjector.getThermalService();
        if (thermalService == null) {
            Slog.w(TAG, "Could not observe thermal status. Service not available");
            return false;
        }
        try {
            thermalService.registerThermalEventListenerWithType(listener, Temperature.TYPE_SKIN);
        } catch (RemoteException e) {
            Slog.e(TAG, "Failed to register thermal status listener", e);
            return false;
        }
        if (DEBUG) {
            Slog.d(TAG, "registerThermalServiceListener complete.");
        }
        return true;
    }

    private void disableExternalDisplays() {
        synchronized (mSyncRoot) {
            mLogicalDisplayMapper.forEachLocked(this::disableExternalDisplayLocked);
        }
    }

    boolean isDisplayReadyForMirroring(int displayId) {
        synchronized (mSyncRoot) {
            if (!mIsBootCompleted) {
                if (DEBUG) {
                    Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
                            + "boot is in progress");
                }
                return false;
            }

            var logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId);
            if (logicalDisplay == null) {
                if (DEBUG) {
                    Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
                            + "logicalDisplay is null");
                }
                return false;
            }

            if (!isExternalDisplayLocked(logicalDisplay)) {
                if (DEBUG) {
                    Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
                            + "logicalDisplay" + logicalDisplay.getDisplayIdLocked()
                            + " type is " + logicalDisplay.getDisplayInfoLocked().type);
                }
                return false;
            }

            if (!logicalDisplay.isEnabledLocked()) {
                if (DEBUG) {
                    Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
                            + "logicalDisplay is disabled");
                }
                return false;
            }
        }

        return true;
    }

    private final class SkinThermalStatusObserver extends IThermalEventListener.Stub {
        @Override
        public void notifyThrottling(@NonNull final Temperature temp) {
            @ThrottlingStatus final int newStatus = temp.getStatus();
            final var previousStatus = mStatus;
            mStatus = newStatus;
            if (THROTTLING_CRITICAL > previousStatus && THROTTLING_CRITICAL <= newStatus) {
                disableExternalDisplays();
            }
        }
    }
}
