chromium/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/MultiInstanceStateUnitTest.java

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.multiwindow;

import android.app.Activity;
import android.app.ActivityManager.AppTask;
import android.app.ActivityManager.RecentTaskInfo;
import android.content.ComponentName;
import android.text.TextUtils;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.chrome.browser.util.AndroidTaskUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** Unit tests for MultiInstanceState. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        manifest = Config.NONE,
        shadows = {MultiInstanceStateUnitTest.ShadowAndroidTaskUtils.class})
public class MultiInstanceStateUnitTest {
    @Implements(AndroidTaskUtils.class)
    static class ShadowAndroidTaskUtils {
        @Implementation
        public static RecentTaskInfo getTaskInfoFromTask(AppTask task) {
            return sTasks.get(task);
        }
    }

    private static Map<AppTask, RecentTaskInfo> sTasks = new HashMap<>();

    private MultiInstanceState mMultiInstanceState;

    private String mBaseActivityClassName = BrowserActivity.class.getName();

    private static class BaseActivity extends Activity {
        private int mTaskId;

        private void setTaskId(int taskId) {
            mTaskId = taskId;
        }

        @Override
        public int getTaskId() {
            return mTaskId;
        }

        public void stop() {
            ApplicationStatus.onStateChangeForTesting(this, ActivityState.STOPPED);
        }

        @Override
        public void destroy() {
            AppTask appTask = null;
            for (AppTask task : sTasks.keySet()) {
                if (AndroidTaskUtils.getTaskInfoFromTask(task).id == mTaskId) appTask = task;
            }
            sTasks.remove(appTask);
            ApplicationStatus.onStateChangeForTesting(this, ActivityState.DESTROYED);
        }
    }

    // Base activity for browser app.
    private static class BrowserActivity extends BaseActivity {}

    // Base activity for custom tab.
    private static class CustomTabActivity extends BaseActivity {}

    private static class ObserverHelper extends CallbackHelper {
        private int mCount;

        private void assertObserverCalled(String msg) {
            assertObserver(true, "Observer was not called: " + msg);
        }

        private void assertObserverNotCalled(String msg) {
            assertObserver(false, "Observer should not have been called: " + msg);
        }

        private void assertObserver(boolean called, String message) {
            try {
                if (called) {
                    Assert.assertTrue(message, getCallCount() == mCount + 1);
                } else {
                    Assert.assertFalse(message, getCallCount() == mCount + 1);
                }
            } finally {
                mCount = getCallCount();
            }
        }
    }

    @Before
    public void setUp() {
        MultiInstanceState.maybeCreate(this::getChromeTasks, this::matchesBaseActivity);
        mMultiInstanceState = MultiInstanceState.getInstanceForTesting();
    }

    @After
    public void tearDown() {
        ApplicationStatus.destroyForJUnitTests();
        sTasks.clear();
        mMultiInstanceState.clear();
        mMultiInstanceState = null;
    }

    private BaseActivity createTaskAndLaunchActivity(int taskId, BaseActivity activity) {
        activity.setTaskId(taskId);
        RecentTaskInfo taskInfo = new RecentTaskInfo();
        taskInfo.id = taskId;
        taskInfo.baseActivity =
                new ComponentName(
                        activity.getClass().getPackage().getName(), activity.getClass().getName());
        sTasks.put(new AppTask(null), taskInfo);
        ApplicationStatus.onStateChangeForTesting(activity, ActivityState.CREATED);
        ApplicationStatus.onStateChangeForTesting(activity, ActivityState.RESUMED);
        return activity;
    }

    private List<AppTask> getChromeTasks() {
        return new ArrayList<AppTask>(sTasks.keySet());
    }

    private boolean matchesBaseActivity(String name) {
        return TextUtils.equals(name, mBaseActivityClassName);
    }

    @Test
    public void testVisibilityObserverForMultiInstance() {
        ObserverHelper helper = new ObserverHelper();
        mMultiInstanceState.addObserver((visible) -> helper.notifyCalled());

        BaseActivity baseActivity1 = createTaskAndLaunchActivity(29, new BrowserActivity());
        assertInSingleInstanceMode("initial state");

        BaseActivity baseActivity2 = createTaskAndLaunchActivity(31, new BrowserActivity());
        helper.assertObserverCalled("a resume event for an activity of the 2nd task triggered");
        assertInMultiInstanceMode("the new task's base activity was resumed");

        BaseActivity baseActivity3 = createTaskAndLaunchActivity(37, new BrowserActivity());
        helper.assertObserverNotCalled("already in multi-instance mode");
        assertInMultiInstanceMode("already in multi-instance mode");

        baseActivity3.stop();
        helper.assertObserverNotCalled("already in multi-instance mode");
        assertInMultiInstanceMode("already in multi-instance mode");

        baseActivity2.stop();
        // Only task29 (BaseActivity1) is visible now. Multi-instance state goes off.
        helper.assertObserverCalled("A single visible task left");
        assertInSingleInstanceMode("A single visible task left");
    }

    @Test
    public void testRuleOutOtherBaseActivityTasks() {
        BaseActivity baseActivity1 = createTaskAndLaunchActivity(29, new CustomTabActivity());
        BaseActivity baseActivity2 = createTaskAndLaunchActivity(31, new CustomTabActivity());
        assertInSingleInstanceMode("Base activity is not legit: " + baseActivity1);
    }

    private void assertInMultiInstanceMode(String msg) {
        Assert.assertTrue(
                "Should be in multi-instance mode: " + msg,
                mMultiInstanceState.isInMultiInstanceMode());
    }

    private void assertInSingleInstanceMode(String msg) {
        Assert.assertFalse(
                "Should be in single-instance mode: " + msg,
                mMultiInstanceState.isInMultiInstanceMode());
    }
}