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

import static com.android.server.wm.ActivityStarter.Request;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import android.view.Display;

import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;

import java.util.ArrayList;
import java.util.List;

/**
 * Class to control the assignment of a display for Car while launching a Activity.
 *
 * <p>This one controls which displays users are allowed to launch.
 * The policy should be passed from car service through
 * {@link com.android.internal.car.ICarServiceHelper} binder interfaces. If no policy is set,
 * this module will not change anything for launch process.</p>
 *
 * <p> The policy can only affect which display passenger users can use. Current user, assumed
 * to be a driver user, is allowed to launch any display always.</p>
 *
 * @hide
 */
public final class CarLaunchParamsModifier implements LaunchParamsController.LaunchParamsModifier {

    private final Context mContext;

    private DisplayManager mDisplayManager;  // set only from init()
    private ActivityTaskManagerService mAtm;  // set only from init()

    private CarLaunchParamsModifierUpdatable mUpdatable;

    // getFallbackDisplayAreasForActivity() can return the most 3 {@link TaskDisplayAreaWrapper}.
    private final ArrayList<TaskDisplayAreaWrapper> mFallBackDisplayAreaList = new ArrayList<>(3);

    /** Constructor. Can be constructed any time. */
    public CarLaunchParamsModifier(Context context) {
        // This can be very early stage. So postpone interaction with other system until init.
        mContext = context;
    }

    public void setUpdatable(CarLaunchParamsModifierUpdatable updatable) {
        mUpdatable = updatable;
    }

    public CarLaunchParamsModifierInterface getBuiltinInterface() {
        return mBuiltinInterface;
    }

    /**
     * Initializes all internal stuffs. This should be called only after ATMS, DisplayManagerService
     * are ready.
     */
    public void init() {
        mAtm = (ActivityTaskManagerService) ActivityTaskManager.getService();
        LaunchParamsController controller = mAtm.mTaskSupervisor.getLaunchParamsController();
        controller.registerModifier(this);
        mDisplayManager = mContext.getSystemService(DisplayManager.class);
        mDisplayManager.registerDisplayListener(mUpdatable.getDisplayListener(),
                new Handler(Looper.getMainLooper()));
    }

    /** Notifies user visibility changed. */
    public void handleUserVisibilityChanged(int userId, boolean visible) {
        mUpdatable.handleUserVisibilityChanged(userId, visible);
    }

    /** Notifies user switching. */
    public void handleUserStarting(@UserIdInt int startingUserId) {
        mUpdatable.handleUserStarting(startingUserId);
    }

    /** Notifies user switching. */
    public void handleCurrentUserSwitching(@UserIdInt int newUserId) {
        mUpdatable.handleCurrentUserSwitching(newUserId);
    }

    /** Notifies user stopped. */
    public void handleUserStopped(@UserIdInt int stoppedUser) {
        mUpdatable.handleUserStopped(stoppedUser);
    }

