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

import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded;
import static android.window.ConfigurationHelper.isDifferentDisplay;
import static android.window.ConfigurationHelper.shouldUpdateResources;

import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;

import android.annotation.AnyThread;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.app.ResourcesManager;
import android.app.servertransaction.ClientTransactionListenerController;
import android.content.Context;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.inputmethodservice.AbstractInputMethodService;
import android.os.Binder;
import android.os.Build;
import android.os.Debug;
import android.os.Handler;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.function.pooled.PooledLambda;

import java.lang.ref.WeakReference;

/**
 * This class is used to receive {@link Configuration} changes from the associated window manager
 * node on the server side, and apply the change to the {@link Context#getResources() associated
 * Resources} of the attached {@link Context}. It is also used as
 * {@link Context#getWindowContextToken() the token of non-Activity UI Contexts}.
 *
 * @see WindowContext
 * @see android.view.IWindowManager#attachWindowContextToDisplayArea
 *
 * @hide
 */
public class WindowTokenClient extends Binder {
    private static final String TAG = WindowTokenClient.class.getSimpleName();

    /**
     * Attached {@link Context} for this window token to update configuration and resources.
     * Initialized by {@link #attachContext(Context)}.
     */
    private WeakReference<Context> mContextRef = null;

    private final ResourcesManager mResourcesManager = ResourcesManager.getInstance();

    @GuardedBy("itself")
    private final Configuration mConfiguration = new Configuration();

    private boolean mShouldDumpConfigForIme;

    private final Handler mHandler = ActivityThread.currentActivityThread().getHandler();

    /**
     * Attaches {@code context} to this {@link WindowTokenClient}. Each {@link WindowTokenClient}
     * can only attach one {@link Context}.
     * <p>This method must be called before invoking
     * {@link android.view.IWindowManager#attachWindowContextToDisplayArea}.<p/>
     *
     * @param context context to be attached
     * @throws IllegalStateException if attached context has already existed.
     */
    public void attachContext(@NonNull Context context) {
        if (mContextRef != null) {
            throw new IllegalStateException("Context is already attached.");
        }
        mContextRef = new WeakReference<>(context);
        mShouldDumpConfigForIme = Build.IS_DEBUGGABLE
                && context instanceof AbstractInputMethodService;
    }

    /**
     * Gets the {@link Context} that this {@link WindowTokenClient} is attached through
     * {@link #attachContext(Context)}.
     */
    @Nullable
    public Context getContext() {
        return mContextRef != null ? mContextRef.get() : null;
    }

    /**
     * Called when {@link Configuration} updates from the server side receive.
     *
     * @param newConfig the updated {@link Configuration}
     * @param newDisplayId the updated {@link android.view.Display} ID
     */
    @MainThread
    @VisibleForTesting(visibility = PACKAGE)
    public void onConfigurationChanged(Configuration newConfig, int newDisplayId) {
        onConfigurationChanged(newConfig, newDisplayId, true /* shouldReportConfigChange */);
    }

    /**
     * Posts an {@link #onConfigurationChanged} to the main thread.
     */
    @VisibleForTesting(visibility = PACKAGE)
    public void postOnConfigurationChanged(@NonNull Configuration newConfig, int newDisplayId) {
        mHandler.post(PooledLambda.obtainRunnable(this::onConfigurationChanged, newConfig,
                newDisplayId, true /* shouldReportConfigChange */).recycleOnUse());
    }

    /**
     * Called when {@link Configuration} updates from the server side receive.
     *
     * Similar to {@link #onConfigurationChanged(Configuration, int)}, but adds a flag to control
     * whether to dispatch configuration update or not.
     * <p>
     * Note that this method must be executed on the main thread if
     * {@code shouldReportConfigChange} is {@code true}, which is usually from
     * {@link #onConfigurationChanged(Configuration, int)}
     * directly, while this method could be run on any thread if it is used to initialize
     * Context's {@code Configuration} via {@link WindowTokenClientController#attachToDisplayArea}
     * or {@link WindowTokenClientController#attachToDisplayContent}.
     *
     * @param shouldReportConfigChange {@code true} to indicate that the {@code Configuration}
     *                                 should be dispatched to listeners.
     */
    @AnyThread
    public void onConfigurationChanged(@NonNull Configuration newConfig, int newDisplayId,
            boolean shouldReportConfigChange) {
        final Context context = mContextRef.get();
        if (context == null) {
            return;
        }
        if (shouldReportConfigChange) {
            // Only report to ClientTransactionListenerController when shouldReportConfigChange.
            final ClientTransactionListenerController controller =
                    getClientTransactionListenerController();
            controller.onContextConfigurationPreChanged(context);
            try {
                onConfigurationChangedInner(context, newConfig, newDisplayId,
                        shouldReportConfigChange);
            } finally {
                controller.onContextConfigurationPostChanged(context);
            }
        } else {
            onConfigurationChangedInner(context, newConfig, newDisplayId, shouldReportConfigChange);
        }
    }

