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

import static androidx.test.InstrumentationRegistry.getContext;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static androidx.test.InstrumentationRegistry.getTargetContext;

import static com.android.launcher3.common.WidgetUtils.createWidgetInfo;
import static com.android.launcher3.testcomponent.TestCommandReceiver.EXTRA_VALUE;
import static com.android.launcher3.testcomponent.TestCommandReceiver.SET_LIST_VIEW_SERVICE_BINDER;
import static com.android.quickstep.NavigationModeSwitchRule.Mode.ZERO_BUTTON;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;

import android.appwidget.AppWidgetManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.RemoteViews;

import androidx.test.filters.LargeTest;
import androidx.test.filters.Suppress;
import androidx.test.runner.AndroidJUnit4;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.Until;

import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.tapl.Background;
import com.android.launcher3.testcomponent.ListViewService;
import com.android.launcher3.testcomponent.ListViewService.SimpleViewsFactory;
import com.android.launcher3.testcomponent.TestCommandReceiver;
import com.android.launcher3.ui.TaplTestsLauncher3;
import com.android.launcher3.ui.TestViewHelpers;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.lang.reflect.Field;
import java.util.function.IntConsumer;

/**
 * Test to verify view inflation does not happen during swipe up.
 * To verify view inflation, we setup a dummy ViewConfiguration and check if any call to that class
 * does from a View.init method or not.
 *
 * Alternative approaches considered:
 *    Overriding LayoutInflater: This does not cover views initialized
 *        directly (ex: new LinearLayout)
 *    Using ExtendedMockito: Mocking static methods from platform classes (loaded in zygote) makes
 *        the main thread extremely slow and untestable
 *
 * Suppressed until b/141579810 is resolved
 */
@Suppress
@LargeTest
@RunWith(AndroidJUnit4.class)
public class ViewInflationDuringSwipeUp extends AbstractQuickStepTest {

    private ContentResolver mResolver;
    private SparseArray<ViewConfiguration> mConfigMap;
    private InitTracker mInitTracker;

