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

import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
import static android.server.wm.WindowManagerState.STATE_INITIALIZING;
import static android.server.wm.WindowManagerState.STATE_STOPPED;
import static android.server.wm.app.Components.BROADCAST_RECEIVER_ACTIVITY;
import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
import static android.server.wm.app.Components.NO_RELAUNCH_ACTIVITY;
import static android.server.wm.app.Components.TEST_ACTIVITY;
import static android.server.wm.app.Components.TRANSLUCENT_ACTIVITY;
import static android.server.wm.app.Components.TestActivity.COMMAND_NAVIGATE_UP_TO;
import static android.server.wm.app.Components.TestActivity.COMMAND_START_ACTIVITIES;
import static android.server.wm.app.Components.TestActivity.EXTRA_INTENT;
import static android.server.wm.app.Components.TestActivity.EXTRA_INTENTS;
import static android.server.wm.app27.Components.SDK_27_LAUNCHING_ACTIVITY;
import static android.server.wm.second.Components.SECOND_ACTIVITY;
import static android.view.Display.DEFAULT_DISPLAY;

import static com.google.common.truth.Truth.assertWithMessage;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;

import android.app.Activity;
import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.platform.test.annotations.Presubmit;
import android.server.wm.CommandSession.ActivitySession;
import android.server.wm.intent.Activities;

import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Build/Install/Run:
 *     atest CtsWindowManagerDeviceTestCases:StartActivityTests
 */
@Presubmit
public class StartActivityTests extends ActivityManagerTestBase {

    @Test
    public void testStartHomeIfNoActivities() {
        if (!hasHomeScreen()) {
	    return;
	}

        final ComponentName defaultHome = getDefaultHomeComponent();
        final int[] allActivityTypes = Arrays.copyOf(ALL_ACTIVITY_TYPE_BUT_HOME,
                ALL_ACTIVITY_TYPE_BUT_HOME.length + 1);
        allActivityTypes[allActivityTypes.length - 1] = ACTIVITY_TYPE_HOME;
        removeRootTasksWithActivityTypes(allActivityTypes);

        waitAndAssertResumedActivity(defaultHome,
                "Home activity should be restarted after force-finish");

        stopTestPackage(defaultHome.getPackageName());

        waitAndAssertResumedActivity(defaultHome,
                "Home activity should be restarted after force-stop");
    }

    /**
     * Ensures {@link Activity} without {@link Intent#FLAG_ACTIVITY_NEW_TASK} can only be launched
     * from an {@link Activity} {@link android.content.Context}.
     */
    @Test
    public void testStartActivityContexts() {
        // Note by default LaunchActivityBuilder will use LAUNCHING_ACTIVITY to launch the target.

        // Launch Activity from application context without FLAG_ACTIVITY_NEW_TASK.
        getLaunchActivityBuilder()
                .setTargetActivity(TEST_ACTIVITY)
                .setUseApplicationContext(true)
                .setSuppressExceptions(true)
                .setWaitForLaunched(false)
                .execute();

        // Launch another activity from activity to ensure previous one has done.
        getLaunchActivityBuilder()
                .setTargetActivity(NO_RELAUNCH_ACTIVITY)
                .execute();

        mWmState.computeState(NO_RELAUNCH_ACTIVITY);

        // Verify Activity was not started.
        assertFalse(mWmState.containsActivity(TEST_ACTIVITY));
        mWmState.assertResumedActivity(
                "Activity launched from activity context should be present", NO_RELAUNCH_ACTIVITY);
    }

    /**
     * Ensures you can start an {@link Activity} from a non {@link Activity}
     * {@link android.content.Context} with the {@code FLAG_ACTIVITY_NEW_TASK}.
     */
    @Test
    public void testStartActivityNewTask() throws Exception {
        // Launch Activity from application context.
        getLaunchActivityBuilder()
                .setTargetActivity(TEST_ACTIVITY)
                .setUseApplicationContext(true)
                .setSuppressExceptions(true)
                .setNewTask(true)
                .execute();

        mWmState.computeState(TEST_ACTIVITY);
        mWmState.assertResumedActivity("Test Activity should be started with new task flag",
                TEST_ACTIVITY);
    }

