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

import static android.view.Display.INVALID_DISPLAY;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.DisplayMetrics;
import android.util.Singleton;
import android.view.RemoteAnimationDefinition;
import android.window.SplashScreenView.SplashScreenViewParcelable;

import java.util.List;

/**
 * This class gives information about, and interacts with activities and their containers like task,
 * stacks, and displays.
 *
 * @hide
 */
@TestApi
@SystemService(Context.ACTIVITY_TASK_SERVICE)
public class ActivityTaskManager {

    /** Invalid stack ID. */
    public static final int INVALID_STACK_ID = -1;

    /**
     * Invalid task ID.
     * @hide
     */
    public static final int INVALID_TASK_ID = -1;

    /**
     * Input parameter to {@link IActivityTaskManager#resizeTask} which indicates
     * that the resize doesn't need to preserve the window, and can be skipped if bounds
     * is unchanged. This mode is used by window manager in most cases.
     * @hide
     */
    public static final int RESIZE_MODE_SYSTEM = 0;

    /**
     * Input parameter to {@link IActivityTaskManager#resizeTask} which indicates
     * that the resize should preserve the window if possible.
     * @hide
     */
    public static final int RESIZE_MODE_PRESERVE_WINDOW   = (0x1 << 0);

    /**
     * Input parameter to {@link IActivityTaskManager#resizeTask} used when the
     * resize is due to a drag action.
     * @hide
     */
    public static final int RESIZE_MODE_USER = RESIZE_MODE_PRESERVE_WINDOW;

    /**
     * Input parameter to {@link IActivityTaskManager#resizeTask} used by window
     * manager during a screen rotation.
     * @hide
     */
    public static final int RESIZE_MODE_SYSTEM_SCREEN_ROTATION = RESIZE_MODE_PRESERVE_WINDOW;

    /**
     * Input parameter to {@link IActivityTaskManager#resizeTask} which indicates
     * that the resize should be performed even if the bounds appears unchanged.
     * @hide
     */
    public static final int RESIZE_MODE_FORCED = (0x1 << 1);

    /**
     * Input parameter to {@link IActivityTaskManager#resizeTask} which indicates
     * that the resize should preserve the window if possible, and should not be skipped
     * even if the bounds is unchanged. Usually used to force a resizing when a drag action
     * is ending.
     * @hide
     */
    public static final int RESIZE_MODE_USER_FORCED =
            RESIZE_MODE_PRESERVE_WINDOW | RESIZE_MODE_FORCED;

    /**
     * Extra included on intents that contain an EXTRA_INTENT, with options that the contained
     * intent may want to be started with.  Type is Bundle.
     * TODO: remove once the ChooserActivity moves to systemui
     * @hide
     */
    public static final String EXTRA_OPTIONS = "android.app.extra.OPTIONS";

    /**
     * Extra included on intents that contain an EXTRA_INTENT, use this boolean value for the
     * parameter of the same name when starting the contained intent.
     * TODO: remove once the ChooserActivity moves to systemui
     * @hide
     */
    public static final String EXTRA_IGNORE_TARGET_SECURITY =
            "android.app.extra.EXTRA_IGNORE_TARGET_SECURITY";

    /** The minimal size of a display's long-edge needed to support split-screen multi-window. */
    public static final int DEFAULT_MINIMAL_SPLIT_SCREEN_DISPLAY_SIZE_DP = 440;

    private static int sMaxRecentTasks = -1;

    private static final Singleton<ActivityTaskManager> sInstance =
            new Singleton<ActivityTaskManager>() {
                @Override
                protected ActivityTaskManager create() {
                    return new ActivityTaskManager();
                }
            };

    private ActivityTaskManager() {
    }

    /** @hide */
    public static ActivityTaskManager getInstance() {
        return sInstance.get();
    }

    /** @hide */
    public static IActivityTaskManager getService() {
        return IActivityTaskManagerSingleton.get();
    }