    @Before
    public void setUp() throws Exception {
        super.setUp();

        // Workaround for b/142351228, when there are no activities, the system may not destroy the
        // activity correctly for activities under instrumentation, which can leave two concurrent
        // activities, which changes the order in which the activities are cleaned up (overlapping
        // stop and start) leading to all sort of issues. To workaround this, ensure that the test
        // is started only after starting another app.
        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));

        TaplTestsLauncher3.initialize(this);

        mResolver = mTargetContext.getContentResolver();
        LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);

        // Get static configuration map
        Field field = ViewConfiguration.class.getDeclaredField("sConfigurations");
        field.setAccessible(true);
        mConfigMap = (SparseArray<ViewConfiguration>) field.get(null);

        mInitTracker = new InitTracker();
    }

    @Test
    @NavigationModeSwitch(mode = ZERO_BUTTON)
    public void testSwipeUpFromApp() throws Exception {
        try {
            // Go to overview once so that all views are initialized and cached
            startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
            mLauncher.getBackground().switchToOverview().dismissAllTasks();

            // Track view creations
            mInitTracker.startTracking();

            startTestActivity(2);
            mLauncher.getBackground().switchToOverview();

            assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount);
        } finally {
            mConfigMap.clear();
        }
    }

    @Test
    @NavigationModeSwitch(mode = ZERO_BUTTON)
    public void testSwipeUpFromApp_widget_update() {
        String dummyText = "Some random dummy text";

        executeSwipeUpTestWithWidget(
                widgetId -> { },
                widgetId -> AppWidgetManager.getInstance(getContext())
                        .updateAppWidget(widgetId, createMainWidgetViews(dummyText)),
                dummyText);
    }

    @Test
    @NavigationModeSwitch(mode = ZERO_BUTTON)
    public void testSwipeUp_with_list_widgets() {
        SimpleViewsFactory viewFactory = new SimpleViewsFactory();
        viewFactory.viewCount = 1;
        Bundle args = new Bundle();
        args.putBinder(EXTRA_VALUE, viewFactory.toBinder());
        TestCommandReceiver.callCommand(SET_LIST_VIEW_SERVICE_BINDER, null, args);

        try {
            executeSwipeUpTestWithWidget(
                    widgetId -> {
                        // Initialize widget
                        RemoteViews views = createMainWidgetViews("List widget title");
                        views.setRemoteAdapter(android.R.id.list,
                                new Intent(getContext(), ListViewService.class));
                        AppWidgetManager.getInstance(getContext()).updateAppWidget(widgetId, views);
                        verifyWidget(viewFactory.getLabel(0));
                    },
                    widgetId -> {
                        // Update widget
                        viewFactory.viewCount = 2;
                        AppWidgetManager.getInstance(getContext())
                                .notifyAppWidgetViewDataChanged(widgetId, android.R.id.list);
                    },
                    viewFactory.getLabel(1)
            );
        } finally {
            TestCommandReceiver.callCommand(SET_LIST_VIEW_SERVICE_BINDER, null, new Bundle());
        }
    }

    private void executeSwipeUpTestWithWidget(IntConsumer widgetIdCreationCallback,
            IntConsumer updateBeforeSwipeUp, String finalWidgetText) {
        try {
            // Clear all existing data
            LauncherSettings.Settings.call(mResolver,
                    LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
            LauncherSettings.Settings.call(mResolver,
                    LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
            LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
            LauncherAppWidgetInfo item = createWidgetInfo(info, getTargetContext(), true);

            addItemToScreen(item);
            assertTrue("Widget is not present",
                    mLauncher.pressHome().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null);
            int widgetId = item.appWidgetId;

            // Verify widget id
            widgetIdCreationCallback.accept(widgetId);

            // Go to overview once so that all views are initialized and cached
            startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
            mLauncher.getBackground().switchToOverview().dismissAllTasks();

            // Track view creations
            mInitTracker.startTracking();

            startTestActivity(2);
            Background background = mLauncher.getBackground();

            // Update widget
            updateBeforeSwipeUp.accept(widgetId);

            background.switchToOverview();
            assertEquals("Views inflated during swipe up", 0, mInitTracker.viewInitCount);

            // Widget is updated when going home
            mInitTracker.disableLog();
            mLauncher.pressHome();
            verifyWidget(finalWidgetText);
            assertNotEquals(1, mInitTracker.viewInitCount);
        } finally {
            mConfigMap.clear();
        }
    }

    private void verifyWidget(String text) {
        assertNotNull("Widget not updated",
                UiDevice.getInstance(getInstrumentation())
                        .wait(Until.findObject(By.text(text)), DEFAULT_UI_TIMEOUT));
    }

    private RemoteViews createMainWidgetViews(String title) {
        Context c = getContext();
        int layoutId = c.getResources().getIdentifier(
                "test_layout_widget_list", "layout", c.getPackageName());
        RemoteViews views = new RemoteViews(c.getPackageName(), layoutId);
        views.setTextViewText(android.R.id.text1, title);
        return views;
    }

    private class InitTracker implements Answer {

        public int viewInitCount = 0;

        public boolean log = true;

        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            Exception ex = new Exception();

            boolean found = false;
            for (StackTraceElement ste : ex.getStackTrace()) {
                if ("<init>".equals(ste.getMethodName())
                        && View.class.getName().equals(ste.getClassName())) {
                    found = true;
                    break;
                }
            }
            if (found) {
                viewInitCount++;
                if (log) {
                    Log.d("InitTracker", "New view inflated", ex);
                }

            }
            return invocation.callRealMethod();
        }

        public void disableLog() {
            log = false;
        }

        public void startTracking() {
            ViewConfiguration vc = ViewConfiguration.get(mTargetContext);
            ViewConfiguration spyVC = spy(vc);
            mConfigMap.put(mConfigMap.keyAt(mConfigMap.indexOfValue(vc)), spyVC);
            doAnswer(this).when(spyVC).getScaledTouchSlop();
        }
    }
}