    /**
     * Decides display to assign while an Activity is launched.
     *
     * <p>For current user (=driver), launching to any display is allowed as long as system
     * allows it.</p>
     *
     * <p>For private display, do not change anything as private display has its own logic.</p>
     *
     * <p>For passenger displays, only run in allowed displays. If requested display is not
     * allowed, change to the 1st allowed display.</p>
     */
    @Override
    @Result
    public int onCalculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
            @Nullable ActivityRecord activity, @Nullable ActivityRecord source,
            ActivityOptions options, @Nullable Request request, int phase,
            LaunchParamsController.LaunchParams currentParams,
            LaunchParamsController.LaunchParams outParams) {
        CalculateParams params = CalculateParams.create(task, layout, activity, source,
                options, request, phase, currentParams, outParams, mAtm.mSupportsMultiDisplay);
        return mUpdatable.calculate(params);
    }

    @Nullable
    private TaskDisplayAreaWrapper getDefaultTaskDisplayAreaOnDisplay(int displayId) {
        if (displayId == Display.INVALID_DISPLAY) {
            return null;
        }
        DisplayContent dc = mAtm.mRootWindowContainer.getDisplayContentOrCreate(displayId);
        if (dc == null) {
            return null;
        }
        return TaskDisplayAreaWrapper.create(dc.getDefaultTaskDisplayArea());
    }

    /**
     * Calculates the default {@link TaskDisplayAreaWrapper} for a task. We attempt to put
     * the activity within the same display area if possible. The strategy is to find the display
     * in the following order:
     *
     * <ol>
     *     <li>The display area of the top activity from the launching process will be used</li>
     *     <li>The display area of the top activity from the real launching process will be used
     *     </li>
     *     <li>Default display area from the associated root window container.</li>
     * </ol>
     * @param activityRecordWrapper the activity being started
     * @param requestWrapper optional {@link RequestWrapper} made to start the activity record
     * @return the list of {@link TaskDisplayAreaWrapper} to house the task
     */
    private List<TaskDisplayAreaWrapper> getFallbackDisplayAreasForActivity(
            @NonNull ActivityRecordWrapper activityRecordWrapper,
            @Nullable RequestWrapper requestWrapper) {
        ActivityRecord activityRecord = activityRecordWrapper.getActivityRecord();
        Request request = requestWrapper != null ? requestWrapper.getRequest() : null;
        mFallBackDisplayAreaList.clear();

        WindowProcessController controllerFromLaunchingRecord = mAtm.getProcessController(
                activityRecord.getLaunchedFromPid(), activityRecord.getLaunchedFromUid());
        TaskDisplayArea displayAreaForLaunchingRecord = controllerFromLaunchingRecord == null
                ? null : controllerFromLaunchingRecord.getTopActivityDisplayArea();
        if (displayAreaForLaunchingRecord != null) {
            mFallBackDisplayAreaList.add(
                    TaskDisplayAreaWrapper.create(displayAreaForLaunchingRecord));
        }

        WindowProcessController controllerFromProcess = mAtm.getProcessController(
                activityRecord.getProcessName(), activityRecord.getUid());
        TaskDisplayArea displayAreaForRecord = controllerFromProcess == null ? null
                : controllerFromProcess.getTopActivityDisplayArea();
        if (displayAreaForRecord != null) {
            mFallBackDisplayAreaList.add(TaskDisplayAreaWrapper.create(displayAreaForRecord));
        }

        WindowProcessController controllerFromRequest =
                request == null ? null : mAtm.getProcessController(request.realCallingPid,
                        request.realCallingUid);
        TaskDisplayArea displayAreaFromSourceProcess = controllerFromRequest == null ? null
                : controllerFromRequest.getTopActivityDisplayArea();
        if (displayAreaFromSourceProcess != null) {
            mFallBackDisplayAreaList.add(
                    TaskDisplayAreaWrapper.create(displayAreaFromSourceProcess));
        }
        return mFallBackDisplayAreaList;
    }

    @Nullable
    private TaskDisplayAreaWrapper findTaskDisplayArea(int displayId, int featureId) {
        DisplayContent display = mAtm.mRootWindowContainer.getDisplayContent(displayId);
        if (display == null) {
            return null;
        }
        TaskDisplayArea tda = display.getItemFromTaskDisplayAreas(
                displayArea -> displayArea.mFeatureId == featureId ? displayArea : null);
        return TaskDisplayAreaWrapper.create(tda);
    }

    private final CarLaunchParamsModifierInterface mBuiltinInterface =
            new CarLaunchParamsModifierInterface() {
        @Nullable
        @Override
        public TaskDisplayAreaWrapper findTaskDisplayArea(int displayId, int featureId) {
            return CarLaunchParamsModifier.this.findTaskDisplayArea(displayId, featureId);
        }

        @Nullable
        @Override
        public TaskDisplayAreaWrapper getDefaultTaskDisplayAreaOnDisplay(int displayId) {
            return CarLaunchParamsModifier.this.getDefaultTaskDisplayAreaOnDisplay(displayId);
        }

        @NonNull
        @Override
        public List<TaskDisplayAreaWrapper> getFallbackDisplayAreasForActivity(
                @NonNull ActivityRecordWrapper activityRecord, @Nullable RequestWrapper request) {
            return CarLaunchParamsModifier.this.getFallbackDisplayAreasForActivity(
                    activityRecord, request);
        }

        @NonNull
        @Override
        public Pair<Integer, Integer> getCurrentAndTargetUserIds() {
            return mAtm.mAmInternal.getCurrentAndTargetUserIds();
        }

        @Override
        public int getUserAssignedToDisplay(int displayId) {
            UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
            int userId = umi.getUserAssignedToDisplay(displayId);
            return userId;
        }

        @Override
        public int getMainDisplayAssignedToUser(int userId) {
            UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
            int displayId = umi.getMainDisplayAssignedToUser(userId);
            return displayId;
        }
    };
}
