chromium/content/public/android/junit/src/org/chromium/content/browser/selection/SelectionPopupControllerTest.java

// Copyright 2017 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.content.browser.selection;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;

import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.os.Build;
import android.provider.Settings;
import android.view.ActionMode;
import android.view.Menu;
import android.view.ViewGroup;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.fakes.RoboMenu;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.util.ReflectionHelpers;

import org.chromium.base.ContextUtils;
import org.chromium.base.FeatureList;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.content.browser.GestureListenerManagerImpl;
import org.chromium.content.browser.PopupController;
import org.chromium.content.browser.RenderCoordinatesImpl;
import org.chromium.content.browser.RenderWidgetHostViewImpl;
import org.chromium.content.browser.webcontents.WebContentsImpl;
import org.chromium.content_public.browser.ContentFeatureList;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.SelectAroundCaretResult;
import org.chromium.content_public.browser.SelectionClient;
import org.chromium.content_public.browser.SelectionEventProcessor;
import org.chromium.content_public.browser.SelectionMenuGroup;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.browser.selection.SelectionDropdownMenuDelegate;
import org.chromium.content_public.browser.test.util.TestSelectionDropdownMenuDelegate;
import org.chromium.content_public.common.ContentFeatures;
import org.chromium.ui.base.MenuSourceType;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.touch_selection.SelectionEventType;
import org.chromium.ui.touch_selection.TouchSelectionDraggableType;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;