    /** Handles onConfiguration changed. */
    @VisibleForTesting
    public void onConfigurationChangedInner(@NonNull Context context,
            @NonNull Configuration newConfig, int newDisplayId, boolean shouldReportConfigChange) {
        CompatibilityInfo.applyOverrideIfNeeded(newConfig);
        final boolean displayChanged;
        final boolean shouldUpdateResources;
        final int publicDiff;
        final Configuration currentConfig;

        synchronized (mConfiguration) {
            displayChanged = isDifferentDisplay(context.getDisplayId(), newDisplayId);
            shouldUpdateResources = shouldUpdateResources(this, mConfiguration,
                    newConfig, newConfig /* overrideConfig */, displayChanged,
                    null /* configChanged */);
            publicDiff = mConfiguration.diffPublicOnly(newConfig);
            currentConfig = mShouldDumpConfigForIme ? new Configuration(mConfiguration) : null;
            if (shouldUpdateResources) {
                mConfiguration.setTo(newConfig);
            }
        }

        if (!shouldUpdateResources && mShouldDumpConfigForIme) {
            Log.d(TAG, "Configuration not dispatch to IME because configuration is up"
                    + " to date. Current config=" + context.getResources().getConfiguration()
                    + ", reported config=" + currentConfig
                    + ", updated config=" + newConfig
                    + ", updated display ID=" + newDisplayId);
        }
        // Update display first. In case callers want to obtain display information(
        // ex: DisplayMetrics) in #onConfigurationChanged callback.
        if (displayChanged) {
            context.updateDisplay(newDisplayId);
        }
        if (shouldUpdateResources) {
            // TODO(ag/9789103): update resource manager logic to track non-activity tokens
            mResourcesManager.updateResourcesForActivity(this, newConfig, newDisplayId);

            if (shouldReportConfigChange && context instanceof ConfigurationDispatcher dispatcher) {
                // Updating resources implies some fields of configuration are updated despite they
                // are public or not.
                if (dispatcher.shouldReportPrivateChanges() || publicDiff != 0) {
                    dispatcher.dispatchConfigurationChanged(newConfig);
                }
            }

            freeTextLayoutCachesIfNeeded(publicDiff);
            if (mShouldDumpConfigForIme) {
                if (!shouldReportConfigChange) {
                    Log.d(TAG, "Only apply configuration update to Resources because "
                            + "shouldReportConfigChange is false. "
                            + "context=" + context
                            + ", config=" + context.getResources().getConfiguration()
                            + ", display ID=" + context.getDisplayId() + "\n"
                            + Debug.getCallers(5));
                } else if (publicDiff == 0) {
                    Log.d(TAG, "Configuration not dispatch to IME because configuration has no "
                            + " public difference with updated config. "
                            + " Current config=" + context.getResources().getConfiguration()
                            + ", reported config=" + currentConfig
                            + ", updated config=" + newConfig
                            + ", display ID=" + context.getDisplayId());
                }
            }
        }
    }

    /**
     * Called when the attached window is removed from the display.
     */
    @VisibleForTesting(visibility = PACKAGE)
    @MainThread
    public void onWindowTokenRemoved() {
        final Context context = mContextRef.get();
        if (context != null) {
            context.destroy();
            mContextRef.clear();
        }
    }

    /** Gets {@link ClientTransactionListenerController}. */
    @VisibleForTesting
    @NonNull
    public ClientTransactionListenerController getClientTransactionListenerController() {
        return ClientTransactionListenerController.getInstance();
    }
}