    @Test
    public void testStartActivityTaskLaunchBehind() {
        // launch an activity
        getLaunchActivityBuilder()
                .setTargetActivity(TEST_ACTIVITY)
                .setUseInstrumentation()
                .setNewTask(true)
                .execute();

        // launch an activity behind
        getLaunchActivityBuilder()
                .setTargetActivity(TRANSLUCENT_ACTIVITY)
                .setUseInstrumentation()
                .setIntentFlags(FLAG_ACTIVITY_NEW_DOCUMENT)
                .setNewTask(true)
                .setLaunchTaskBehind(true)
                .execute();

        waitAndAssertActivityState(TRANSLUCENT_ACTIVITY, STATE_STOPPED,
                "Activity should be stopped");
        mWmState.assertResumedActivity("Test Activity should be remained on top and resumed",
                TEST_ACTIVITY);
    }

    @Test
    public void testStartActivityFromFinishingActivity() {
        // launch TEST_ACTIVITY from LAUNCHING_ACTIVITY
        getLaunchActivityBuilder()
                .setTargetActivity(TEST_ACTIVITY)
                .setFinishBeforeLaunch(true)
                .execute();

        // launch LAUNCHING_ACTIVITY again
        getLaunchActivityBuilder()
                .setTargetActivity(LAUNCHING_ACTIVITY)
                .setUseInstrumentation()
                .execute();

        // make sure TEST_ACTIVITY is still on top and resumed
        mWmState.computeState(TEST_ACTIVITY);
        mWmState.assertResumedActivity("Test Activity should be remained on top and resumed",
                TEST_ACTIVITY);
    }

    /**
     * Ensures you can start an {@link Activity} from a non {@link Activity}
     * {@link android.content.Context} when the target sdk is between N and O Mr1.
     * @throws Exception
     */
    @Test
    public void testLegacyStartActivityFromNonActivityContext() {
        getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY)
                .setLaunchingActivity(SDK_27_LAUNCHING_ACTIVITY)
                .setUseApplicationContext(true)
                .execute();