/** Unit tests for {@link SelectionPopupController}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class SelectionPopupControllerTest {
    private SelectionPopupControllerImpl mController;
    private Context mContext;
    private WeakReference<Context> mWeakContext;
    private TypedArray mTypedArray;
    private WindowAndroid mWindowAndroid;
    private WebContentsImpl mWebContents;
    private ViewGroup mView;
    private ViewAndroidDelegate mViewAndroidDelegate;
    private ActionMode mActionMode;
    private PackageManager mPackageManager;
    private SmartSelectionEventProcessor mLogger;
    private RenderWidgetHostViewImpl mRenderWidgetHostViewImpl;
    private RenderCoordinatesImpl mRenderCoordinates;
    private ContentResolver mContentResolver;
    private PopupController mPopupController;
    private GestureListenerManagerImpl mGestureStateListenerManager;
    private RenderFrameHost mRenderFrameHost;
    private FeatureList.TestValues mTestValues;

    private static final String MOUNTAIN_FULL = "585 Franklin Street, Mountain View, CA 94041";
    private static final String MOUNTAIN = "Mountain";
    private static final String AMPHITHEATRE_FULL = "1600 Amphitheatre Parkway";
    private static final String AMPHITHEATRE = "Amphitheatre";

    private static class TestSelectionClient implements SelectionClient {
        private SelectionClient.Result mResult;
        private SelectionClient.ResultCallback mResultCallback;
        private SmartSelectionEventProcessor mLogger;

        @Override
        public void onSelectionChanged(String selection) {}

        @Override
        public void onSelectionEvent(int eventType, float posXPix, float poxYPix) {}

        @Override
        public void selectAroundCaretAck(SelectAroundCaretResult result) {}

        @Override
        public boolean requestSelectionPopupUpdates(boolean shouldSuggest) {
            mResultCallback.onClassified(mResult);
            return true;
        }

        @Override
        public void cancelAllRequests() {}

        @Override
        public SelectionEventProcessor getSelectionEventProcessor() {
            return mLogger;
        }

        public void setResult(SelectionClient.Result result) {
            mResult = result;
        }

        public void setResultCallback(SelectionClient.ResultCallback callback) {
            mResultCallback = callback;
        }

        public void setLogger(SmartSelectionEventProcessor logger) {
            mLogger = logger;
        }
    }

    private static class SelectionClientOnlyReturnTrue extends TestSelectionClient {
        @Override
        public boolean requestSelectionPopupUpdates(boolean shouldSuggest) {
            return true;
        }
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        ShadowLog.stream = System.out;

        mContext = Mockito.mock(Context.class);
        mWeakContext = new WeakReference<Context>(mContext);
        mTypedArray = Mockito.mock(TypedArray.class);
        mWindowAndroid = Mockito.mock(WindowAndroid.class);
        mWebContents = Mockito.mock(WebContentsImpl.class);
        mView = Mockito.mock(ViewGroup.class);
        mViewAndroidDelegate = ViewAndroidDelegate.createBasicDelegate(mView);
        mActionMode = Mockito.mock(ActionMode.class);
        mPackageManager = Mockito.mock(PackageManager.class);
        mRenderWidgetHostViewImpl = Mockito.mock(RenderWidgetHostViewImpl.class);
        mRenderFrameHost = Mockito.mock(RenderFrameHost.class);
        mRenderCoordinates = Mockito.mock(RenderCoordinatesImpl.class);
        mLogger = Mockito.mock(SmartSelectionEventProcessor.class);
        mPopupController = Mockito.mock(PopupController.class);
        mGestureStateListenerManager = Mockito.mock(GestureListenerManagerImpl.class);

        mTestValues = new FeatureList.TestValues();
        setDropdownMenuFeatureEnabled(false);
        mTestValues.addFeatureFlagOverride(ContentFeatures.SELECTION_MENU_ITEM_MODIFICATION, true);
        FeatureList.setTestValues(mTestValues);

        SelectionPopupControllerImpl.setDisableMagnifierForTesting(true);

        mContentResolver = RuntimeEnvironment.application.getContentResolver();
        // To let isDeviceProvisioned() call in showSelectionMenu() return true.
        Settings.Global.putInt(mContentResolver, Settings.Global.DEVICE_PROVISIONED, 1);

        when(mContext.getPackageManager()).thenReturn(mPackageManager);
        when(mContext.getContentResolver()).thenReturn(mContentResolver);
        when(mContext.obtainStyledAttributes(Mockito.any(int[].class))).thenReturn(mTypedArray);
        when(mWebContents.getRenderWidgetHostView()).thenReturn(mRenderWidgetHostViewImpl);
        when(mWebContents.getRenderCoordinates()).thenReturn(mRenderCoordinates);
        when(mRenderCoordinates.getDeviceScaleFactor()).thenReturn(1.f);
        when(mWebContents.getViewAndroidDelegate()).thenReturn(mViewAndroidDelegate);
        when(mWebContents.getContext()).thenReturn(mContext);
        when(mWebContents.getTopLevelNativeWindow()).thenReturn(mWindowAndroid);
        when(mGestureStateListenerManager.isScrollInProgress()).thenReturn(false);
        when(mWindowAndroid.getContext()).thenReturn(mWeakContext);

        mController = SelectionPopupControllerImpl.createForTesting(mWebContents, mPopupController);
        when(mController.getGestureListenerManager()).thenReturn(mGestureStateListenerManager);
    }

    @Test
    @Feature({"TextInput", "SmartSelection"})
    public void testSmartSelectionAdjustSelectionRange() {
        InOrder order = inOrder(mWebContents, mView);
        SelectionClient.Result result = resultForAmphitheatre();

        // Setup SelectionClient for SelectionPopupController.
        TestSelectionClient client = new TestSelectionClient();
        client.setResult(result);
        client.setResultCallback(mController.getResultCallback());
        mController.setSelectionClient(client);

        // Long press triggered showSelectionMenu() call.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE,
                /* selectionOffset= */ 5,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_LONG_PRESS,
                mRenderFrameHost);

        // adjustSelectionByCharacterOffset() should be called.
        order.verify(mWebContents)
                .adjustSelectionByCharacterOffset(result.startAdjust, result.endAdjust, true);
        assertFalse(mController.isActionModeValid());

        when(mView.startActionMode(any(), anyInt())).thenReturn(mActionMode);

        // Call showSelectionMenu again, which is adjustSelectionByCharacterOffset triggered.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_ADJUST_SELECTION,
                mRenderFrameHost);

        order.verify(mView).startActionMode(isNull(), eq(ActionMode.TYPE_FLOATING));

        SelectionClient.Result returnResult = mController.getClassificationResult();
        assertEquals(-5, returnResult.startAdjust);
        assertEquals(8, returnResult.endAdjust);
        assertEquals("Maps", returnResult.label);

        assertTrue(mController.isActionModeValid());
    }

    @Test
    @Feature({"TextInput", "SmartSelection"})
    public void testSmartSelectionAnotherLongPressAfterAdjustment() {
        InOrder order = inOrder(mWebContents, mView);
        SelectionClient.Result result = resultForAmphitheatre();
        SelectionClient.Result newResult = resultForMountain();

        // Set SelectionClient for SelectionPopupController.
        TestSelectionClient client = new TestSelectionClient();
        client.setResult(result);
        client.setResultCallback(mController.getResultCallback());
        mController.setSelectionClient(client);

        // Long press triggered showSelectionMenu() call.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE,
                /* selectionOffset= */ 5,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_LONG_PRESS,
                mRenderFrameHost);

        // adjustSelectionByCharacterOffset() should be called.
        order.verify(mWebContents)
                .adjustSelectionByCharacterOffset(result.startAdjust, result.endAdjust, true);
        assertFalse(mController.isActionModeValid());

        // Another long press triggered showSelectionMenu() call.
        client.setResult(newResult);
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                MOUNTAIN,
                /* selectionOffset= */ 21,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_LONG_PRESS,
                mRenderFrameHost);
        order.verify(mWebContents)
                .adjustSelectionByCharacterOffset(newResult.startAdjust, newResult.endAdjust, true);
        assertFalse(mController.isActionModeValid());

        when(mView.startActionMode(any(), anyInt())).thenReturn(mActionMode);

        // First adjustSelectionByCharacterOffset() triggered.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_ADJUST_SELECTION,
                mRenderFrameHost);

        SelectionClient.Result returnResult = mController.getClassificationResult();
        assertEquals(-21, returnResult.startAdjust);
        assertEquals(15, returnResult.endAdjust);
        assertEquals("Maps", returnResult.label);

        // Second adjustSelectionByCharacterOffset() triggered.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                MOUNTAIN_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_ADJUST_SELECTION,
                mRenderFrameHost);

        order.verify(mView).startActionMode(isNull(), eq(ActionMode.TYPE_FLOATING));
        assertTrue(mController.isActionModeValid());
    }

    @Test
    @Feature({"TextInput", "SmartSelection"})
    public void testSmartSelectionAnotherLongPressBeforeAdjustment() {
        InOrder order = inOrder(mWebContents, mView);
        SelectionClient.Result result = resultForAmphitheatre();
        SelectionClient.Result newResult = resultForMountain();

        // This client won't call SmartSelectionCallback.
        TestSelectionClient client = new SelectionClientOnlyReturnTrue();
        mController.setSelectionClient(client);

        // Long press triggered showSelectionMenu() call.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE,
                /* selectionOffset= */ 5,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_LONG_PRESS,
                mRenderFrameHost);

        // Another long press triggered showSelectionMenu() call.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                MOUNTAIN,
                /* selectionOffset= */ 21,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_LONG_PRESS,
                mRenderFrameHost);

        // Then we are done with the first classification.
        mController.getResultCallback().onClassified(result);

        // Followed by the second classifaction.
        mController.getResultCallback().onClassified(newResult);

        // adjustSelectionByCharacterOffset() should be called.
        order.verify(mWebContents)
                .adjustSelectionByCharacterOffset(result.startAdjust, result.endAdjust, true);
        order.verify(mWebContents)
                .adjustSelectionByCharacterOffset(newResult.startAdjust, newResult.endAdjust, true);
        assertFalse(mController.isActionModeValid());

        when(mView.startActionMode(any(), anyInt())).thenReturn(mActionMode);

        // First adjustSelectionByCharacterOffset() triggered.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_ADJUST_SELECTION,
                mRenderFrameHost);

        SelectionClient.Result returnResult = mController.getClassificationResult();
        assertEquals(-21, returnResult.startAdjust);
        assertEquals(15, returnResult.endAdjust);
        assertEquals("Maps", returnResult.label);

        // Second adjustSelectionByCharacterOffset() triggered.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                MOUNTAIN_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_ADJUST_SELECTION,
                mRenderFrameHost);

        order.verify(mView).startActionMode(isNull(), eq(ActionMode.TYPE_FLOATING));
        assertTrue(mController.isActionModeValid());
    }

    @Test
    @Feature({"TextInput", "SmartSelection"})
    public void testSmartSelectionLoggingExpansion() {
        InOrder order = inOrder(mLogger);
        SelectionClient.Result result = resultForAmphitheatre();

        // Setup SelectionClient for SelectionPopupController.
        TestSelectionClient client = new SelectionClientOnlyReturnTrue();
        client.setLogger(mLogger);
        client.setResult(result);
        client.setResultCallback(mController.getResultCallback());
        mController.setSelectionClient(client);

        // Long press triggered showSelectionMenu() call.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE,
                /* selectionOffset= */ 5,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_LONG_PRESS,
                mRenderFrameHost);

        when(mView.startActionMode(any(), anyInt())).thenReturn(mActionMode);

        order.verify(mLogger).onSelectionStarted(AMPHITHEATRE, 5, true);

        mController.getResultCallback().onClassified(result);

        // Call showSelectionMenu again, which is adjustSelectionByCharacterOffset triggered.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_ADJUST_SELECTION,
                mRenderFrameHost);

        order.verify(mLogger)
                .onSelectionModified(
                        eq(AMPHITHEATRE_FULL), eq(0), isA(SelectionClient.Result.class));

        // Dragging selection handle, select "1600 Amphitheatre".
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL.substring(0, 17),
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_TOUCH_HANDLE,
                mRenderFrameHost);

        order.verify(mLogger, never())
                .onSelectionModified(anyString(), anyInt(), any(SelectionClient.Result.class));

        mController.getResultCallback().onClassified(resultForNoChange());

        order.verify(mLogger)
                .onSelectionModified(
                        eq("1600 Amphitheatre"), eq(0), isA(SelectionClient.Result.class));
    }

    @Test
    @Feature({"TextInput", "SmartSelection"})
    public void testSmartSelectionLoggingNoExpansion() {
        InOrder order = inOrder(mLogger);
        SelectionClient.Result result = resultForNoChange();

        // Setup SelectionClient for SelectionPopupController.
        TestSelectionClient client = new SelectionClientOnlyReturnTrue();
        client.setLogger(mLogger);
        client.setResult(result);
        client.setResultCallback(mController.getResultCallback());
        mController.setSelectionClient(client);

        // Long press triggered showSelectionMenu() call.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE,
                /* selectionOffset= */ 5,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_LONG_PRESS,
                mRenderFrameHost);

        when(mView.startActionMode(any(ActionMode.Callback2.class), anyInt()))
                .thenReturn(mActionMode);
        order.verify(mLogger).onSelectionStarted(AMPHITHEATRE, 5, true);

        // No expansion.
        mController.getResultCallback().onClassified(result);
        order.verify(mLogger)
                .onSelectionModified(eq(AMPHITHEATRE), eq(5), any(SelectionClient.Result.class));

        // Dragging selection handle, select "1600 Amphitheatre".
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL.substring(0, 17),
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_TOUCH_HANDLE,
                mRenderFrameHost);

        order.verify(mLogger, never())
                .onSelectionModified(anyString(), anyInt(), any(SelectionClient.Result.class));
        mController.getResultCallback().onClassified(resultForNoChange());
        order.verify(mLogger)
                .onSelectionModified(
                        eq("1600 Amphitheatre"), eq(0), isA(SelectionClient.Result.class));
    }

    @Test
    @Config(sdk = Build.VERSION_CODES.O)
    @Feature({"TextInput", "SmartSelection"})
    public void testBlockSelectionClientWhenUnprovisioned() {
        // Device is not provisioned.
        Settings.Global.putInt(mContentResolver, Settings.Global.DEVICE_PROVISIONED, 0);

        assertNull(
                SmartSelectionClient.fromWebContents(
                        mController.getResultCallback(), mWebContents));
    }

    @Test
    @Config(sdk = Build.VERSION_CODES.O)
    @Feature({"TextInput", "SmartSelection"})
    public void testBlockSelectionClientWhenIncognito() {
        // Incognito.
        when(mWebContents.isIncognito()).thenReturn(true);

        assertNull(
                SmartSelectionClient.fromWebContents(
                        mController.getResultCallback(), mWebContents));
    }

    @Test
    @Feature({"TextInput", "Magnifier"})
    public void testHandleObserverSelectionHandle() {
        MagnifierAnimator magnifierAnimator = Mockito.mock(MagnifierAnimator.class);
        InOrder order = inOrder(magnifierAnimator);
        mController.setMagnifierAnimator(magnifierAnimator);

        // Selection handles shown.
        mController.onSelectionEvent(SelectionEventType.SELECTION_HANDLES_SHOWN, 0, 0, 1, 1);

        // Selection handles drag started.
        mController.onDragUpdate(TouchSelectionDraggableType.TOUCH_HANDLE, 0.f, 0.f);
        order.verify(magnifierAnimator).handleDragStartedOrMoved(0.f, 0.f);

        // Moving.
        mController.onDragUpdate(TouchSelectionDraggableType.TOUCH_HANDLE, 5.f, 5.f);
        order.verify(magnifierAnimator).handleDragStartedOrMoved(5.f, 5.f);

        // Selection handle drag stopped.
        mController.onSelectionEvent(SelectionEventType.SELECTION_HANDLE_DRAG_STOPPED, 0, 0, 1, 1);
        order.verify(magnifierAnimator).handleDragStopped();
    }

    @Test
    @Feature({"TextInput", "Magnifier"})
    public void testHandleObserverInsertionHandle() {
        MagnifierAnimator magnifierAnimator = Mockito.mock(MagnifierAnimator.class);
        InOrder order = inOrder(magnifierAnimator);
        mController.setMagnifierAnimator(magnifierAnimator);

        // Insertion handle shown.
        mController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_SHOWN, 0, 0, 1, 1);

        // Insertion handle drag started.
        mController.onDragUpdate(TouchSelectionDraggableType.TOUCH_HANDLE, 0.f, 0.f);
        order.verify(magnifierAnimator).handleDragStartedOrMoved(0.f, 0.f);

        // Moving.
        mController.onDragUpdate(TouchSelectionDraggableType.TOUCH_HANDLE, 5.f, 5.f);
        order.verify(magnifierAnimator).handleDragStartedOrMoved(5.f, 5.f);

        // Insertion handle drag stopped.
        mController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_DRAG_STOPPED, 0, 0, 1, 1);
        order.verify(magnifierAnimator).handleDragStopped();
    }

    @Test
    @Feature({"TextInput", "HandleHapticFeedback"})
    public void testInsertionHandleHapticFeedback() {
        SelectionPopupControllerImpl spyController = Mockito.spy(mController);
        spyController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_MOVED, 0, 0, 1, 1);
        // Any INSERTION_HANDLE_MOVED before INSERTION_HANDLE_DRAG_STARTED should not trigger haptic
        // feedback.
        Mockito.verify(spyController, never()).performHapticFeedback();

        spyController.onSelectionEvent(
                SelectionEventType.INSERTION_HANDLE_DRAG_STARTED, 0, 0, 1, 1);
        spyController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_MOVED, 0, 0, 1, 1);
        spyController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_MOVED, 0, 0, 1, 1);
        spyController.onSelectionEvent(
                SelectionEventType.INSERTION_HANDLE_DRAG_STOPPED, 0, 0, 1, 1);

        // We called twice.
        Mockito.verify(spyController, times(2)).performHapticFeedback();

        spyController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_MOVED, 0, 0, 1, 1);
        // Any INSERTION_HANDLE_MOVED after INSERTION_HANDLE_DRAG_STOPPED should not trigger more
        // haptic feedback.
        Mockito.verify(spyController, times(2)).performHapticFeedback();
    }

    @Test
    @Feature({"TextInput", "HandleHapticFeedback"})
    public void testSelectionHandleHapticFeedback() {
        SelectionPopupControllerImpl spyController = Mockito.spy(mController);
        spyController.onSelectionEvent(SelectionEventType.SELECTION_HANDLES_MOVED, 0, 0, 1, 1);
        // Any SELECTION_HANDLES_MOVED before SELECTION_HANDLE_DRAG_STARTED should not trigger
        // haptic feedback.
        Mockito.verify(spyController, never()).performHapticFeedback();

        spyController.onSelectionEvent(
                SelectionEventType.SELECTION_HANDLE_DRAG_STARTED, 0, 0, 1, 1);
        spyController.onSelectionEvent(SelectionEventType.SELECTION_HANDLES_MOVED, 0, 0, 1, 1);
        spyController.onSelectionEvent(SelectionEventType.SELECTION_HANDLES_MOVED, 0, 0, 1, 1);
        spyController.onSelectionEvent(
                SelectionEventType.SELECTION_HANDLE_DRAG_STOPPED, 0, 0, 1, 1);

        // We called twice.
        Mockito.verify(spyController, times(2)).performHapticFeedback();

        spyController.onSelectionEvent(SelectionEventType.SELECTION_HANDLES_MOVED, 0, 0, 1, 1);
        // Any SELECTION_HANDLES_MOVED after SELECTION_HANDLE_DRAG_STOPPED should not trigger more
        // haptic feedback.
        Mockito.verify(spyController, times(2)).performHapticFeedback();
    }

    @Test
    @Feature({"TextInput"})
    public void testSelectionWhenUnselectAndFocusedNodeChanged() {
        SelectionPopupControllerImpl spyController = Mockito.spy(mController);

        when(mView.startActionMode(any(), anyInt())).thenReturn(mActionMode);

        // Long press triggered showSelectionMenu() call.
        spyController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_LONG_PRESS,
                mRenderFrameHost);

        Mockito.verify(mView).startActionMode(isNull(), eq(ActionMode.TYPE_FLOATING));
        // showSelectionMenu() will invoke the first call to finishActionMode() in the
        // showActionModeOrClearOnFailure().
        Mockito.verify(spyController, times(1)).finishActionMode();
        assertTrue(spyController.isSelectActionBarShowing());
        assertTrue(spyController.isSelectActionBarShowingSupplier().get());

        // Clear the selected text.
        spyController.onSelectionChanged("");
        // Changed the focused node attribute to non-editable and non-password.
        spyController.updateSelectionState(false, false);

        // finishActionMode will be called twice for unselect.
        Mockito.verify(spyController, times(2)).finishActionMode();

        // SELECTION_HANDLES_CLEARED happens.
        spyController.onSelectionEvent(SelectionEventType.SELECTION_HANDLES_CLEARED, 0, 0, 1, 1);

        assertFalse(spyController.isSelectActionBarShowing());
        assertFalse(spyController.isSelectActionBarShowingSupplier().get());
        Mockito.verify(spyController, times(3)).finishActionMode();
    }

    @Test
    @Feature({"TextInput"})
    public void testSelectionWhenWindowIsNull() {
        SelectionPopupControllerImpl spyController = Mockito.spy(mController);

        when(mView.startActionMode(any(), anyInt())).thenReturn(mActionMode);

        // Long press triggered showSelectionMenu() call.
        spyController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_LONG_PRESS,
                mRenderFrameHost);

        Mockito.verify(mView).startActionMode(isNull(), eq(ActionMode.TYPE_FLOATING));
        // showSelectionMenu() will invoke the first call to finishActionMode() in the
        // showActionModeOrClearOnFailure().
        Mockito.verify(spyController, times(1)).finishActionMode();
        assertTrue(spyController.isSelectActionBarShowing());
        assertTrue(spyController.isSelectActionBarShowingSupplier().get());

        // Setting the window to null should clear selections and reset the state.
        spyController.onWindowAndroidChanged(null);
        // Changed the focused node attribute to non-editable and non-password.
        spyController.updateSelectionState(false, false);

        // finishActionMode will be called twice for unselect.
        Mockito.verify(spyController, times(2)).finishActionMode();

        // SELECTION_HANDLES_CLEARED happens.
        spyController.onSelectionEvent(SelectionEventType.SELECTION_HANDLES_CLEARED, 0, 0, 1, 1);

        assertFalse(spyController.isSelectActionBarShowing());
        assertFalse(spyController.isSelectActionBarShowingSupplier().get());
        Mockito.verify(spyController, times(3)).finishActionMode();
    }

    @Test
    @Config(sdk = Build.VERSION_CODES.O)
    @Feature({"TextInput"})
    public void testProcessTextMenuItemWithActivityInfo() {
        // TODO(ctzsm): Consider a better way to distinguish app context and |mContext|.
        ContextUtils.initApplicationContextForTests(mContext);
        SelectionPopupControllerImpl spyController = Mockito.spy(mController);

        // test activityInfo exported=false
        List<ResolveInfo> list1 = new ArrayList();
        ResolveInfo resolveInfo1 = createResolveInfoWithActivityInfo("ProcessTextActivity1", false);
        list1.add(resolveInfo1);
        when(mPackageManager.queryIntentActivities(any(Intent.class), anyInt())).thenReturn(list1);

        Menu menu1 = new RoboMenu();
        assertEquals(0, menu1.size());
        spyController.initializeTextProcessingMenuForTesting(mActionMode, menu1);
        assertEquals(0, menu1.size());

        // test activityInfo exported=true
        List<ResolveInfo> list2 = new ArrayList();
        ResolveInfo resolveInfo2 = createResolveInfoWithActivityInfo("ProcessTextActivity2", true);
        list2.add(resolveInfo2);
        when(mPackageManager.queryIntentActivities(any(Intent.class), anyInt())).thenReturn(list2);

        Menu menu2 = new RoboMenu();
        assertEquals(0, menu2.size());
        spyController.initializeTextProcessingMenuForTesting(mActionMode, menu2);
        assertEquals(1, menu2.size());

        // test null activityInfo
        List<ResolveInfo> list3 = new ArrayList();
        ResolveInfo resolveInfo3 = new ResolveInfo();
        resolveInfo3.activityInfo = null;
        list3.add(resolveInfo3);
        when(mPackageManager.queryIntentActivities(any(Intent.class), anyInt())).thenReturn(list3);

        Menu menu3 = new RoboMenu();
        assertEquals(0, menu3.size());
        spyController.initializeTextProcessingMenuForTesting(mActionMode, menu3);
        assertEquals(0, menu3.size());
    }

    @Test
    @Feature({"TextInput"})
    public void testShowDropdownMenuWhenMenuSourceIsMouse() {
        setDropdownMenuFeatureEnabled(true);
        SelectionPopupControllerImpl spyController = Mockito.spy(mController);
        SelectionDropdownMenuDelegate dropdownMenuDelegate =
                Mockito.spy(new TestSelectionDropdownMenuDelegate());
        spyController.setDropdownMenuDelegate(dropdownMenuDelegate);
        spyController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_MOUSE,
                mRenderFrameHost);
        Mockito.verify(spyController, times(1)).createAndShowDropdownMenu();
        Mockito.verify(spyController, times(1)).destroyActionModeAndKeepSelection();
        Mockito.verify(dropdownMenuDelegate, times(1)).dismiss();
        Mockito.verify(dropdownMenuDelegate, times(1))
                .show(any(), any(), any(), any(), anyInt(), anyInt());
        Mockito.verify(spyController, never()).showActionModeOrClearOnFailure();
    }

    @Test
    @Feature({"TextInput"})
    public void testShowPasteMenuWhenSourceIsLongPressWithNoSelection() {
        setDropdownMenuFeatureEnabled(true);
        SelectionPopupControllerImpl spyController = Mockito.spy(mController);
        SelectionDropdownMenuDelegate dropdownMenuDelegate =
                Mockito.spy(new TestSelectionDropdownMenuDelegate());
        spyController.setDropdownMenuDelegate(dropdownMenuDelegate);
        spyController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                "",
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_LONG_PRESS,
                mRenderFrameHost);
        Mockito.verify(spyController, times(1)).showActionModeOrClearOnFailure();
        Mockito.verify(dropdownMenuDelegate, times(1)).dismiss();
        Mockito.verify(spyController, never()).createAndShowDropdownMenu();
    }

    @Test
    @Feature({"TextInput"})
    public void testShowSelectionActionMenuWhenSourceIsLongPressWithSelection() {
        setDropdownMenuFeatureEnabled(true);
        SelectionPopupControllerImpl spyController = Mockito.spy(mController);
        SelectionDropdownMenuDelegate dropdownMenuDelegate =
                Mockito.spy(new TestSelectionDropdownMenuDelegate());
        spyController.setDropdownMenuDelegate(dropdownMenuDelegate);
        spyController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_LONG_PRESS,
                mRenderFrameHost);
        Mockito.verify(spyController, times(1)).showActionModeOrClearOnFailure();
        Mockito.verify(dropdownMenuDelegate, times(1)).dismiss();
        Mockito.verify(spyController, never()).createAndShowDropdownMenu();
    }

    @Test
    public void testMenuIsCachedForSameSelectionState() {
        Assert.assertNull(mController.getSelectionMenuCachedResultForTesting());

        // Called twice to check the selection menu has been cached properly.
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_MOUSE,
                mRenderFrameHost);

        SortedSet<SelectionMenuGroup> result = mController.getMenuItems();
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_MOUSE,
                mRenderFrameHost);

        Assert.assertNotNull(mController.getSelectionMenuCachedResultForTesting());
        Assert.assertSame(result, mController.getMenuItems());
    }

    @Test
    public void testNewMenuIsProcessedForDifferentSelectionState() {
        Assert.assertNull(mController.getSelectionMenuCachedResultForTesting());

        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ true,
                /* isPasswordType= */ false,
                AMPHITHEATRE_FULL,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_MOUSE,
                mRenderFrameHost);

        SortedSet<SelectionMenuGroup> result = mController.getMenuItems();
        mController.showSelectionMenu(
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                /* isEditable= */ false,
                /* isPasswordType= */ true,
                AMPHITHEATRE,
                /* selectionOffset= */ 0,
                /* canSelectAll= */ true,
                /* canRichlyEdit= */ true,
                /* shouldSuggest= */ true,
                MenuSourceType.MENU_SOURCE_MOUSE,
                mRenderFrameHost);

        // Check the menu is different and not similar to the one we have stored.
        Assert.assertNotNull(mController.getSelectionMenuCachedResultForTesting());
        Assert.assertNotSame(result, mController.getMenuItems());
        Assert.assertNotSame(
                mController.getSelectionMenuCachedResultForTesting(), mController.getMenuItems());
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testNotSetExclusionRectsOnSelectionHandlesShownEvent() {
        mController.onSelectionEvent(SelectionEventType.SELECTION_HANDLES_SHOWN, 0, 0, 0, 0);
        Mockito.verify(mView, never()).setSystemGestureExclusionRects(anyList());
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testNotSetExclusionRectsOnSelectionHandlesMovedEvent() {
        mController.onSelectionEvent(SelectionEventType.SELECTION_HANDLES_MOVED, 0, 0, 0, 0);
        Mockito.verify(mView, never()).setSystemGestureExclusionRects(anyList());
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testSetExclusionRectsOnSelectionHandlesClearedEvent() {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 29);
        mController.onSelectionEvent(SelectionEventType.SELECTION_HANDLES_CLEARED, 0, 0, 0, 0);
        Mockito.verify(mView, times(1))
                .setSystemGestureExclusionRects(List.of(new Rect(0, 0, 0, 0)));
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testNotSetExclusionRectsOnSelectionHandlesDragStartedEvent() {
        mController.onSelectionEvent(SelectionEventType.SELECTION_HANDLE_DRAG_STARTED, 0, 0, 0, 0);
        Mockito.verify(mView, never()).setSystemGestureExclusionRects(anyList());
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testNotSetExclusionRectsOnInsertionHandlesShownEvent() {
        mController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_SHOWN, 0, 0, 0, 0);
        Mockito.verify(mView, never()).setSystemGestureExclusionRects(anyList());
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testNotSetExclusionRectsOnInsertionHandlesMovedEvent() {
        mController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_MOVED, 0, 0, 0, 0);
        Mockito.verify(mView, never()).setSystemGestureExclusionRects(anyList());
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testNotSetExclusionRectsOnInsertionHandleTappedEvent() {
        mController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_TAPPED, 0, 0, 0, 0);
        Mockito.verify(mView, never()).setSystemGestureExclusionRects(anyList());
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testNotSetExclusionRectsOnInsertionHandleClearedEvent() {
        mController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_CLEARED, 0, 0, 0, 0);
        Mockito.verify(mView, never()).setSystemGestureExclusionRects(anyList());
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testNotSetExclusionRectsOnInsertionHandleDragStartedEvent() {
        mController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_DRAG_STARTED, 0, 0, 0, 0);
        Mockito.verify(mView, never()).setSystemGestureExclusionRects(anyList());
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testNotSetExclusionRectsOnInsertionHandleDragStoppedEvent() {
        mController.onSelectionEvent(SelectionEventType.INSERTION_HANDLE_DRAG_STOPPED, 0, 0, 0, 0);
        Mockito.verify(mView, never()).setSystemGestureExclusionRects(anyList());
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testSetExclusionRectsOnSelectionHandleDragStopped() {
        SelectionPopupControllerImpl mockController =
                Mockito.spy(
                        SelectionPopupControllerImpl.createForTesting(
                                mWebContents, mPopupController));
        when(mWebContents.getRenderCoordinates()).thenReturn(mRenderCoordinates);
        when(mRenderCoordinates.getDeviceScaleFactor()).thenReturn(1.0f);
        when(mRenderCoordinates.getContentOffsetYPix()).thenReturn(0.0f);
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 29);
        Object[] handleRects = new Rect[2];
        handleRects[0] = new Rect(0, 0, 10, 10);
        handleRects[1] = new Rect(10, 10, 20, 20);
        List<Rect> rects = new ArrayList<>();
        rects.add((Rect) handleRects[0]);
        rects.add((Rect) handleRects[1]);
        when(mockController.getTouchHandleRects()).thenReturn(handleRects);
        mockController.onSelectionEvent(
                SelectionEventType.SELECTION_HANDLE_DRAG_STOPPED, 0, 0, 0, 0);
        Mockito.verify(mView, times(1)).setSystemGestureExclusionRects(rects);
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testNotSetExclusionRectsBelowAndroidQ() {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28);
        mController.onSelectionEvent(SelectionEventType.SELECTION_HANDLE_DRAG_STOPPED, 0, 0, 0, 0);
        Mockito.verify(mView, never()).setSystemGestureExclusionRects(anyList());
    }

    @Test
    @Feature({"TextInput"})
    @Config(minSdk = Build.VERSION_CODES.Q)
    public void testNotSetExclusionRectsWithNullView() {
        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 29);
        when(mWebContents.getViewAndroidDelegate()).thenReturn(null);
        mController.onSelectionEvent(SelectionEventType.SELECTION_HANDLE_DRAG_STOPPED, 0, 0, 0, 0);
        Mockito.verify(mView, never()).setSystemGestureExclusionRects(anyList());
    }

    private void setDropdownMenuFeatureEnabled(boolean enabled) {
        mTestValues.addFeatureFlagOverride(
                ContentFeatureList.MOUSE_AND_TRACKPAD_DROPDOWN_MENU, enabled);
        SelectionPopupControllerImpl.setEnableTabletUiModeForTesting(enabled);
    }

    private ResolveInfo createResolveInfoWithActivityInfo(String activityName, boolean exported) {
        String packageName = "org.chromium.content.browser.selection.SelectionPopupControllerTest";

        ActivityInfo activityInfo = new ActivityInfo();
        activityInfo.packageName = packageName;
        activityInfo.name = activityName;
        activityInfo.exported = exported;
        activityInfo.applicationInfo = new ApplicationInfo();
        activityInfo.applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM;

        ResolveInfo resolveInfo =
                new ResolveInfo() {
                    @Override
                    public CharSequence loadLabel(PackageManager pm) {
                        return "TEST_LABEL";
                    }
                };
        resolveInfo.activityInfo = activityInfo;
        return resolveInfo;
    }

    // Result generated by long press "Amphitheatre" in "1600 Amphitheatre Parkway".
    private SelectionClient.Result resultForAmphitheatre() {
        SelectionClient.Result result = new SelectionClient.Result();
        result.startAdjust = -5;
        result.endAdjust = 8;
        result.label = "Maps";
        return result;
    }

    // Result generated by long press "Mountain" in "585 Franklin Street, Mountain View, CA 94041".
    private SelectionClient.Result resultForMountain() {
        SelectionClient.Result result = new SelectionClient.Result();
        result.startAdjust = -21;
        result.endAdjust = 15;
        result.label = "Maps";
        return result;
    }

    private SelectionClient.Result resultForNoChange() {
        SelectionClient.Result result = new SelectionClient.Result();
        result.startAdjust = 0;
        result.endAdjust = 0;
        result.label = "Maps";
        return result;
    }
}