chromium/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/SelectionClientManager.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.chrome.browser.contextualsearch;

import android.os.Build;
import android.view.textclassifier.TextClassifier;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

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.SelectionPopupController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.touch_selection.SelectionEventType;

/**
 * Manages the current {@link SelectionClient} instances, with support for 0-2 instances.
 * This class supports one permanent instance for Smart Text Selection, and one non-permanent
 * instance for Contextual Search that can be added or removed. <p> Usage: After being constructed
 * this class knows if Smart Select is active or not, and can return a {@link SelectionClient}.
 * If Smart Select is active it will return the Smart Select Client from
 * {@link #getSelectionClient}, and if not then {@link #getSelectionClient()} will return {@code
 * null}. A non-permanent client may be added using
 * {@link #addContextualSearchSelectionClient(SelectionClient)} to connect to Contextual Search.
 * This client may be removed later using {@link #removeContextualSearchSelectionClient()}.
 */
public class SelectionClientManager {
    // Whether Smart Select is allowed to be enabled in Chrome.
    private final boolean mIsSmartSelectionEnabledInChrome;

    /**
     * The single optional client supported directly by this class.
     * It may be null, the Smart Selection client, or our bridge between Smart Select and Contextual
     * Search.
     */
    private @Nullable SelectionClient mOptionalSelectionClient;

    /**
     * Constructs an instance that can return a {@link SelectionClient} that's a mix of an optional
     * Smart Selection client and a transient Contextual Search client.
     * @param webContents The {@link WebContents} that will show popups for this client.
     */
    SelectionClientManager(WebContents webContents) {
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
            assert webContents != null;
            mOptionalSelectionClient = SelectionClient.createSmartSelectionClient(webContents);
            SelectionPopupController controller =
                    SelectionPopupController.fromWebContents(webContents);
            controller.setSelectionClient(mOptionalSelectionClient);
        }
        mIsSmartSelectionEnabledInChrome = mOptionalSelectionClient != null;
    }

    @VisibleForTesting
    /** Test-only constructor. */
    SelectionClientManager(SelectionClient optionalSelectionClient, boolean enableSmartSelection) {
        mOptionalSelectionClient = optionalSelectionClient;
        mIsSmartSelectionEnabledInChrome = enableSmartSelection;
    }

    /**
     * @return the current {@link SelectionClient} or {@code null} if there is none currently
     *         active.
     */
    @Nullable
    SelectionClient getSelectionClient() {
        return mOptionalSelectionClient;
    }

    /**
     * Adds the given {@link SelectionClient} as an additional temporary instance that will be
     * notified of method calls.
     * @param contextualSearchSelectionClient An additional {@link SelectionClient} that should be
     *        notified of requests going forward, used by Contextual Search.
     * @return The resulting {@link SelectionClient} that should be active after the addition.
     */
    SelectionClient addContextualSearchSelectionClient(
            SelectionClient contextualSearchSelectionClient) {
        assert contextualSearchSelectionClient != null;
        assert !(mOptionalSelectionClient instanceof SelectionClientBridge)
                : "No more than two selection client instances are supported!";
        if (mIsSmartSelectionEnabledInChrome) {
            mOptionalSelectionClient =
                    new SelectionClientBridge(
                            mOptionalSelectionClient, contextualSearchSelectionClient);
        } else {
            mOptionalSelectionClient = contextualSearchSelectionClient;
        }
        return mOptionalSelectionClient;
    }

    /**
     * Removes the current {@link SelectionClient} from the current instances that will be notified
     * of method calls.
     * @return A remaining {@link SelectionClient} used for Smart Selection or {@code null}.
     */
    @Nullable
    SelectionClient removeContextualSearchSelectionClient() {
        if (mIsSmartSelectionEnabledInChrome) {
            assert mOptionalSelectionClient instanceof SelectionClientBridge
                    : "Looks like it was never added.";
            SelectionClientBridge currentSelectionClientBridge =
                    (SelectionClientBridge) mOptionalSelectionClient;
            mOptionalSelectionClient = currentSelectionClientBridge.getSmartSelectionClient();
        } else {
            assert !(mOptionalSelectionClient instanceof SelectionClientBridge)
                    : "Internal error managing selection clients.";
            mOptionalSelectionClient = null;
        }
        return mOptionalSelectionClient;
    }

    /**
     * Bridges exactly two {@link SelectionClient} instances.  Both sides of the bridge receive
     * calls for most incoming calls to this instance.
     */
    private static class SelectionClientBridge implements SelectionClient {
        private final SelectionClient mSmartSelectionClient;
        private final SelectionClient mContextualSearchSelectionClient;

        /**
         * Constructs a bridge between the {@code smartSelectionClient} and the given
         * {@code contextualSearchSelectionClient}.  Method calls to this class are repeated to both
         * instances, with some exceptions: calls that return a value are only routed to the
         * {@code smartSelectionClient}.
         * @param smartSelectionClient The platform-dependent {@link SelectionClient}, which will be
         *        used as the dominant client, responsible for any results that need to be returned
         *        from method calls (typically a {@code SmartSelectionClient}).
         * @param contextualSearchSelectionClient A {@link SelectionClient} based on the
         *        {@code ContextualSearchManager}.
         */
        private SelectionClientBridge(
                SelectionClient smartSelectionClient,
                SelectionClient contextualSearchSelectionClient) {
            mSmartSelectionClient = smartSelectionClient;
            mContextualSearchSelectionClient = contextualSearchSelectionClient;
        }

        /**
         * @return The current Smart Selection client.
         */
        private SelectionClient getSmartSelectionClient() {
            return mSmartSelectionClient;
        }

        @Override
        public void onSelectionChanged(String selection) {
            mSmartSelectionClient.onSelectionChanged(selection);
            mContextualSearchSelectionClient.onSelectionChanged(selection);
        }

        @Override
        public void onSelectionEvent(
                @SelectionEventType int eventType, float posXPix, float posYPix) {
            mSmartSelectionClient.onSelectionEvent(eventType, posXPix, posYPix);
            mContextualSearchSelectionClient.onSelectionEvent(eventType, posXPix, posYPix);
        }

        @Override
        public void selectAroundCaretAck(@Nullable SelectAroundCaretResult result) {
            mSmartSelectionClient.selectAroundCaretAck(result);
            mContextualSearchSelectionClient.selectAroundCaretAck(result);
        }

        @Override
        public boolean requestSelectionPopupUpdates(boolean shouldSuggest) {
            return mSmartSelectionClient.requestSelectionPopupUpdates(shouldSuggest);
        }

        @Override
        public void cancelAllRequests() {
            mSmartSelectionClient.cancelAllRequests();
            mContextualSearchSelectionClient.cancelAllRequests();
        }

        @Override
        public void setTextClassifier(TextClassifier textClassifier) {
            mSmartSelectionClient.setTextClassifier(textClassifier);
            mContextualSearchSelectionClient.setTextClassifier(textClassifier);
        }

        @Override
        public TextClassifier getTextClassifier() {
            return mSmartSelectionClient.getTextClassifier();
        }

        @Override
        public TextClassifier getCustomTextClassifier() {
            return mSmartSelectionClient.getCustomTextClassifier();
        }

        @Override
        public SelectionEventProcessor getSelectionEventProcessor() {
            return mSmartSelectionClient.getSelectionEventProcessor();
        }
    }
}