chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/KeyboardHideHelper.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.omnibox;

import android.content.res.Configuration;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowManager;

import androidx.annotation.VisibleForTesting;

import org.chromium.ui.base.WindowDelegate;

/**
 * Helps to detect whether the virtual keyboard was hidden to allow unfocusing of the omnibox.
 *
 * <p>There are no Android APIs to determine the visibility of a soft keyboard, so this class
 * aggressively detects signals that might indicate the keyboard has been hidden.
 */
class KeyboardHideHelper implements ViewTreeObserver.OnGlobalLayoutListener {
    private static final long SOFT_KEYBOARD_HIDDEN_TIMEOUT_MS = 1000;

    private final View mView;
    private final Runnable mOnHideCallback;
    private final Runnable mClearListenerDelayedTask;
    private final Rect mTempRect;

    private WindowDelegate mWindowDelegate;
    private boolean mIsLayoutListenerAttached;
    private int mInitialViewportHeight;

    /**
     * Constructs the helper for hiding the keyboard.
     *
     * @param view The view the keyboard is shown for.
     * @param onHideCallback The callback to be triggered when the keyboard is detected as hidden.
     */
    public KeyboardHideHelper(View view, Runnable onHideCallback) {
        mView = view;
        mOnHideCallback = onHideCallback;
        mClearListenerDelayedTask =
                new Runnable() {
                    @Override
                    public void run() {
                        cleanUp();
                    }
                };
        mTempRect = new Rect();
    }

    /** Initialize the delegate that allows interaction with the Window. */
    public void setWindowDelegate(WindowDelegate windowDelegate) {
        mWindowDelegate = windowDelegate;
    }

    /**
     * Begin monitoring for keyboard hidden and defocuses the omnibox if it is detected.
     *
     * <p>Only call this method once a strong signal arrives that indicates the keyboard likely will
     * be hidden (i.e. KeyEvent.KEYCODE_BACK in View#onKeyPreIme). Any increase in window size will
     * trigger the hide callback to be notified after this is called. This is meant to be a "good"
     * approximation for user intent to dimiss the keyboard to compensate for the lack of a proper
     * signal from the system.
     */
    public void monitorForKeyboardHidden() {
        cleanUp();

        // If a hardware keyboard is attached, they might be hiding the virtual keyboard, but
        // attempting to continue typing with the hardware keyboard.  Disable unfocusing the
        // omnibox automatically if we detect this case might be possible.
        if (mView.getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY) {
            return;
        }

        if (mWindowDelegate != null) {
            assert mWindowDelegate.getWindowSoftInputMode()
                            != WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
                    : "SOFT_INPUT_ADJUST_NOTHING prevents detecting window size changes.";
        }

        mView.getViewTreeObserver().addOnGlobalLayoutListener(this);
        mIsLayoutListenerAttached = true;

        mInitialViewportHeight = availableWindowHeight();
        mView.postDelayed(mClearListenerDelayedTask, SOFT_KEYBOARD_HIDDEN_TIMEOUT_MS);
    }

    @Override
    public void onGlobalLayout() {
        if (availableWindowHeight() > mInitialViewportHeight) {
            mOnHideCallback.run();
            cleanUp();
        }
    }

    @VisibleForTesting
    boolean isMonitoringForLayoutChanges() {
        return mIsLayoutListenerAttached;
    }

    private int availableWindowHeight() {
        if (mWindowDelegate == null) {
            return mView.getRootView().getHeight();
        }

        mWindowDelegate.getWindowVisibleDisplayFrame(mTempRect);
        return Math.min(mTempRect.height(), mWindowDelegate.getDecorViewHeight());
    }

    private void cleanUp() {
        if (!mIsLayoutListenerAttached) return;
        mView.removeCallbacks(mClearListenerDelayedTask);
        mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        mIsLayoutListenerAttached = false;
    }
}