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

import static android.perftests.utils.ManualBenchmarkState.StatsReport;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static org.hamcrest.core.AnyOf.anyOf;
import static org.hamcrest.core.Is.is;

import android.app.ActivityManager;
import android.app.ActivityManager.TaskSnapshot;
import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.SystemClock;
import android.perftests.utils.ManualBenchmarkState;
import android.perftests.utils.ManualBenchmarkState.ManualBenchmarkTest;
import android.perftests.utils.PerfManualStatusReporter;
import android.util.Pair;
import android.view.IRecentsAnimationController;
import android.view.IRecentsAnimationRunner;
import android.view.RemoteAnimationTarget;

import androidx.test.filters.LargeTest;
import androidx.test.runner.lifecycle.Stage;

import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

@RunWith(Parameterized.class)
@LargeTest
public class RecentsAnimationPerfTest extends WindowManagerPerfTestBase {
    private static Intent sRecentsIntent;

    @Rule
    public final PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter();

    @Rule
    public final PerfTestActivityRule mActivityRule =
            new PerfTestActivityRule(true /* launchActivity */);

    private long mMeasuredTimeNs;

    /**
     * Used to skip each test method if there is error. It cannot be raised in static setup because
     * that will break the amount of target method count.
     */
    private static Exception sSetUpClassException;

    @Parameterized.Parameter(0)
    public int intervalBetweenOperations;

    @Parameterized.Parameters(name = "interval{0}ms")
    public static Collection<Object[]> getParameters() {
        return Arrays.asList(new Object[][] {
                { 0 },
                { 100 },
                { 300 },
        });
    }

    @BeforeClass
    public static void setUpClass() {
        // Get the permission to invoke startRecentsActivity.
        sUiAutomation.adoptShellPermissionIdentity();

        final Context context = getInstrumentation().getContext();
        final PackageManager pm = context.getPackageManager();
        final ComponentName defaultHome = pm.getHomeActivities(new ArrayList<>());

        try {
            final ComponentName recentsComponent =
                    ComponentName.unflattenFromString(context.getResources().getString(
                            com.android.internal.R.string.config_recentsComponentName));
            final int enabledState = pm.getComponentEnabledSetting(recentsComponent);
            Assume.assumeThat(enabledState, anyOf(
                    is(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT),
                    is(PackageManager.COMPONENT_ENABLED_STATE_ENABLED)));

            final boolean homeIsRecents =
                    recentsComponent.getPackageName().equals(defaultHome.getPackageName());
            sRecentsIntent =
                    new Intent().setComponent(homeIsRecents ? defaultHome : recentsComponent);
        } catch (Exception e) {
            sSetUpClassException = e;
        }
    }

    @AfterClass
    public static void tearDownClass() {
        sSetUpClassException = null;
        try {
            // Recents activity may stop app switches. Restore the state to avoid affecting
            // the next test.
            ActivityManager.resumeAppSwitches();
        } catch (RemoteException ignored) {
        }
        sUiAutomation.dropShellPermissionIdentity();
    }

    @Before
    public void setUp() {
        Assume.assumeNoException(sSetUpClassException);
    }

    /** Simulate the timing of touch. */
    private void makeInterval() {
        SystemClock.sleep(intervalBetweenOperations);
    }

    /**
     * <pre>
     * Steps:
     * (1) Start recents activity (only make it visible).
     * (2) Finish animation, take turns to execute (a), (b).
     *     (a) Move recents activity to top.
     * ({@link com.android.server.wm.RecentsAnimationController#REORDER_MOVE_TO_TOP})
     *         Move test app to top by startActivityFromRecents.
     *     (b) Cancel (it is similar to swipe a little distance and give up to enter recents).
     * ({@link com.android.server.wm.RecentsAnimationController#REORDER_MOVE_TO_ORIGINAL_POSITION})
     * (3) Loop (1).
     * </pre>
     */
    @Test
    @ManualBenchmarkTest(
            warmupDurationNs = TIME_1_S_IN_NS,
            targetTestDurationNs = TIME_5_S_IN_NS,
            statsReport = @StatsReport(flags = StatsReport.FLAG_ITERATION | StatsReport.FLAG_MEAN
                    | StatsReport.FLAG_COEFFICIENT_VAR))
    public void testRecentsAnimation() throws Throwable {
        final ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        final IActivityTaskManager atm = ActivityTaskManager.getService();

        final ArrayList<Pair<String, Boolean>> finishCases = new ArrayList<>();
        // Real launch the recents activity.
        finishCases.add(new Pair<>("finishMoveToTop", true));
        // Return to the original top.
        finishCases.add(new Pair<>("finishCancel", false));

        // Ensure startRecentsActivity won't be called before finishing the animation.
        final Semaphore recentsSemaphore = new Semaphore(1);

        final int testActivityTaskId = mActivityRule.getActivity().getTaskId();
        final IRecentsAnimationRunner.Stub anim = new IRecentsAnimationRunner.Stub() {
            int mIteration;

            @Override
            public void onAnimationStart(IRecentsAnimationController controller,
                    RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
                    Rect homeContentInsets, Rect minimizedHomeBounds) throws RemoteException {
                final Pair<String, Boolean> finishCase = finishCases.get(mIteration++ % 2);
                final boolean moveRecentsToTop = finishCase.second;
                makeInterval();

                long startTime = SystemClock.elapsedRealtimeNanos();
                controller.finish(moveRecentsToTop, false /* sendUserLeaveHint */);
                final long elapsedTimeNsOfFinish = SystemClock.elapsedRealtimeNanos() - startTime;
                mMeasuredTimeNs += elapsedTimeNsOfFinish;
                state.addExtraResult(finishCase.first, elapsedTimeNsOfFinish);

                if (moveRecentsToTop) {
                    mActivityRule.waitForIdleSync(Stage.STOPPED);

                    startTime = SystemClock.elapsedRealtimeNanos();
                    atm.startActivityFromRecents(testActivityTaskId, null /* options */);
                    final long elapsedTimeNs = SystemClock.elapsedRealtimeNanos() - startTime;
                    mMeasuredTimeNs += elapsedTimeNs;
                    state.addExtraResult("startFromRecents", elapsedTimeNs);

                    mActivityRule.waitForIdleSync(Stage.RESUMED);
                }

                makeInterval();
                recentsSemaphore.release();
            }

            @Override
            public void onAnimationCanceled(TaskSnapshot taskSnapshot) throws RemoteException {
                Assume.assumeNoException(
                        new AssertionError("onAnimationCanceled should not be called"));
            }

            @Override
            public void onTaskAppeared(RemoteAnimationTarget app) throws RemoteException {
                /* no-op */
            }
        };

        recentsSemaphore.tryAcquire();
        while (state.keepRunning(mMeasuredTimeNs)) {
            mMeasuredTimeNs = 0;

            final long startTime = SystemClock.elapsedRealtimeNanos();
            atm.startRecentsActivity(sRecentsIntent, null /* unused */, anim);
            final long elapsedTimeNsOfStart = SystemClock.elapsedRealtimeNanos() - startTime;
            mMeasuredTimeNs += elapsedTimeNsOfStart;
            state.addExtraResult("start", elapsedTimeNsOfStart);

            // Ensure the animation callback is done.
            Assume.assumeTrue(recentsSemaphore.tryAcquire(TIME_5_S_IN_NS, TimeUnit.NANOSECONDS));
        }
    }
}