    @UnsupportedAppUsage(trackingBug = 129726065)
    private static final Singleton<IActivityTaskManager> IActivityTaskManagerSingleton =
            new Singleton<IActivityTaskManager>() {
                @Override
                protected IActivityTaskManager create() {
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
                    return IActivityTaskManager.Stub.asInterface(b);
                }
            };

    /**
     * Removes root tasks in the windowing modes from the system if they are of activity type
     * ACTIVITY_TYPE_STANDARD or ACTIVITY_TYPE_UNDEFINED
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
    public void removeRootTasksInWindowingModes(@NonNull int[] windowingModes) {
        try {
            getService().removeRootTasksInWindowingModes(windowingModes);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** Removes root tasks of the activity types from the system. */
    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
    public void removeRootTasksWithActivityTypes(@NonNull int[] activityTypes) {
        try {
            getService().removeRootTasksWithActivityTypes(activityTypes);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Removes all visible recent tasks from the system.
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.REMOVE_TASKS)
    public void removeAllVisibleRecentTasks() {
        try {
            getService().removeAllVisibleRecentTasks();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Return the maximum number of recents entries that we will maintain and show.
     * @hide
     */
    public static int getMaxRecentTasksStatic() {
        if (sMaxRecentTasks < 0) {
            return sMaxRecentTasks = ActivityManager.isLowRamDeviceStatic() ? 36 : 48;
        }
        return sMaxRecentTasks;
    }

    /**
     * Notify the server that splash screen of the given task has been copied"
     *
     * @param taskId Id of task to handle the material to reconstruct the splash screen view.
     * @param parcelable Used to reconstruct the view, null means the surface is un-copyable.
     * @hide
     */
    public void onSplashScreenViewCopyFinished(int taskId,
            @Nullable SplashScreenViewParcelable parcelable) {
        try {
            getService().onSplashScreenViewCopyFinished(taskId, parcelable);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Return the default limit on the number of recents that an app can make.
     * @hide
     */
    public static int getDefaultAppRecentsLimitStatic() {
        return getMaxRecentTasksStatic() / 6;
    }

    /**
     * Return the maximum limit on the number of recents that an app can make.
     * @hide
     */
    public static int getMaxAppRecentsLimitStatic() {
        return getMaxRecentTasksStatic() / 2;
    }

    /**
     * Returns true if the system supports at least one form of multi-window.
     * E.g. freeform, split-screen, picture-in-picture.
     */
    public static boolean supportsMultiWindow(Context context) {
        // On watches, multi-window is used to present essential system UI, and thus it must be
        // supported regardless of device memory characteristics.
        boolean isWatch = context.getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_WATCH);
        return (!ActivityManager.isLowRamDeviceStatic() || isWatch)
                && Resources.getSystem().getBoolean(
                com.android.internal.R.bool.config_supportsMultiWindow);
    }

    /**
     * Returns {@code true} if the display the context is associated with supports split screen
     * multi-window.
     *
     * @throws UnsupportedOperationException if the supplied {@link Context} is not associated with
     * a display.
     */
    public static boolean supportsSplitScreenMultiWindow(Context context) {
        DisplayMetrics dm = new DisplayMetrics();
        context.getDisplay().getRealMetrics(dm);

        int widthDp = (int) (dm.widthPixels / dm.density);
        int heightDp = (int) (dm.heightPixels / dm.density);
        if (Math.max(widthDp, heightDp) < DEFAULT_MINIMAL_SPLIT_SCREEN_DISPLAY_SIZE_DP) {
            return false;
        }

        return supportsMultiWindow(context)
                && Resources.getSystem().getBoolean(
                com.android.internal.R.bool.config_supportsSplitScreenMultiWindow);
    }

    /**
     * Start to enter lock task mode for given task by system(UI).
     * @param taskId Id of task to lock.
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
    public void startSystemLockTaskMode(int taskId) {
        try {
            getService().startSystemLockTaskMode(taskId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Stop lock task mode by system(UI).
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
    public void stopSystemLockTaskMode() {
        try {
            getService().stopSystemLockTaskMode();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Move task to root task with given id.
     * @param taskId Id of the task to move.
     * @param rootTaskId Id of the rootTask for task moving.
     * @param toTop Whether the given task should shown to top of stack.
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
    public void moveTaskToRootTask(int taskId, int rootTaskId, boolean toTop) {
        try {
            getService().moveTaskToRootTask(taskId, rootTaskId, toTop);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Resize task to given bounds.
     * @param taskId Id of task to resize.
     * @param bounds Bounds to resize task.
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
    public void resizeTask(int taskId, Rect bounds) {
        try {
            getService().resizeTask(taskId, bounds, RESIZE_MODE_SYSTEM);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Clears launch params for the given package.
     * @param packageNames the names of the packages of which the launch params are to be cleared
     */
    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
    public void clearLaunchParamsForPackages(List<String> packageNames) {
        try {
            getService().clearLaunchParamsForPackages(packageNames);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    /**
     * @return whether the UI mode of the given config supports error dialogs (ANR, crash, etc).
     * @hide
     */
    public static boolean currentUiModeSupportsErrorDialogs(@NonNull Configuration config) {
        int modeType = config.uiMode & Configuration.UI_MODE_TYPE_MASK;
        return (modeType != Configuration.UI_MODE_TYPE_CAR
                && !(modeType == Configuration.UI_MODE_TYPE_WATCH && Build.IS_USER)
                && modeType != Configuration.UI_MODE_TYPE_TELEVISION
                && modeType != Configuration.UI_MODE_TYPE_VR_HEADSET);
    }

    /** @return whether the current UI mode supports error dialogs (ANR, crash, etc). */
    public static boolean currentUiModeSupportsErrorDialogs(@NonNull Context context) {
        final Configuration config = context.getResources().getConfiguration();
        return currentUiModeSupportsErrorDialogs(config);
    }

    /** @return max allowed number of actions in picture-in-picture mode. */
    public static int getMaxNumPictureInPictureActions(@NonNull Context context) {
        return context.getResources().getInteger(
                com.android.internal.R.integer.config_pictureInPictureMaxNumberOfActions);
    }

    /**
     * @return List of running tasks.
     * @hide
     */
    public List<ActivityManager.RunningTaskInfo> getTasks(int maxNum) {
        return getTasks(maxNum, false /* filterForVisibleRecents */, false /* keepIntentExtra */,
                INVALID_DISPLAY);
    }

    /**
     * @return List of running tasks that can be filtered by visibility in recents.
     * @hide
     */
    public List<ActivityManager.RunningTaskInfo> getTasks(
            int maxNum, boolean filterOnlyVisibleRecents) {
        return getTasks(maxNum, filterOnlyVisibleRecents, false /* keepIntentExtra */,
                INVALID_DISPLAY);
    }

    /**
     * @return List of running tasks that can be filtered by visibility in recents and keep intent
     * extra.
     * @hide
     */
    public List<ActivityManager.RunningTaskInfo> getTasks(
            int maxNum, boolean filterOnlyVisibleRecents, boolean keepIntentExtra) {
        return getTasks(maxNum, filterOnlyVisibleRecents, keepIntentExtra, INVALID_DISPLAY);
    }

    /**
     * @return List of running tasks that can be filtered by visibility and displayId in recents
     * and keep intent extra.
     * @param displayId the target display id, or {@link INVALID_DISPLAY} not to filter by displayId
     * @hide
     */
    public List<ActivityManager.RunningTaskInfo> getTasks(
            int maxNum, boolean filterOnlyVisibleRecents, boolean keepIntentExtra, int displayId) {
        try {
            return getService().getTasks(maxNum, filterOnlyVisibleRecents, keepIntentExtra,
                    displayId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * @return List of recent tasks.
     * @hide
     */
    public List<ActivityManager.RecentTaskInfo> getRecentTasks(
            int maxNum, int flags, int userId) {
        try {
            return getService().getRecentTasks(maxNum, flags, userId).getList();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public void registerTaskStackListener(TaskStackListener listener) {
        try {
            getService().registerTaskStackListener(listener);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public void unregisterTaskStackListener(TaskStackListener listener) {
        try {
            getService().unregisterTaskStackListener(listener);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public Rect getTaskBounds(int taskId) {
        try {
            return getService().getTaskBounds(taskId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Registers remote animations for a display.
     * @hide
     */
    public void registerRemoteAnimationsForDisplay(
            int displayId, RemoteAnimationDefinition definition) {
        try {
            getService().registerRemoteAnimationsForDisplay(displayId, definition);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** @hide */
    public boolean isInLockTaskMode() {
        try {
            return getService().isInLockTaskMode();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /** Removes task by a given taskId */
    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
    public boolean removeTask(int taskId) {
        try {
            return getService().removeTask(taskId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Detaches the navigation bar from the app it was attached to during a transition.
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS)
    public void detachNavigationBarFromApp(@NonNull IBinder transition) {
        try {
            getService().detachNavigationBarFromApp(transition);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Information you can retrieve about a root task in the system.
     * @hide
     */
    public static class RootTaskInfo extends TaskInfo implements Parcelable {
        // TODO(b/148895075): Move some of the fields to TaskInfo.
        public Rect bounds = new Rect();
        public int[] childTaskIds;
        public String[] childTaskNames;
        public Rect[] childTaskBounds;
        public int[] childTaskUserIds;
        public boolean visible;
        // Index of the stack in the display's stack list, can be used for comparison of stack order
        public int position;

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeTypedObject(bounds, flags);
            dest.writeIntArray(childTaskIds);
            dest.writeStringArray(childTaskNames);
            dest.writeTypedArray(childTaskBounds, flags);
            dest.writeIntArray(childTaskUserIds);
            dest.writeInt(visible ? 1 : 0);
            dest.writeInt(position);
            super.writeToParcel(dest, flags);
        }

        @Override
        void readFromParcel(Parcel source) {
            bounds = source.readTypedObject(Rect.CREATOR);
            childTaskIds = source.createIntArray();
            childTaskNames = source.createStringArray();
            childTaskBounds = source.createTypedArray(Rect.CREATOR);
            childTaskUserIds = source.createIntArray();
            visible = source.readInt() > 0;
            position = source.readInt();
            super.readFromParcel(source);
        }

        public static final @NonNull Creator<RootTaskInfo> CREATOR = new Creator<>() {
            @Override
            public RootTaskInfo createFromParcel(Parcel source) {
                return new RootTaskInfo(source);
            }

            @Override
            public RootTaskInfo[] newArray(int size) {
                return new RootTaskInfo[size];
            }
        };

        public RootTaskInfo() {
        }

        private RootTaskInfo(Parcel source) {
            readFromParcel(source);
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder(256);
            sb.append("RootTask id="); sb.append(taskId);
            sb.append(" bounds="); sb.append(bounds.toShortString());
            sb.append(" displayId="); sb.append(displayId);
            sb.append(" userId="); sb.append(userId);
            sb.append("\n");

            sb.append(" configuration="); sb.append(configuration);
            sb.append("\n");

            for (int i = 0; i < childTaskIds.length; ++i) {
                sb.append("  taskId="); sb.append(childTaskIds[i]);
                sb.append(": "); sb.append(childTaskNames[i]);
                if (childTaskBounds != null) {
                    sb.append(" bounds="); sb.append(childTaskBounds[i].toShortString());
                }
                sb.append(" userId=").append(childTaskUserIds[i]);
                sb.append(" visible=").append(visible);
                if (topActivity != null) {
                    sb.append(" topActivity=").append(topActivity);
                }
                sb.append("\n");
            }
            return sb.toString();
        }
    }
}
