From e7dd0eeebe98aaf41a31944e0b6eb62d5f4db8bf Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 10 Jun 2021 03:06:10 +0500 Subject: [PATCH] Fix issue where soft keyboard overlaps extra keys or terminal in some cases Check TermuxActivityRootView javadocs for details. --- .../java/com/termux/app/TermuxActivity.java | 45 +++- .../app/terminal/TermuxActivityRootView.java | 227 ++++++++++++++++++ .../terminal/TermuxTerminalViewClient.java | 9 +- app/src/main/res/layout/activity_termux.xml | 132 +++++----- .../com/termux/shared/view/KeyboardUtils.java | 2 +- .../com/termux/shared/view/ViewUtils.java | 98 ++++++++ 6 files changed, 447 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java create mode 100644 termux-shared/src/main/java/com/termux/shared/view/ViewUtils.java diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index 91215ba242..51ac549486 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -30,6 +30,7 @@ import android.widget.Toast; import com.termux.R; +import com.termux.app.terminal.TermuxActivityRootView; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; import com.termux.app.activities.HelpActivity; @@ -68,7 +69,6 @@ */ public final class TermuxActivity extends Activity implements ServiceConnection { - /** * The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to * {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in @@ -77,7 +77,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection TermuxService mTermuxService; /** - * The main view of the activity showing the terminal. Initialized in onCreate(). + * The {@link TerminalView} shown in {@link TermuxActivity} that displays the terminal. */ TerminalView mTerminalView; @@ -103,6 +103,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection */ private TermuxAppSharedProperties mProperties; + /** + * The root view of the {@link TermuxActivity}. + */ + TermuxActivityRootView mTermuxActivityRootView; + + /** + * The space at the bottom of {@link @mTermuxActivityRootView} of the {@link TermuxActivity}. + */ + View mTermuxActivityBottomSpaceView; + /** * The terminal extra keys view. */ @@ -189,6 +199,10 @@ public void onCreate(Bundle savedInstanceState) { return; } + mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view); + mTermuxActivityRootView.setActivity(this); + mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view); + View content = findViewById(android.R.id.content); content.setOnApplyWindowInsetsListener((v, insets) -> { mNavBarHeight = insets.getSystemWindowInsetBottom(); @@ -241,6 +255,8 @@ public void onStart() { if (mTermuxTerminalViewClient != null) mTermuxTerminalViewClient.onStart(); + addTermuxActivityRootViewGlobalLayoutListener(); + registerTermuxActivityBroadcastReceiver(); } @@ -277,6 +293,8 @@ protected void onStop() { if (mTermuxTerminalViewClient != null) mTermuxTerminalViewClient.onStop(); + removeTermuxActivityRootViewGlobalLayoutListener(); + unregisterTermuxActivityBroadcastReceiever(); getDrawer().closeDrawers(); } @@ -390,6 +408,17 @@ private void setDrawerTheme() { + public void addTermuxActivityRootViewGlobalLayoutListener() { + getTermuxActivityRootView().getViewTreeObserver().addOnGlobalLayoutListener(getTermuxActivityRootView()); + } + + public void removeTermuxActivityRootViewGlobalLayoutListener() { + if (getTermuxActivityRootView() != null) + getTermuxActivityRootView().getViewTreeObserver().removeOnGlobalLayoutListener(getTermuxActivityRootView()); + } + + + private void setTermuxTerminalViewAndClients() { // Set termux terminal view and session clients mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this); @@ -438,8 +467,8 @@ private void setTerminalToolbarHeight() { if (terminalToolbarViewPager == null) return; ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams(); layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight * - (mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) * - mProperties.getTerminalToolbarHeightScaleFactor()); + (mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) * + mProperties.getTerminalToolbarHeightScaleFactor()); terminalToolbarViewPager.setLayoutParams(layoutParams); } @@ -679,6 +708,14 @@ public int getNavBarHeight() { return mNavBarHeight; } + public TermuxActivityRootView getTermuxActivityRootView() { + return mTermuxActivityRootView; + } + + public View getTermuxActivityBottomSpaceView() { + return mTermuxActivityBottomSpaceView; + } + public ExtraKeysView getExtraKeysView() { return mExtraKeysView; } diff --git a/app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java b/app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java new file mode 100644 index 0000000000..33b6510f6b --- /dev/null +++ b/app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java @@ -0,0 +1,227 @@ +package com.termux.app.terminal; + +import android.content.Context; +import android.graphics.Rect; +import android.inputmethodservice.InputMethodService; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.inputmethod.EditorInfo; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +import com.termux.app.TermuxActivity; +import com.termux.shared.logger.Logger; +import com.termux.shared.view.ViewUtils; + + +/** + * The {@link TermuxActivity} relies on {@link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE)} + * set by {@link TermuxTerminalViewClient#setSoftKeyboardState(boolean, boolean)} to automatically + * resize the view and push the terminal up when soft keyboard is opened. However, this does not + * always work properly. When `enforce-char-based-input=true` is set in `termux.properties` + * and {@link com.termux.view.TerminalView#onCreateInputConnection(EditorInfo)} sets the inputType + * to `InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS` + * instead of the default `InputType.TYPE_NULL` for termux, some keyboards may still show suggestions. + * Gboard does too, but only when text is copied and clipboard suggestions **and** number keys row + * toggles are enabled in its settings. When number keys row toggle is not enabled, Gboard will still + * show the row but will switch it with suggestions if needed. If its enabled, then number keys row + * is always shown and suggestions are shown in an additional row on top of it. This additional row is likely + * part of the candidates view returned by the keyboard app in {@link InputMethodService#onCreateCandidatesView()}. + * + * With the above configuration, the additional clipboard suggestions row partially covers the + * extra keys/terminal. Reopening the keyboard/activity does not fix the issue. This is either a bug + * in the Android OS where it does not consider the candidate's view height in its calculation to push + * up the view or because Gboard does not include the candidate's view height in the height reported + * to android that should be used, hence causing an overlap. + * + * Gboard logs the following entry to `logcat` when its opened with or without the suggestions bar showing: + * I/KeyboardViewUtil: KeyboardViewUtil.calculateMaxKeyboardBodyHeight():62 leave 500 height for app when screen height:2392, header height:176 and isFullscreenMode:false, so the max keyboard body height is:1716 + * where `keyboard_height = screen_height - height_for_app - header_height` (62 is a hardcoded value in Gboard source code and may be a version number) + * So this may in fact be due to Gboard but https://stackoverflow.com/questions/57567272 suggests + * otherwise. Another similar report https://stackoverflow.com/questions/66761661. + * Also check https://github.com/termux/termux-app/issues/1539. + * + * This overlap may happen even without `enforce-char-based-input=true` for keyboards with extended layouts + * like number row, etc. + * + * To fix these issues, `activity_termux.xml` has the constant 1sp transparent + * `activity_termux_bottom_space_view` View at the bottom. This will appear as a line matching the + * activity theme. When {@link TermuxActivity} {@link ViewTreeObserver.OnGlobalLayoutListener} is + * called when any of the sub view layouts change, like keyboard opening/closing keyboard, + * extra keys/input view switched, etc, we check if the bottom space view is visible or not. + * If its not, then we add a margin to the bottom of the root view, so that the keyboard does not + * overlap the extra keys/terminal, since the margin will push up the view. By default the margin + * added is equal to the height of the hidden part of extra keys/terminal. For Gboard's case, the + * hidden part equals the `header_height`. The updates to margins may cause a jitter in some cases + * when the view is redrawn if the margin is incorrect, but logic has been implemented to avoid that. + */ +public class TermuxActivityRootView extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener { + + public TermuxActivity mActivity; + public Integer marginBottom; + public Integer lastMarginBottom; + + /** Log root view events. */ + private boolean ROOT_VIEW_LOGGING_ENABLED = false; + + private static final String LOG_TAG = "TermuxActivityRootView"; + + public TermuxActivityRootView(Context context) { + super(context); + + } + + public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setActivity(TermuxActivity activity) { + mActivity = activity; + } + + /** + * Sets whether root view logging is enabled or not. + * + * @param value The boolean value that defines the state. + */ + public void setIsRootViewLoggingEnabled(boolean value) { + ROOT_VIEW_LOGGING_ENABLED = value; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (marginBottom != null) { + if (ROOT_VIEW_LOGGING_ENABLED) + Logger.logVerbose(LOG_TAG, "onMeasure: Setting bottom margin to " + marginBottom); + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams(); + params.setMargins(0, 0, 0, marginBottom); + setLayoutParams(params); + marginBottom = null; + requestLayout(); + } + } + + @Override + public void onGlobalLayout() { + if (mActivity == null || !mActivity.isVisible()) return; + + View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView(); + if (bottomSpaceView == null) return; + + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams(); + + // Get the position Rects of the bottom space view and the main window holding it + Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView); + if (windowAndViewRects == null) + return; + + Rect windowAvailableRect = windowAndViewRects[0]; + Rect bottomSpaceViewRect = windowAndViewRects[1]; + + // If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible + boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect); + boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && params.bottomMargin > 0; + boolean isVisibleBecauseExtraMargin = (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0; + + if (ROOT_VIEW_LOGGING_ENABLED) + Logger.logVerbose(LOG_TAG, "onGlobalLayout: windowAvailableRect " + windowAvailableRect.bottom + ", bottomSpaceViewRect " + bottomSpaceViewRect.bottom + ", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin + ", isVisible " + isVisible + ", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin); + + // If the bottomSpaceViewRect is visible, then remove the margin if needed + if (isVisible) { + // If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect + // and a margin has been added + // Necessary so that we don't get stuck in an infinite loop since setting margin + // will call OnGlobalLayoutListener again and next time bottom space view + // will be visible and margin will be set to 0, which again will call + // OnGlobalLayoutListener... + // Calling addTermuxActivityRootViewGlobalLayoutListener with a delay fails to + // set appropriate margins when views are changed quickly since some changes + // may be missed. + if (isVisibleBecauseMargin) { + if (ROOT_VIEW_LOGGING_ENABLED) + Logger.logVerbose(LOG_TAG, "onGlobalLayout: Visible due to margin"); + // Once the view has been redrawn with new margin, we set margin back to 0 so that + // when next time onMeasure() is called, margin 0 is used. This is necessary for + // cases when view has been redrawn with new margin because bottom space view was + // hidden by keyboard and then view was redrawn again due to layout change (like + // keyboard symbol view is switched to), android will add margin below its new position + // if its greater than 0, which was already above the keyboard creating x2x margin. + marginBottom = 0; + return; + } + + boolean setMargin = params.bottomMargin != 0; + + // If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect + // onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false + // onGlobalLayout: Bottom margin already equals 0 + if (isVisibleBecauseExtraMargin) { + if (ROOT_VIEW_LOGGING_ENABLED) + Logger.logVerbose(LOG_TAG, "onGlobalLayout: Resetting margin since visible due to extra margin"); + setMargin = true; + // lastMarginBottom must be invalid. May also happen when keyboards are changed. + lastMarginBottom = null; + } + + if (setMargin) { + if (ROOT_VIEW_LOGGING_ENABLED) + Logger.logVerbose(LOG_TAG, "onGlobalLayout: Setting bottom margin to 0"); + params.setMargins(0, 0, 0, 0); + setLayoutParams(params); + } else { + if (ROOT_VIEW_LOGGING_ENABLED) + Logger.logVerbose(LOG_TAG, "onGlobalLayout: Bottom margin already equals 0"); + // This is done so that when next time onMeasure() is called, lastMarginBottom is used. + // This is done since we **expect** the keyboard to have same dimensions next time layout + // changes, so best set margin while view is drawn the first time, otherwise it will + // cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the + // likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic + // works fine for all cases. + marginBottom = lastMarginBottom; + } + } + // ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly + else { + int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom; + if (ROOT_VIEW_LOGGING_ENABLED) + Logger.logVerbose(LOG_TAG, "onGlobalLayout: pxHidden " + pxHidden + ", bottom " + params.bottomMargin); + + boolean setMargin = params.bottomMargin != pxHidden; + + // If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect + // is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener + // again, so that margins are set properly. May happen when toolbar/extra keys is disabled + // and enabled from left drawer, just like case for isVisibleBecauseExtraMargin. + // onMeasure: Setting bottom margin to 176 + // onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false + // onGlobalLayout: Bottom margin already equals 176 + if ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) > 0) { + if (ROOT_VIEW_LOGGING_ENABLED) + Logger.logVerbose(LOG_TAG, "onGlobalLayout: Force setting margin since not visible despite margin"); + setMargin = true; + } + + if (setMargin) { + if (ROOT_VIEW_LOGGING_ENABLED) + Logger.logVerbose(LOG_TAG, "onGlobalLayout: Setting bottom margin to " + pxHidden); + params.setMargins(0, 0, 0, pxHidden); + setLayoutParams(params); + lastMarginBottom = pxHidden; + } else { + if (ROOT_VIEW_LOGGING_ENABLED) + Logger.logVerbose(LOG_TAG, "onGlobalLayout: Bottom margin already equals " + pxHidden); + } + } + } + +} diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java index 231f40bba8..611e8ce246 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java @@ -79,10 +79,13 @@ public void onCreate() { * Should be called when mActivity.onStart() is called */ public void onStart() { - // Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value // Also required if user changed the preference from {@link TermuxSettings} activity and returns - mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(mActivity.getPreferences().isTerminalViewKeyLoggingEnabled()); + boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled(); + mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled); + + // Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future + mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled); } /** @@ -459,7 +462,7 @@ public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProper mShowSoftKeyboardWithDelayOnce = true; } else { // Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it - KeyboardUtils.setResizeTerminalViewForSoftKeyboardFlags(mActivity); + KeyboardUtils.setSoftInputModeAdjustResize(mActivity); // Clear any previous flags to disable soft keyboard in case setting updated KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity); diff --git a/app/src/main/res/layout/activity_termux.xml b/app/src/main/res/layout/activity_termux.xml index 12ee0d46c0..32bb354b72 100644 --- a/app/src/main/res/layout/activity_termux.xml +++ b/app/src/main/res/layout/activity_termux.xml @@ -1,80 +1,96 @@ - - + android:layout_height="0dp" + android:layout_weight="1" + android:orientation="vertical"> - + android:layout_alignParentTop="true" + android:layout_above="@+id/terminal_toolbar_view_pager" + android:layout_height="match_parent"> - - - + android:layout_height="match_parent" + android:layout_marginRight="3dp" + android:layout_marginLeft="3dp" + android:focusableInTouchMode="true" + android:scrollbarThumbVertical="@drawable/terminal_scroll_shape" + android:scrollbars="vertical" + android:importantForAutofill="no" + android:autofillHints="password" /> + android:id="@+id/left_drawer" + android:layout_width="240dp" + android:layout_height="match_parent" + android:layout_gravity="start" + android:background="@android:color/white" + android:choiceMode="singleChoice" + android:divider="@android:color/transparent" + android:dividerHeight="0dp" + android:descendantFocusability="blocksDescendants" + android:orientation="vertical"> -