/*
 * Copyright (C) 2021 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.view.Display.DEFAULT_DISPLAY;

import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.annotation.UiContext;
import android.app.ActivityThread;
import android.app.LoadedApk;
import android.app.Service;
import android.content.ComponentCallbacks;
import android.content.ComponentCallbacksController;
import android.content.Context;
import android.content.res.Configuration;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.Display;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams.WindowType;
import android.view.WindowManagerImpl;

/**
 * A {@link Service} responsible for showing a non-activity window, such as software keyboards or
 * accessibility overlay windows. This {@link Service} has similar behavior to
 * {@link WindowContext}, but is represented as {@link Service}.
 *
 * @see android.inputmethodservice.InputMethodService
 *
 * @hide
 */
@TestApi
@UiContext
public abstract class WindowProviderService extends Service implements WindowProvider {

    private static final String TAG = WindowProviderService.class.getSimpleName();

    private final Bundle mOptions;
    private final WindowTokenClient mWindowToken = new WindowTokenClient();
    private final WindowContextController mController = new WindowContextController(mWindowToken);
    private WindowManager mWindowManager;
    private boolean mInitialized;
    private final ComponentCallbacksController mCallbacksController =
            new ComponentCallbacksController();

    /**
     * Returns {@code true} if the {@code windowContextOptions} declares that it is a
     * {@link WindowProviderService}.
     *
     * @hide
     */
    public static boolean isWindowProviderService(@Nullable Bundle windowContextOptions) {
        if (windowContextOptions == null) {
            return false;
        }
        return (windowContextOptions.getBoolean(KEY_IS_WINDOW_PROVIDER_SERVICE, false));
    }

    public WindowProviderService() {
        mOptions = new Bundle();
        mOptions.putBoolean(KEY_IS_WINDOW_PROVIDER_SERVICE, true);
    }

    /**
     * Returns the window type of this {@link WindowProviderService}.
     * Each inheriting class must implement this method to provide the type of the window. It is
     * used similar to {@code type} of {@link Context#createWindowContext(int, Bundle)}
     *
     * @see Context#createWindowContext(int, Bundle)
     *
     * @hide
     */
    @TestApi
    @SuppressLint("OnNameExpected")
    // Suppress the lint because it is not a callback and users should provide window type
    // so we cannot make it final.
    @WindowType
    @Override
    public abstract int getWindowType();

    /**
     * Returns the option of this {@link WindowProviderService}.
     * <p>
     * The inheriting class can implement this method to provide the customization {@code option} of
     * the window, but must be based on this method's returned value.
     * It is used similar to {@code options} of {@link Context#createWindowContext(int, Bundle)}
     * </p>
     * <pre class="prettyprint">
     * public Bundle getWindowContextOptions() {
     *     final Bundle options = super.getWindowContextOptions();
     *     options.put(KEY_ROOT_DISPLAY_AREA_ID, displayAreaInfo.rootDisplayAreaId);
     *     return options;
     * }
     * </pre>
     *
     * @hide
     */
    @TestApi
    @SuppressLint({"OnNameExpected", "NullableCollection"})
    // Suppress the lint because it is not a callback and users may override this API to provide
    // launch option. Also, the return value of this API is null by default.
    @Nullable
    @CallSuper
    @Override
    public Bundle getWindowContextOptions() {
        return mOptions;
    }

    @SuppressLint({"OnNameExpected", "ExecutorRegistration"})
    // Suppress lint because this is a legacy named function and doesn't have an optional param
    // for executor.
    /**
     * Here we override to prevent WindowProviderService from invoking
     * {@link Application.registerComponentCallback}, which will result in callback registered
     * for process-level Configuration change updates.
     */
    @Override
    public void registerComponentCallbacks(@NonNull ComponentCallbacks callback) {
        // For broadcasting Configuration Changes.
        mCallbacksController.registerCallbacks(callback);
    }

    @SuppressLint("OnNameExpected")
    @Override
    public void unregisterComponentCallbacks(@NonNull ComponentCallbacks callback) {
        mCallbacksController.unregisterCallbacks(callback);
    }

    @SuppressLint("OnNameExpected")
    @Override
    public void onConfigurationChanged(@NonNull Configuration configuration) {
        // This is only called from WindowTokenClient.
        mCallbacksController.dispatchConfigurationChanged(configuration);
    }

    /**
     * Override {@link Service}'s empty implementation and listen to {@code ActivityThread} for
     * low memory and trim memory events.
     */
    @Override
    public void onLowMemory() {
        mCallbacksController.dispatchLowMemory();
    }

    @Override
    public void onTrimMemory(int level) {
        mCallbacksController.dispatchTrimMemory(level);
    }

    /**
     * Returns the display ID to launch this {@link WindowProviderService}.
     *
     * @hide
     */
    @TestApi
    @SuppressLint({"OnNameExpected"})
    // Suppress the lint because it is not a callback and users may override this API to provide
    // display.
    @NonNull
    public int getInitialDisplayId() {
        return DEFAULT_DISPLAY;
    }

    /**
     * Attaches this WindowProviderService to the {@code windowToken}.
     *
     * @hide
     */
    @TestApi
    public final void attachToWindowToken(@NonNull IBinder windowToken) {
        mController.attachToWindowToken(windowToken);
    }

    /** @hide */
    @Override
    public final Context createServiceBaseContext(ActivityThread mainThread,
            LoadedApk packageInfo) {
        final Context context = super.createServiceBaseContext(mainThread, packageInfo);
        final DisplayManager displayManager = context.getSystemService(DisplayManager.class);
        final int initialDisplayId = getInitialDisplayId();
        Display display = displayManager.getDisplay(initialDisplayId);
        // Fallback to use the default display if the initial display to start WindowProviderService
        // is detached.
        if (display == null) {
            Log.e(TAG, "Display with id " + initialDisplayId + " not found, falling back to "
                    + "DEFAULT_DISPLAY");
            display = displayManager.getDisplay(DEFAULT_DISPLAY);
        }
        return context.createTokenContext(mWindowToken, display);
    }

    /** @hide */
    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        if (!mInitialized) {
            mWindowToken.attachContext(this);
            mController.attachToDisplayArea(getWindowType(), getDisplayId(),
                    getWindowContextOptions());
            mWindowManager = WindowManagerImpl.createWindowContextWindowManager(this);
            mInitialized = true;
        }
    }

    // Suppress the lint because ths is overridden from Context.
    @SuppressLint("OnNameExpected")
    @Override
    @Nullable
    public Object getSystemService(@NonNull String name) {
        if (WINDOW_SERVICE.equals(name)) {
            return mWindowManager;
        }
        return super.getSystemService(name);
    }

    @CallSuper
    @Override
    public void onDestroy() {
        super.onDestroy();
        mController.detachIfNeeded();
        mCallbacksController.clearCallbacks();
    }
}
