/*
 * 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.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
import static android.view.WindowManager.TRANSIT_NONE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.window.ActivityWindowInfo.getActivityWindowInfo;

import android.annotation.CallSuper;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.TestApi;
import android.app.Activity;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.RemoteAnimationDefinition;
import android.view.WindowManager;

import com.android.window.flags.Flags;

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

/**
 * Interface for WindowManager to delegate control of {@code TaskFragment}.
 * @hide
 */
@TestApi
public class TaskFragmentOrganizer extends WindowOrganizer {

    /**
     * Key to the {@link Throwable} in {@link TaskFragmentTransaction.Change#getErrorBundle()}.
     */
    public static final String KEY_ERROR_CALLBACK_THROWABLE = "fragment_throwable";

    /**
     * Key to the {@link TaskFragmentInfo} in
     * {@link TaskFragmentTransaction.Change#getErrorBundle()}.
     */
    public static final String KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO = "task_fragment_info";

    /**
     * Key to the {@link TaskFragmentOperation.OperationType} in
     * {@link TaskFragmentTransaction.Change#getErrorBundle()}.
     */
    public static final String KEY_ERROR_CALLBACK_OP_TYPE = "operation_type";

    /**
     * Key to bundle {@link TaskFragmentInfo}s from the system in
     * {@link #registerOrganizer(boolean, Bundle)}
     *
     * @hide
     */
    public static final String KEY_RESTORE_TASK_FRAGMENTS_INFO = "key_restore_task_fragments_info";

    /**
     * Key to bundle {@link TaskFragmentParentInfo} from the system in
     * {@link #registerOrganizer(boolean, Bundle)}
     *
     * @hide
     */
    public static final String KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO =
            "key_restore_task_fragment_parent_info";

    /**
     * No change set.
     */
    @WindowManager.TransitionType
    @TaskFragmentTransitionType
    public static final int TASK_FRAGMENT_TRANSIT_NONE = TRANSIT_NONE;

    /**
     * A window that didn't exist before has been created and made visible.
     */
    @WindowManager.TransitionType
    @TaskFragmentTransitionType
    public static final int TASK_FRAGMENT_TRANSIT_OPEN = TRANSIT_OPEN;

    /**
     * A window that was visible no-longer exists (was finished or destroyed).
     */
    @WindowManager.TransitionType
    @TaskFragmentTransitionType
    public static final int TASK_FRAGMENT_TRANSIT_CLOSE = TRANSIT_CLOSE;

    /**
     * A window is visible before and after but changes in some way (eg. it resizes or changes
     * windowing-mode).
     */
    @WindowManager.TransitionType
    @TaskFragmentTransitionType
    public static final int TASK_FRAGMENT_TRANSIT_CHANGE = TRANSIT_CHANGE;


    /**
     * The task fragment drag resize transition used by activity embedding.
     *
     * This value is also used in Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE and must not
     * conflict with other predefined transition types.
     *
     * @hide
     */
    @WindowManager.TransitionType
    @TaskFragmentTransitionType
    public static final int TASK_FRAGMENT_TRANSIT_DRAG_RESIZE = TRANSIT_FIRST_CUSTOM + 17;

