| Index: chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInternalStateController.java
|
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInternalStateController.java b/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInternalStateController.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..c598fa1ac28b2593a9f65883b24daa13003f9bd6
|
| --- /dev/null
|
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchInternalStateController.java
|
| @@ -0,0 +1,327 @@
|
| +// Copyright 2017 The Chromium Authors. All rights reserved.
|
| +// 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.contextualsearch;
|
| +
|
| +import org.chromium.base.Log;
|
| +import org.chromium.base.VisibleForTesting;
|
| +import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.StateChangeReason;
|
| +
|
| +import javax.annotation.Nullable;
|
| +
|
| +/**
|
| + * Controls the internal state of the Contextual Search Manager.
|
| + * <p>
|
| + * This class keeps track of the current internal state of the {@code ContextualSearchManager} and
|
| + * helps it to transition between states and return to the idle state when work has been
|
| + * interrupted.
|
| + * <p>
|
| + * Usage: Call {@link #reset(StateChangeReason)} to reset to the {@code IDLE} state, which hides
|
| + * the UI.<br>
|
| + * Call {@link #enter(InternalState)} to enter a start-state (when a user gesture is recognized).
|
| + * When doing some work on a state, which may be done in an asynchronous manner:<ol>
|
| + * <li>call {@link #notifyStartingWorkOn(InternalState)} to note that work is starting on that state
|
| + * <li>call {@link #notifyFinishedWorkOn(InternalState)} when work is completed.
|
| + * <li>If a handler of an async response needs to do additional work, such as updating the UI, it
|
| + * should first call {@link #isStillWorkingOn(InternalState)} to check that work has not been
|
| + * interrupted since the async operation was started.
|
| + * </ol><p>
|
| + * The {@link #notifyFinishedWorkOn(InternalState)} method will automatically start a transition to
|
| + * the appropriate next state.
|
| + * <p>
|
| + * Policy decisions about state transitions should only be done in the private
|
| + * {@link #transitionTo(InternalState)} method of this class (not within the
|
| + * {@code ContextualSearchManager} itself).
|
| + */
|
| +class ContextualSearchInternalStateController {
|
| + private static final String TAG = "ContextualSearch";
|
| +
|
| + private final ContextualSearchPolicy mPolicy;
|
| + private final ContextualSearchInternalStateHandler mStateHandler;
|
| +
|
| + /**
|
| + * The current internal state of the {@code ContextualSearchManager}.
|
| + * States can be "start states" which can be passed to #enter(), or "transitional states" which
|
| + * automatically transition to the appropriate next state when work is done on them, or
|
| + * "resting states" which do not transition into any next state, or a combination of the
|
| + * above.
|
| + */
|
| + public static enum InternalState {
|
| + /** This start state should only be used when the manager is not yet initialized or already
|
| + * destroyed.
|
| + */
|
| + UNDEFINED,
|
| + /** This start/resting state shows no UI (panel is closed). */
|
| + IDLE,
|
| +
|
| + /** This starts a transition that leads to the SHOWING_LONGPRESS_SEARCH resting state. */
|
| + LONG_PRESS_RECOGNIZED,
|
| + /** Resting state when showing the panel in response to a Long-press gesture. */
|
| + SHOWING_LONGPRESS_SEARCH,
|
| +
|
| + /** This is a start state when the selection is cleared typically due to a tap on the base
|
| + * page. If the previous state wasn't IDLE then it could be a tap near a previous Tap.
|
| + * Transitions to WAITING_FOR_POSSIBLE_TAP_NEAR_PREVIOUS to wait for a Tap and hide the Bar
|
| + * if no tap ever happens. */
|
| + SELECTION_CLEARED_RECOGNIZED,
|
| + /** Waits to see if the tap gesture was valid so we can just update the Bar instead of
|
| + * hiding/showing it. */
|
| + WAITING_FOR_POSSIBLE_TAP_NEAR_PREVIOUS,
|
| +
|
| + /** This starts a sequence of states needed to get to the SHOWING_TAP_SEARCH resting state.
|
| + */
|
| + TAP_RECOGNIZED,
|
| + /** Gathers text surrounding the selection. */
|
| + GATHERING_SURROUNDINGS,
|
| + /** Decides if the gesture should trigger the UX or be suppressed. */
|
| + DECIDING_SUPPRESSION,
|
| + /** Start showing the Tap UI. Currently this means select the word that was tapped. */
|
| + START_SHOWING_TAP_UI,
|
| + /** Show the full Tap UI. Currently this means showing the Overlay Panel. */
|
| + SHOW_FULL_TAP_UI,
|
| + /** Resolving the Search Term using the surrounding text and additional context.
|
| + * Currently this makes a server request, which could take a long time. */
|
| + RESOLVING,
|
| + /** Resting state when showing the panel in response to a Tap gesture. */
|
| + SHOWING_TAP_SEARCH
|
| + }
|
| +
|
| + // The current state of this instance.
|
| + private InternalState mState;
|
| +
|
| + // Whether work has started on the current state.
|
| + private boolean mDidStartWork;
|
| +
|
| + // The previous state of this instance.
|
| + private InternalState mPreviousState;
|
| +
|
| + /**
|
| + * Constructs an instance of this class, which has the same lifetime as the
|
| + * {@code ContextualSearchManager} and the given parameters.
|
| + */
|
| + ContextualSearchInternalStateController(
|
| + ContextualSearchPolicy policy, ContextualSearchInternalStateHandler stateHandler) {
|
| + mPolicy = policy;
|
| + mStateHandler = stateHandler;
|
| + }
|
| +
|
| + // ============================================================================================
|
| + // State-transition management.
|
| + // This code is designed to solve several problems:
|
| + // 1) Document the sequencing of handling a gesture in code. Now there's a single method that
|
| + // determines the sequence that should be followed for Tap handling (our most complicated
|
| + // case.
|
| + // 2) Document the initiation and subsequent notification/handling of operations. Now the
|
| + // method that starts an operation and the notification handler are tied together by their
|
| + // references to the same state. This allows a simple search to find the
|
| + // initiation and handler together (which is not always easy, e.g. SelectWordAroundCaret
|
| + // does not yet have an ACK so we infer that it's complete when the selection change -- or
|
| + // does not change after some short waiting period).
|
| + // 3) Gracefully handle sequence interruptions. When an asynchronous operation is in progress
|
| + // the user may start a new sequence or abort the current sequence. Now the handler for an
|
| + // asynchronous operation can easily detect that it's no longer working on that operation
|
| + // and skip the normal completion of the operation.
|
| + // ============================================================================================
|
| +
|
| + /**
|
| + * Reset the current state to the IDLE state.
|
| + * @param reason The reason for the reset.
|
| + */
|
| + void reset(StateChangeReason reason) {
|
| + transitionTo(InternalState.IDLE, reason);
|
| + }
|
| +
|
| + /**
|
| + * Enters the given starting state immediately.
|
| + * @param state The new starting {@link InternalState} we're now in.
|
| + */
|
| + void enter(InternalState state) {
|
| + assert state == InternalState.UNDEFINED || state == InternalState.IDLE
|
| + || state == InternalState.LONG_PRESS_RECOGNIZED
|
| + || state == InternalState.TAP_RECOGNIZED
|
| + || state == InternalState.SELECTION_CLEARED_RECOGNIZED;
|
| + mPreviousState = mState;
|
| + mState = state;
|
| +
|
| + notifyStartingWorkOn(mState);
|
| + notifyFinishedWorkOn(mState);
|
| + }
|
| +
|
| + /**
|
| + * Confirms that work is starting on the given state.
|
| + * @param state The {@link InternalState} that we're now working on.
|
| + */
|
| + void notifyStartingWorkOn(InternalState state) {
|
| + assert mState == state;
|
| + mDidStartWork = true;
|
| + }
|
| +
|
| + /**
|
| + * @return Whether we're still working on the given state.
|
| + */
|
| + boolean isStillWorkingOn(InternalState state) {
|
| + return mState == state;
|
| + }
|
| +
|
| + /**
|
| + * Confirms that work has been finished on the given state.
|
| + * This should be called by every operation that waits for some kind of completion when it
|
| + * completes. The operation's start must be flagged using {@link #notifyStartingWorkOn}.
|
| + * @param state The {@link InternalState} that we've finished working on.
|
| + */
|
| + void notifyFinishedWorkOn(InternalState state) {
|
| + finishWorkingOn(state);
|
| + }
|
| +
|
| + /**
|
| + * @return The current internal state for testing purposes.
|
| + */
|
| + @VisibleForTesting
|
| + protected InternalState getState() {
|
| + return mState;
|
| + }
|
| +
|
| + /**
|
| + * Establishes the given state by calling code that starts work on that state.
|
| + * @param state The new {@link InternalState} to establish.
|
| + */
|
| + private void transitionTo(InternalState state) {
|
| + transitionTo(state, null);
|
| + }
|
| +
|
| + /**
|
| + * Establishes the given state by calling code that starts work on that state or simply
|
| + * displays the appropriate UX for that state.
|
| + * @param state The new {@link InternalState} to establish.
|
| + * @param reason The reason we're starting this state, or {@code null} if not significant
|
| + * or known. Only needed when we enter the IDLE state.
|
| + */
|
| + private void transitionTo(final InternalState state, @Nullable final StateChangeReason reason) {
|
| + if (state == mState) return;
|
| +
|
| + // This should be the only part of the code that changes the state (other than #enter)!
|
| + mPreviousState = mState;
|
| + mState = state;
|
| +
|
| + mDidStartWork = false;
|
| + startWorkingOn(state, reason);
|
| + }
|
| +
|
| + /**
|
| + * Starts working on the given state by calling code that starts work on that state or simply
|
| + * displays the appropriate UX for that state.
|
| + * @param state The new {@link InternalState} to establish.
|
| + * @param reason The reason we're starting this state, or {@code null} if not significant
|
| + * or known. Only needed when we enter the IDLE state.
|
| + */
|
| + private void startWorkingOn(InternalState state, @Nullable StateChangeReason reason) {
|
| + switch (state) {
|
| + case IDLE:
|
| + assert reason != null;
|
| + mStateHandler.hideContextualSearchUi(reason);
|
| + break;
|
| +
|
| + case LONG_PRESS_RECOGNIZED:
|
| + break;
|
| + case SHOWING_LONGPRESS_SEARCH:
|
| + mStateHandler.showContextualSearchLongpressUi();
|
| + break;
|
| +
|
| + case WAITING_FOR_POSSIBLE_TAP_NEAR_PREVIOUS:
|
| + mStateHandler.waitForPossibleTapNearPrevious();
|
| + break;
|
| + case TAP_RECOGNIZED:
|
| + break;
|
| + case GATHERING_SURROUNDINGS:
|
| + mStateHandler.gatherSurroundingText();
|
| + break;
|
| + case DECIDING_SUPPRESSION:
|
| + mStateHandler.decideSuppression();
|
| + break;
|
| + case START_SHOWING_TAP_UI:
|
| + mStateHandler.startShowingTapUi();
|
| + break;
|
| + case SHOW_FULL_TAP_UI:
|
| + mStateHandler.showContextualSearchTapUi();
|
| + break;
|
| + case RESOLVING:
|
| + mStateHandler.resolveSearchTerm();
|
| + break;
|
| + case SHOWING_TAP_SEARCH:
|
| + break;
|
| + default:
|
| + Log.w(TAG, "Warning: unexpected startWorkingOn " + state.toString());
|
| + break;
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Finishes working on the given state by making a transition to the next state if needed.
|
| + * @param state The {@link InternalState} that we've finished working on.
|
| + */
|
| + private void finishWorkingOn(InternalState state) {
|
| + // When an async task finishes work some action may have caused a reset and now we're
|
| + // in a new sequence, so no need to finish work on the abandoned state.
|
| + if (state != mState) return;
|
| +
|
| + // Should have called #nofifyStartingWorkOn this state already.
|
| + assert mDidStartWork;
|
| +
|
| + if (mState == InternalState.IDLE || mState == InternalState.UNDEFINED) {
|
| + Log.w(TAG, "Warning, the " + state.toString() + " state was aborted.");
|
| + return;
|
| + }
|
| +
|
| + switch (state) {
|
| + case LONG_PRESS_RECOGNIZED:
|
| + transitionTo(InternalState.GATHERING_SURROUNDINGS);
|
| + break;
|
| + case SHOWING_LONGPRESS_SEARCH:
|
| + break;
|
| + case SELECTION_CLEARED_RECOGNIZED:
|
| + if (mPreviousState != null && mPreviousState != InternalState.IDLE) {
|
| + transitionTo(InternalState.WAITING_FOR_POSSIBLE_TAP_NEAR_PREVIOUS);
|
| + } else {
|
| + reset(StateChangeReason.BASE_PAGE_TAP);
|
| + }
|
| + break;
|
| + case WAITING_FOR_POSSIBLE_TAP_NEAR_PREVIOUS:
|
| + // If a tap near the previous was detected we've started another sequence and won't
|
| + // get here. So we know the wait completed without any other action so we need to
|
| + // reset the UX.
|
| + reset(StateChangeReason.BASE_PAGE_TAP);
|
| + break;
|
| + case TAP_RECOGNIZED:
|
| + transitionTo(InternalState.GATHERING_SURROUNDINGS);
|
| + break;
|
| + case GATHERING_SURROUNDINGS:
|
| + // We gather surroundings for both Tap and Long-press in order to notify icing.
|
| + if (mPreviousState == InternalState.LONG_PRESS_RECOGNIZED) {
|
| + transitionTo(InternalState.SHOWING_LONGPRESS_SEARCH);
|
| + } else {
|
| + transitionTo(InternalState.DECIDING_SUPPRESSION);
|
| + }
|
| + break;
|
| + case DECIDING_SUPPRESSION:
|
| + transitionTo(InternalState.START_SHOWING_TAP_UI);
|
| + break;
|
| + case START_SHOWING_TAP_UI:
|
| + transitionTo(InternalState.SHOW_FULL_TAP_UI);
|
| + break;
|
| + case SHOW_FULL_TAP_UI:
|
| + if (mPolicy.shouldPreviousTapResolve()) {
|
| + transitionTo(InternalState.RESOLVING);
|
| + } else {
|
| + transitionTo(InternalState.SHOWING_TAP_SEARCH);
|
| + }
|
| + break;
|
| + case RESOLVING:
|
| + transitionTo(InternalState.SHOWING_TAP_SEARCH);
|
| + break;
|
| + default:
|
| + Log.e(TAG, "The state " + state.toString() + " is not transitional!");
|
| + assert false;
|
| + }
|
| + }
|
| +}
|
|
|