chromium/content/public/android/java/src/org/chromium/content/browser/input/InputMethodManagerWrapperImpl.java

// Copyright 2013 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.input;

import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.os.IBinder;
import android.os.ResultReceiver;
import android.os.StrictMode;
import android.view.View;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.InputMethodManager;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.Log;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.content_public.browser.ContentFeatureList;
import org.chromium.content_public.browser.ContentFeatureMap;
import org.chromium.content_public.browser.InputMethodManagerWrapper;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.display.DisplayAndroid;

import java.lang.ref.WeakReference;

/** Wrapper around Android's InputMethodManager */
public class InputMethodManagerWrapperImpl implements InputMethodManagerWrapper {
    private static final boolean DEBUG_LOGS = false;
    private static final String TAG = "IMM";

    private final Context mContext;

    private WindowAndroid mWindowAndroid;

    private Delegate mDelegate;

    private Runnable mPendingRunnableOnInputConnection;

    private boolean mOptimizeImmHideCalls;

    public InputMethodManagerWrapperImpl(
            Context context, WindowAndroid windowAndroid, Delegate delegate) {
        if (DEBUG_LOGS) Log.i(TAG, "Constructor");
        mContext = context;
        mWindowAndroid = windowAndroid;
        mDelegate = delegate;
        mOptimizeImmHideCalls =
                ContentFeatureMap.isEnabled(ContentFeatureList.OPTIMIZE_IMM_HIDE_CALLS);
    }

    @Override
    public void onWindowAndroidChanged(WindowAndroid windowAndroid) {
        mWindowAndroid = windowAndroid;
    }

    private Context getContextForMultiDisplay() {
        Activity activity = getActivityFromWindowAndroid(mWindowAndroid);
        if (DEBUG_LOGS) {
            if (activity == null) Log.i(TAG, "activity is null.");
        }
        return activity == null ? mContext : activity;
    }

    /**
     * Get an Activity from WindowAndroid.
     *
     * @param windowAndroid
     * @return The Activity. May return null if it fails.
     */
    private static Activity getActivityFromWindowAndroid(WindowAndroid windowAndroid) {
        if (windowAndroid == null) return null;
        // Unwrap this when we actually need it.
        WeakReference<Activity> weakRef = windowAndroid.getActivity();
        if (weakRef == null) return null;
        return weakRef.get();
    }

    private InputMethodManager getInputMethodManager() {
        // For multi-display case, we need to have the correct activity context.
        // However, for Chrome, we wrap application context as container view's
        // context in order to prevent leakage. (e.g. see TabImpl.java).
        // This is a workaround to update mContext with the activity from
        // ActivityWindowAndroid. https://crbug.com/1021403
        Context context = getContextForMultiDisplay();
        return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
    }

    @Override
    public void restartInput(View view) {
        if (DEBUG_LOGS) Log.i(TAG, "restartInput");
        InputMethodManager manager = getInputMethodManager();
        if (manager == null) return;
        manager.restartInput(view);
    }

    @VisibleForTesting
    protected boolean hasCorrectDisplayId(Context context, Activity activity) {
        // We did not support multi-display before O.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return true;

        int contextDisplayId = getDisplayId(context);
        int activityDisplayId = getDisplayId(activity);
        if (activityDisplayId != contextDisplayId) {
            Log.w(
                    TAG,
                    "Activity's display ID(%d) does not match context's display ID(%d). "
                            + "Using a workaround to show soft input on the correct display...",
                    activityDisplayId,
                    contextDisplayId);
            return false;
        }
        return true;
    }

    @VisibleForTesting
    protected int getDisplayId(Context context) {
        return DisplayAndroid.getNonMultiDisplay(context).getDisplayId();
    }

    @Override
    public void showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
        if (DEBUG_LOGS) Log.i(TAG, "showSoftInput");
        mPendingRunnableOnInputConnection = null;
        Activity activity = getActivityFromWindowAndroid(mWindowAndroid);
        if (activity != null && !hasCorrectDisplayId(mContext, activity)) {
            // https://crbug.com/1021403
            // This is a workaround for multi-display case. We need this as long as
            // Chrome uses the application context for creating the content view.
            // Note that this will create a few ms delay in showing keyboard.
            activity.getWindow().setLocalFocus(true, true);

            if (mDelegate != null && !mDelegate.hasInputConnection()) {
                // Delay keyboard showing until input connection is established.
                mPendingRunnableOnInputConnection =
                        () -> {
                            if (isActive(view)) showSoftInputInternal(view, flags, resultReceiver);
                        };
                return;
            }
            // If we already have InputConnection, then show soft input now.
        }
        showSoftInputInternal(view, flags, resultReceiver);
    }

    private void showSoftInputInternal(View view, int flags, ResultReceiver resultReceiver) {
        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); // crbug.com/616283
        try {
            InputMethodManager manager = getInputMethodManager();
            if (manager != null) {
                boolean result = manager.showSoftInput(view, flags, resultReceiver);
                if (DEBUG_LOGS) Log.i(TAG, "showSoftInputInternal: " + view + ", " + result);
            }
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }
    }

    @Override
    public boolean isActive(View view) {
        InputMethodManager manager = getInputMethodManager();
        final boolean active = manager != null && manager.isActive(view);
        if (DEBUG_LOGS) Log.i(TAG, "isActive: " + active);
        return active;
    }

    @Override
    public boolean hideSoftInputFromWindow(
            IBinder windowToken, int flags, ResultReceiver resultReceiver) {
        if (DEBUG_LOGS) Log.i(TAG, "hideSoftInputFromWindow");
        mPendingRunnableOnInputConnection = null;
        InputMethodManager manager = getInputMethodManager();
        if (manager == null || mOptimizeImmHideCalls && !manager.isAcceptingText()) return false;
        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); // crbug.com/616283
        try {
            return manager.hideSoftInputFromWindow(windowToken, flags, resultReceiver);
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }
    }

    @Override
    public void updateSelection(
            View view, int selStart, int selEnd, int candidatesStart, int candidatesEnd) {
        if (DEBUG_LOGS) {
            Log.i(
                    TAG,
                    "updateSelection: SEL [%d, %d], COM [%d, %d]",
                    selStart,
                    selEnd,
                    candidatesStart,
                    candidatesEnd);
        }
        InputMethodManager manager = getInputMethodManager();
        if (manager == null) return;
        manager.updateSelection(view, selStart, selEnd, candidatesStart, candidatesEnd);
    }

    @Override
    public void updateCursorAnchorInfo(View view, CursorAnchorInfo cursorAnchorInfo) {
        if (DEBUG_LOGS) Log.i(TAG, "updateCursorAnchorInfo");
        InputMethodManager manager = getInputMethodManager();
        if (manager == null) return;
        manager.updateCursorAnchorInfo(view, cursorAnchorInfo);
    }

    @Override
    public void updateExtractedText(
            View view, int token, android.view.inputmethod.ExtractedText text) {
        if (DEBUG_LOGS) Log.d(TAG, "updateExtractedText");
        InputMethodManager manager = getInputMethodManager();
        if (manager == null) return;
        manager.updateExtractedText(view, token, text);
    }

    @Override
    public void onInputConnectionCreated() {
        if (mPendingRunnableOnInputConnection == null) return;
        Runnable runnable = mPendingRunnableOnInputConnection;
        mPendingRunnableOnInputConnection = null;
        PostTask.postTask(TaskTraits.UI_DEFAULT, runnable);
    }
}