    /**
     * Introduced a sub set of {@link WindowManager.TransitionType} for the types that are used for
     * TaskFragment transition.
     *
     * Doing this instead of exposing {@link WindowManager.TransitionType} because we want to keep
     * the Shell transition API hidden until it comes fully stable.
     * @hide
     */
    @IntDef(prefix = { "TASK_FRAGMENT_TRANSIT_" }, value = {
            TASK_FRAGMENT_TRANSIT_NONE,
            TASK_FRAGMENT_TRANSIT_OPEN,
            TASK_FRAGMENT_TRANSIT_CLOSE,
            TASK_FRAGMENT_TRANSIT_CHANGE,
            TASK_FRAGMENT_TRANSIT_DRAG_RESIZE,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface TaskFragmentTransitionType {}

    /**
     * Creates a {@link Bundle} with an exception, operation type and TaskFragmentInfo (if any)
     * that can be passed to {@link ITaskFragmentOrganizer#onTaskFragmentError}.
     * @hide
     */
    public static @NonNull Bundle putErrorInfoInBundle(@NonNull Throwable exception,
            @Nullable TaskFragmentInfo info, @TaskFragmentOperation.OperationType int opType) {
        final Bundle errorBundle = new Bundle();
        errorBundle.putSerializable(KEY_ERROR_CALLBACK_THROWABLE, exception);
        if (info != null) {
            errorBundle.putParcelable(KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO, info);
        }
        errorBundle.putInt(KEY_ERROR_CALLBACK_OP_TYPE, opType);
        return errorBundle;
    }

    /**
     * Callbacks from WM Core are posted on this executor.
     */
    private final Executor mExecutor;

    public TaskFragmentOrganizer(@NonNull Executor executor) {
        mExecutor = executor;
    }

    /**
     * Gets the executor to run callbacks on.
     */
    @NonNull
    public Executor getExecutor() {
        return mExecutor;
    }

    /**
     * Registers a {@link TaskFragmentOrganizer} to manage TaskFragments.
     */
    @CallSuper
    public void registerOrganizer() {
        registerOrganizer(false /* isSystemOrganizer */, null /* outSavedState */);
    }

    /**
     * Registers a {@link TaskFragmentOrganizer} to manage TaskFragments.
     * <p>
     * Registering a system organizer requires MANAGE_ACTIVITY_TASKS permission, and the organizer
     * will have additional system capabilities, including: (1) it will receive SurfaceControl for
     * the organized TaskFragment, and (2) it needs to update the
     * {@link android.view.SurfaceControl} following the window change accordingly.
     *
     * @hide
     */
    @CallSuper
    @RequiresPermission(value = "android.permission.MANAGE_ACTIVITY_TASKS", conditional = true)
    @FlaggedApi(Flags.FLAG_TASK_FRAGMENT_SYSTEM_ORGANIZER_FLAG)
    public void registerOrganizer(boolean isSystemOrganizer) {
        registerOrganizer(isSystemOrganizer, null /* outSavedState */);
    }

    /**
     * Registers a {@link TaskFragmentOrganizer} to manage TaskFragments.
     * <p>
     * Registering a system organizer requires MANAGE_ACTIVITY_TASKS permission, and the organizer
     * will have additional system capabilities, including: (1) it will receive SurfaceControl for
     * the organized TaskFragment, and (2) it needs to update the
     * {@link android.view.SurfaceControl} following the window change accordingly.
     *
     * @param isSystemOrganizer  If it is a system organizer
     * @param outSavedState      Returning the saved state (if any) that previously saved. This is
     *                           useful when retrieve the state from the same TaskFragmentOrganizer
     *                           that was killed by the system (e.g. to reclaim memory). Note that
     *                           the save state is dropped and unable to retrieve once the system
     *                           restarts or the organizer is unregistered.
     * @hide
     */
    @CallSuper
    @RequiresPermission(value = "android.permission.MANAGE_ACTIVITY_TASKS", conditional = true)
    public void registerOrganizer(boolean isSystemOrganizer, @Nullable Bundle outSavedState) {
        try {
            getController().registerOrganizer(mInterface, isSystemOrganizer,
                    outSavedState != null ? outSavedState : new Bundle());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Unregisters a previously registered TaskFragmentOrganizer.
     */
    @CallSuper
    public void unregisterOrganizer() {
        try {
            getController().unregisterOrganizer(mInterface);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Registers remote animations per transition type for the organizer. It will override the
     * animations if the transition only contains windows that belong to the organized
     * TaskFragments, and at least one of the transition window is embedded (not filling the Task).
     * @hide
     */
    @CallSuper
    public void registerRemoteAnimations(@NonNull RemoteAnimationDefinition definition) {
        try {
            getController().registerRemoteAnimations(mInterface, definition);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Unregisters remote animations per transition type for the organizer.
     * @hide
     */
    @CallSuper
    public void unregisterRemoteAnimations() {
        try {
            getController().unregisterRemoteAnimations(mInterface);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Saves the state in the system, where the state can be restored if the process of
     * the TaskFragmentOrganizer is restarted.
     *
     * @hide
     *
     * @param state the state to save.
     */
    public void setSavedState(@NonNull Bundle state) {
        if (state.getSize() > 200000) {
            throw new IllegalArgumentException("Saved state too large, " + state.getSize());
        }

        try {
            getController().setSavedState(mInterface, state);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Notifies the server that the organizer has finished handling the given transaction. The
     * server should apply the given {@link WindowContainerTransaction} for the necessary changes.
     *
     * @param transactionToken  {@link TaskFragmentTransaction#getTransactionToken()} from
     *                          {@link #onTransactionReady(TaskFragmentTransaction)}
     * @param wct               {@link WindowContainerTransaction} that the server should apply for
     *                          update of the transaction.
     * @param transitionType    {@link TaskFragmentTransitionType} if it needs to start a
     *                          transition.
     * @param shouldApplyIndependently  If {@code true}, the {@code wct} will request a new
     *                                  transition, which will be queued until the sync engine is
     *                                  free if there is any other active sync. If {@code false},
     *                                  the {@code wct} will be directly applied to the active sync.
     * @see com.android.server.wm.WindowOrganizerController#enforceTaskFragmentOrganizerPermission
     * for permission enforcement.
     */
    public void onTransactionHandled(@NonNull IBinder transactionToken,
            @NonNull WindowContainerTransaction wct,
            @TaskFragmentTransitionType int transitionType, boolean shouldApplyIndependently) {
        wct.setTaskFragmentOrganizer(mInterface);
        try {
            getController().onTransactionHandled(transactionToken, wct, transitionType,
                    shouldApplyIndependently);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Must use {@link #applyTransaction(WindowContainerTransaction, int, boolean)} instead.
     * @see #applyTransaction(WindowContainerTransaction, int, boolean)
     */
    @Override
    public void applyTransaction(@NonNull WindowContainerTransaction wct) {
        throw new RuntimeException("Not allowed!");
    }

    /**
     * Requests the server to apply the given {@link WindowContainerTransaction}.
     *
     * @param wct   {@link WindowContainerTransaction} to apply.
     * @param transitionType    {@link TaskFragmentTransitionType} if it needs to start a
     *                          transition.
     * @param shouldApplyIndependently  If {@code true}, the {@code wct} will request a new
     *                                  transition, which will be queued until the sync engine is
     *                                  free if there is any other active sync. If {@code false},
     *                                  the {@code wct} will be directly applied to the active sync.
     * @see com.android.server.wm.WindowOrganizerController#enforceTaskFragmentOrganizerPermission
     * for permission enforcement.
     */
    public void applyTransaction(@NonNull WindowContainerTransaction wct,
            @TaskFragmentTransitionType int transitionType, boolean shouldApplyIndependently) {
        if (wct.isEmpty()) {
            return;
        }
        wct.setTaskFragmentOrganizer(mInterface);
        try {
            getController().applyTransaction(
                    wct, transitionType, shouldApplyIndependently, null /* remoteTransition */);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Applies a transaction with a {@link RemoteTransition}. Only a system organizer is allowed to
     * use {@link RemoteTransition}. See {@link TaskFragmentOrganizer#registerOrganizer(boolean)}.
     *
     * @hide
     */
    @FlaggedApi(Flags.FLAG_TASK_FRAGMENT_SYSTEM_ORGANIZER_FLAG)
    public void applySystemTransaction(@NonNull WindowContainerTransaction wct,
            @TaskFragmentTransitionType int transitionType,
            @Nullable RemoteTransition remoteTransition) {
        if (wct.isEmpty()) {
            return;
        }
        wct.setTaskFragmentOrganizer(mInterface);
        try {
            getController().applyTransaction(
                    wct, transitionType, remoteTransition != null /* shouldApplyIndependently */,
                    remoteTransition);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Called when the transaction is ready so that the organizer can update the TaskFragments based
     * on the changes in transaction.
     */
    public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
        // Notify the server to finish the transaction.
        onTransactionHandled(transaction.getTransactionToken(), new WindowContainerTransaction(),
                TASK_FRAGMENT_TRANSIT_NONE, false /* shouldApplyIndependently */);
    }

    private final ITaskFragmentOrganizer mInterface = new ITaskFragmentOrganizer.Stub() {
        @Override
        public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
            mExecutor.execute(() -> TaskFragmentOrganizer.this.onTransactionReady(transaction));
        }
    };

    private final TaskFragmentOrganizerToken mToken = new TaskFragmentOrganizerToken(mInterface);

    @NonNull
    public TaskFragmentOrganizerToken getOrganizerToken() {
        return mToken;
    }

    private ITaskFragmentOrganizerController getController() {
        try {
            return getWindowOrganizerController().getTaskFragmentOrganizerController();
        } catch (RemoteException e) {
            return null;
        }
    }

    /**
     * Checks if an activity is organized by a {@link android.window.TaskFragmentOrganizer} and
     * only occupies a portion of Task bounds.
     *
     * @see ActivityWindowInfo for additional window info.
     * @hide
     */
    public static boolean isActivityEmbedded(@NonNull Activity activity) {
        Objects.requireNonNull(activity);
        final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity);
        return activityWindowInfo != null && activityWindowInfo.isEmbedded();
    }
}
