Chromium Code Reviews| Index: chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchStateController.java |
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchStateController.java b/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchStateController.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..b14ea48e8fbfa7e083f250a17284b1e2d41ee010 |
| --- /dev/null |
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchStateController.java |
| @@ -0,0 +1,276 @@ |
| +// 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 android.util.Log; |
| + |
| +import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.StateChangeReason; |
| +import org.chromium.chrome.browser.contextualsearch.ContextualSearchSelectionController.SelectionType; |
| + |
| +import javax.annotation.Nullable; |
| + |
| +/** |
| + * Controls the State of the Contextual Search Manager. |
| + * <p> |
| + * This class keeps track of the current state of the {@code ContextualSearchManager} and helps it |
| + * to transition between states return to the idle state when work has been interrupted. |
| + * <p> |
| + * Usage: Call {@link #reset(StateChangeReason)} to reset to the {@code IDLE} state.<br> |
| + * Call {@link #enter(State)} to enter a start-state (when a gesture is recognized). |
| + * When doing some work in an asynchronous manner:<ol> |
| + * <li>call {@link #notifyStartingWorkOn(State)} to note that work is starting on that state |
| + * <li>call {@link #notifyFinishedWorkOn(State)} when work is completed. |
| + * <li>If a handler needs to do additional work, such as updating the UX, it should first call |
| + * {@link #isStillWorkingOn(State)} to check that work has not been interrupted. |
| + * </ol><p> |
| + * The {@link #notifyFinishedWorkOn(State)} method will automatically start a transition to the |
| + * next state. |
| + * <p> |
| + * Policy decisions about state transitions should only be done in the private |
| + * {@link #transitionTo(State)} method of this class, not in the {@code ContextualSearchManager}. |
| + */ |
| +class ContextualSearchStateController { |
| + private static final String TAG = "Contextual Search"; |
| + |
| + private final ContextualSearchSelectionController mSelectionController; |
| + private final ContextualSearchPolicy mPolicy; |
| + private final ContextualSearchStateControlled mControlled; |
|
Theresa
2017/03/28 15:52:55
I'd prefer a different name for this class. Maybe
Donn Denman
2017/03/29 18:56:51
Changed to ContextualSearchInternalStateHandler.
|
| + |
| + /** The current state of the manager. */ |
| + public static enum State { |
| + // When not yet initialized or already destroyed only |
| + UNDEFINED, |
| + // This is the default resting state. |
| + IDLE, |
|
Theresa
2017/03/28 15:52:55
Is this equivalent to the panel "closed" state (as
Donn Denman
2017/03/29 18:56:51
Yes, updated the comment to reflect that the IDLE
|
| + |
| + // This is a sequence of transition states needed to get to the SHOWING_TAP resting state. |
|
Theresa
2017/03/28 15:52:55
s/SHOWING_STATE/SHOWING_LONGPRESS_SEARCH?
Donn Denman
2017/03/29 18:56:51
Done.
|
| + LONG_PRESS_RECOGNIZED, |
| + // Transitional state when extending the selection. |
| + EXTENDING_SELECTION, // needed??? |
|
Theresa
2017/03/28 15:52:55
Would this be used when the user is dragging the s
Donn Denman
2017/03/29 18:56:51
Yep. Still not sure it's needed, but we're suppos
|
| + // Resting state when showing the panel in response to a Long-press gesture. |
| + SHOWING_LONGPRESS_SEARCH, |
| + |
| + // This is a sequence of transition states needed to get to the SHOWING_TAP resting state. |
| + TAP_RECOGNIZED, |
| + // May be done for Tap or Long-press. |
| + GATHERING_SURROUNDINGS, |
| + DECIDING_SUPPRESSION, |
|
Theresa
2017/03/28 15:52:55
I expected tap suppression be synchronous (for now
Donn Denman
2017/03/29 18:56:51
This CL adds both sequencing and handling of async
|
| + WAITING_FOR_POSSIBLE_NAVIGATION, |
| + SELECTING_WORD, |
| + // Resolving the Search Term using the surrounding text and additional context. |
| + RESOLVING, |
| + // Resting state when showing the panel in response to a Tap gesture. |
| + SHOWING_TAP_SEARCH |
| + } |
| + |
| + // The current state of this instance. |
| + private State mState; |
| + |
| + // Whether work has started on the current state. |
| + private boolean mDidStartWork; |
| + |
| + /** |
| + * Constructs an instance of this class, which has the same lifetime as the |
| + * {@code ContextualSearchManager} and the given parameters. |
| + */ |
| + ContextualSearchStateController(ContextualSearchSelectionController selectionController, |
| + ContextualSearchPolicy policy, ContextualSearchStateControlled controlled) { |
| + mSelectionController = selectionController; |
| + mPolicy = policy; |
| + mControlled = controlled; |
| + } |
| + |
| + // ============================================================================================ |
| + // 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(State.IDLE, reason); |
| + } |
| + |
| + /** |
| + * Enters the given starting state immediately. |
| + * @param state the new starting {@link State} we're now in |
| + */ |
| + void enter(State state) { |
| + assert state == State.UNDEFINED || state == State.IDLE |
| + || state == State.LONG_PRESS_RECOGNIZED || state == State.TAP_RECOGNIZED; |
| + mState = state; |
| + System.out.println("ctxs enter : " + mSelectionController.getSelectionType()); |
| + notifyStartingWorkOn(mState); |
| + notifyFinishedWorkOn(mState); |
| + } |
| + |
| + /** |
| + * Confirms that work is starting on the given state. |
| + * @param state the {@link State} that we're now working on |
| + */ |
| + void notifyStartingWorkOn(State state) { |
| + assert mState == state; |
| + mDidStartWork = true; |
| + } |
| + |
| + /** |
| + * @return whether we're still working on the given state |
| + */ |
| + boolean isStillWorkingOn(State 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 #starting}. |
| + * @param state the {@link State} that we've finished working on. |
| + */ |
| + void notifyFinishedWorkOn(State state) { |
|
Theresa
2017/03/28 15:52:55
There's nothing here enforcing the order of state
Donn Denman
2017/03/29 18:56:51
I tried to do this with the initial implementation
|
| + if (state != mState) return; |
| + |
| + assert mDidStartWork; |
| + |
| + if (mState == State.IDLE) { |
| + Log.w(TAG, "Warning, the " + state.toString() + " state was aborted."); |
| + return; |
| + } |
| + |
| + switch (state) { |
| + case LONG_PRESS_RECOGNIZED: |
| + transitionTo(State.GATHERING_SURROUNDINGS); |
| + break; |
| + case TAP_RECOGNIZED: |
| + transitionTo(State.GATHERING_SURROUNDINGS); |
| + break; |
| + case GATHERING_SURROUNDINGS: |
| + System.out.println("ctxs finished gathering, checking if tap..." |
| + + mSelectionController.getSelectionType()); |
| + if (mSelectionController.getSelectionType() == SelectionType.TAP) { |
| + transitionTo(State.DECIDING_SUPPRESSION); |
| + } else { |
| + // No suppression yet for Long-press. |
| + transitionTo(State.SHOWING_LONGPRESS_SEARCH, |
| + StateChangeReason.TEXT_SELECT_LONG_PRESS); |
| + } |
| + break; |
| + case DECIDING_SUPPRESSION: |
| + transitionTo(State.WAITING_FOR_POSSIBLE_NAVIGATION); |
| + break; |
| + case WAITING_FOR_POSSIBLE_NAVIGATION: |
| + transitionTo(State.SELECTING_WORD); |
| + break; |
| + case SELECTING_WORD: |
| + System.out.println("checking if we should resolve..."); |
| + if (mPolicy.shouldPreviousTapResolve()) { |
| + transitionTo(State.RESOLVING); |
| + } else { |
| + transitionTo(State.SHOWING_TAP_SEARCH); |
| + } |
| + break; |
| + case RESOLVING: |
| + transitionTo(State.SHOWING_TAP_SEARCH); |
| + break; |
| + default: |
| + System.out.println("The state " + state.toString() + " is not transitional!"); |
| + Log.e(TAG, "The state " + state.toString() + " is not transitional!"); |
| + assert false; |
| + } |
| + } |
| + |
| + /** |
| + * @return The current State. |
| + */ |
| + State getState() { |
| + return mState; |
| + } |
| + |
| + /** |
| + * Establishes the given state by calling code that starts work on that state. |
| + * @param state the new {@link State} to establish. |
| + */ |
| + private void transitionTo(State state) { |
| + transitionTo(state, null); |
| + } |
| + |
| + // TODO(donnd): remove! |
| + private String nameOf(State state) { |
| + if (state == null) return "null"; |
| + return state.name(); |
| + } |
| + |
| + /** |
| + * 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 State} 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(State state, @Nullable StateChangeReason reason) { |
| + State previousState = mState; |
| + mState = state; |
| + mDidStartWork = false; |
| + System.out.println( |
| + "ctxs transition from " + nameOf(previousState) + " to " + nameOf(mState)); |
| + |
| + switch (state) { |
| + case IDLE: |
| + assert reason != null; |
| + mControlled.hideContextualSearchUx(reason); |
| + break; |
| + case SHOWING_LONGPRESS_SEARCH: |
| + mControlled.showContextualSearchUx(StateChangeReason.TEXT_SELECT_LONG_PRESS); |
| + break; |
| + |
| + case TAP_RECOGNIZED: |
| + // Fall through: |
| + case LONG_PRESS_RECOGNIZED: |
| + // Fall through: |
| + case GATHERING_SURROUNDINGS: |
| + System.out.println( |
| + "ctxs gather... is tap? " + mSelectionController.getSelectionType()); |
| + mControlled.gatherSurroundingText(); |
| + break; |
| + case WAITING_FOR_POSSIBLE_NAVIGATION: |
| + mControlled.waitForPossibleNavigation(); |
| + break; |
| + case SELECTING_WORD: |
| + mControlled.selectWordAroundCaret(); |
| + break; |
| + case DECIDING_SUPPRESSION: |
| + mControlled.decideTapSuppression(); |
| + break; |
| + case RESOLVING: |
| + System.out.println("RESOLVING..."); |
| + mControlled.showContextualSearchUx(StateChangeReason.TEXT_SELECT_TAP); |
| + mControlled.startSearchTermResolutionRequest(); |
| + break; |
| + case SHOWING_TAP_SEARCH: |
| + System.out.println("SHOWING_TAP_SEARCH..."); |
| + // The panel may already be showing a Tap, but we'll make sure it's showing now. |
| + mControlled.showContextualSearchUx(StateChangeReason.TEXT_SELECT_TAP); |
| + break; |
| + |
| + default: |
| + System.out.println("ctxs Warning! Transition ignored to " + state); |
| + break; |
| + } |
| + } |
| +} |