        mWmState.computeState(TEST_ACTIVITY);
        mWmState.assertResumedActivity("Test Activity should be resumed without older sdk",
                TEST_ACTIVITY);
    }

    /**
     * Starts 3 activities A, B, C in the same task. A and B belong to current package and are not
     * exported. C belongs to a different package with different uid. After C called
     * {@link Activity#navigateUpTo(Intent)} with the intent of A, the activities B, C should be
     * finished and instead of creating a new instance of A, the original A should become the top
     * activity because the caller C in different uid cannot launch a non-exported activity.
     */
    @Test
    public void testStartActivityByNavigateUpToFromDiffUid() {
        final Intent intent1 = new Intent(mContext, Activities.RegularActivity.class);
        final String regularActivityName = Activities.RegularActivity.class.getName();
        final TestActivitySession<Activities.RegularActivity> activitySession1 =
                createManagedTestActivitySession();
        activitySession1.launchTestActivityOnDisplaySync(regularActivityName, intent1,
                DEFAULT_DISPLAY);
        final TestActivitySession<Activities.SingleTopActivity> activitySession2 =
                createManagedTestActivitySession();
        activitySession2.launchTestActivityOnDisplaySync(Activities.SingleTopActivity.class,
                DEFAULT_DISPLAY);

        final CommandSession.ActivitySession activitySession3 =
                createManagedActivityClientSession().startActivity(
                        new CommandSession.DefaultLaunchProxy() {
                            @Override
                            public void execute() {
                                final Intent intent = new Intent().setComponent(TEST_ACTIVITY);
                                mLaunchInjector.setupIntent(intent);
                                activitySession2.getActivity().startActivity(intent);
                            }
                        });

        final Bundle data = new Bundle();
        data.putParcelable(EXTRA_INTENT, intent1);
        activitySession3.sendCommand(COMMAND_NAVIGATE_UP_TO, data);

        waitAndAssertTopResumedActivity(intent1.getComponent(), DEFAULT_DISPLAY,
                "navigateUpTo should return to the first activity");
        // Make sure the resumed first activity is the original instance.
        assertFalse("The target of navigateUpTo should not be destroyed",
                activitySession1.getActivity().isDestroyed());

        // The activities above the first one should be destroyed.
        mWmState.waitAndAssertActivityRemoved(
                activitySession3.getOriginalLaunchIntent().getComponent());
        mWmState.waitAndAssertActivityRemoved(activitySession2.getActivity().getComponentName());
    }

    /**
     * Assume there are 3 activities (A1, A2, A3) with different task affinities and the same uid.
     * After A1 called {@link Activity#startActivities} to start A2 (with NEW_TASK) and A3, the
     * result should be 2 tasks: [A1] and [A2, A3].
     */
    @Test
    public void testStartActivitiesInNewAndSameTask() {
        final int[] taskIds = startActivitiesAndGetTaskIds(new Intent[] {
                new Intent().setComponent(NO_RELAUNCH_ACTIVITY)
                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
                new Intent().setComponent(LAUNCHING_ACTIVITY) });

        assertNotEquals("The activity with different task affinity started by flag NEW_TASK"
                + " should be in a different task", taskIds[0], taskIds[1]);
        assertEquals("The activity started without flag NEW_TASK should be put in the same task",
                taskIds[1], taskIds[2]);
    }

    @Test
    public void testNormalActivityCanNotSetActivityType() {
        // Activities should not be started if the launch activity type is set.
        boolean useShellPermission = false;
        startingActivityWithType(ACTIVITY_TYPE_STANDARD, useShellPermission);
        startingActivityWithType(ACTIVITY_TYPE_HOME, useShellPermission);
        startingActivityWithType(ACTIVITY_TYPE_RECENTS, useShellPermission);
        startingActivityWithType(ACTIVITY_TYPE_ASSISTANT, useShellPermission);
        startingActivityWithType(ACTIVITY_TYPE_DREAM, useShellPermission);

        // Activities can be started because they are started with shell permissions.
        useShellPermission = true;
        startingActivityWithType(ACTIVITY_TYPE_STANDARD, useShellPermission);
        startingActivityWithType(ACTIVITY_TYPE_HOME, useShellPermission);
        startingActivityWithType(ACTIVITY_TYPE_RECENTS, useShellPermission);
        startingActivityWithType(ACTIVITY_TYPE_ASSISTANT, useShellPermission);
        startingActivityWithType(ACTIVITY_TYPE_DREAM, useShellPermission);
    }

    private void startingActivityWithType(int type, boolean useShellPermission) {
        separateTestJournal();
        getLaunchActivityBuilder()
                .setTargetActivity(BROADCAST_RECEIVER_ACTIVITY)
                .setUseInstrumentation()
                .setWithShellPermission(useShellPermission)
                .setActivityType(type)
                .setWaitForLaunched(false)
                .setMultipleTask(true)
                .execute();

        mWmState.computeState();
        if (useShellPermission) {
            waitAndAssertResumedActivity(BROADCAST_RECEIVER_ACTIVITY,
                    "Activity should be started and resumed");
            mWmState.assertFrontStackActivityType("The activity type should be same as requested.",
                    type);
            mBroadcastActionTrigger.finishBroadcastReceiverActivity();
            mWmState.waitAndAssertActivityRemoved(BROADCAST_RECEIVER_ACTIVITY);
        } else {
            assertSecurityExceptionFromActivityLauncher();
        }
    }

    /**
     * Assume there are 3 activities (A1, A2, B1) with default launch mode. The uid of B1 is
     * different from A1 and A2. After A1 called {@link Activity#startActivities} to start B1 and
     * A2, the result should be 3 tasks.
     */
    @Test
    public void testStartActivitiesWithDiffUidNotInSameTask() {
        final int[] taskIds = startActivitiesAndGetTaskIds(new Intent[] {
                new Intent().setComponent(SECOND_ACTIVITY)
                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
                new Intent().setComponent(LAUNCHING_ACTIVITY) });

        assertNotEquals("The activity in a different application (uid) started by flag NEW_TASK"
                + " should be in a different task", taskIds[0], taskIds[1]);
        assertWithMessage("The last started activity should be in a different task because "
                + SECOND_ACTIVITY + " has a different uid from the source caller")
                        .that(taskIds[2]).isNotIn(Arrays.asList(taskIds[0], taskIds[1]));
    }

    /**
     * Test the activity launched with ActivityOptions#setTaskOverlay should remain on top of the
     * task after start another activity.
     */
    @Test
    public void testStartActivitiesTaskOverlayStayOnTop() {
        final Intent baseIntent = new Intent(mContext, Activities.RegularActivity.class);
        final String regularActivityName = Activities.RegularActivity.class.getName();
        final TestActivitySession<Activities.RegularActivity> activitySession =
                createManagedTestActivitySession();
        activitySession.launchTestActivityOnDisplaySync(regularActivityName, baseIntent,
                DEFAULT_DISPLAY);
        mWmState.computeState(baseIntent.getComponent());
        final int taskId = mWmState.getTaskByActivity(baseIntent.getComponent()).getTaskId();
        final Activity baseActivity = activitySession.getActivity();

        final ActivityOptions overlayOptions = ActivityOptions.makeBasic();
        overlayOptions.setTaskOverlay(true, true);
        overlayOptions.setLaunchTaskId(taskId);
        final Intent taskOverlay = new Intent().setComponent(SECOND_ACTIVITY);
        runWithShellPermission(() ->
                baseActivity.startActivity(taskOverlay, overlayOptions.toBundle()));

        waitAndAssertResumedActivity(taskOverlay.getComponent(),
                "taskOverlay activity on top");
        final Intent behindOverlay = new Intent().setComponent(TEST_ACTIVITY);
        baseActivity.startActivity(behindOverlay);

        waitAndAssertActivityState(TEST_ACTIVITY, STATE_INITIALIZING,
                "Activity behind taskOverlay should not resumed");
        // check order: SecondActivity(top) -> TestActivity -> RegularActivity(base)
        final List<String> activitiesOrder = mWmState.getTaskByActivity(baseIntent.getComponent())
                .mActivities
                .stream()
                .map(WindowManagerState.Activity::getName)
                .collect(Collectors.toList());

        final List<String> expectedOrder = Stream.of(
                SECOND_ACTIVITY,
                TEST_ACTIVITY,
                baseIntent.getComponent())
                .map(c -> c.flattenToShortString())
                .collect(Collectors.toList());
        assertEquals(activitiesOrder, expectedOrder);
        mWmState.assertResumedActivity("TaskOverlay activity should be remained on top and "
                        + "resumed", taskOverlay.getComponent());
    }

    /**
     * Invokes {@link android.app.Activity#startActivities} from {@link #TEST_ACTIVITY} and returns
     * the task id of each started activity (the index 0 will be the caller {@link #TEST_ACTIVITY}).
     */
    private int[] startActivitiesAndGetTaskIds(Intent[] intents) {
        final ActivitySession activity = createManagedActivityClientSession()
                .startActivity(getLaunchActivityBuilder().setUseInstrumentation());
        final Bundle intentBundle = new Bundle();
        intentBundle.putParcelableArray(EXTRA_INTENTS, intents);
        // The {@link Activity#startActivities} cannot be called from the instrumentation
        // package because the implementation (given by test runner) may be overridden.
        activity.sendCommand(COMMAND_START_ACTIVITIES, intentBundle);

        final int[] taskIds = new int[intents.length + 1];
        // The {@code intents} are started, wait for the last (top) activity to be ready and then
        // verify their task ids.
        mWmState.computeState(intents[intents.length - 1].getComponent());
        taskIds[0] = mWmState.getTaskByActivity(TEST_ACTIVITY).getTaskId();
        for (int i = 0; i < intents.length; i++) {
            taskIds[i + 1] = mWmState.getTaskByActivity(intents[i].getComponent()).getTaskId();
        }
        return taskIds;
    }
}
