From 78a93515ca0d1665a6c7c6b01be5ecb195c82f84 Mon Sep 17 00:00:00 2001 From: Gabriele Mangiavacchi Date: Wed, 15 Apr 2026 13:59:57 +0200 Subject: [PATCH 1/4] fix(android): add null safety checks to prevent NPE in window insets callbacks - Null-check getRootWindowInsets() in setOnApplyWindowInsetsListener to prevent NPE when view is detached - Null-check getRootWindowInsets() in onStart() and refactor to single call - Null-check getRootWindowInsets() in onEnd() to prevent NPE when view is detached - Null-check keyboardEventListener (@Nullable) before invoking in onStart() and onEnd() - Remove unused imports (Build, TypedValue, Insets) --- .../plugins/keyboard/Keyboard.java | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java b/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java index 7cf87fc..3d716fd 100644 --- a/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java +++ b/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java @@ -2,9 +2,7 @@ import android.content.Context; import android.graphics.Rect; -import android.os.Build; import android.util.DisplayMetrics; -import android.util.TypedValue; import android.view.View; import android.view.Window; import android.view.inputmethod.InputMethodManager; @@ -12,7 +10,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsAnimationCompat; import androidx.core.view.WindowInsetsCompat; @@ -59,14 +56,16 @@ public Keyboard(AppCompatActivity activity, boolean resizeOnFullScreen) { rootView = content.getRootView(); ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> { - boolean showingKeyboard = ViewCompat.getRootWindowInsets(rootView).isVisible(WindowInsetsCompat.Type.ime()); + WindowInsetsCompat rootInsets = ViewCompat.getRootWindowInsets(rootView); + if (rootInsets == null) { + return insets; + } + boolean showingKeyboard = rootInsets.isVisible(WindowInsetsCompat.Type.ime()); if (showingKeyboard && resizeOnFullScreen) { possiblyResizeChildOfContent(true); } - v.onApplyWindowInsets(insets.toWindowInsets()); - return insets; }); @@ -88,8 +87,11 @@ public WindowInsetsAnimationCompat.BoundsCompat onStart( @NonNull WindowInsetsAnimationCompat animation, @NonNull WindowInsetsAnimationCompat.BoundsCompat bounds ) { - boolean showingKeyboard = ViewCompat.getRootWindowInsets(rootView).isVisible(WindowInsetsCompat.Type.ime()); WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(rootView); + if (insets == null) { + return super.onStart(animation, bounds); + } + boolean showingKeyboard = insets.isVisible(WindowInsetsCompat.Type.ime()); int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom; DisplayMetrics dm = activity.getResources().getDisplayMetrics(); final float density = dm.density; @@ -98,10 +100,12 @@ public WindowInsetsAnimationCompat.BoundsCompat onStart( possiblyResizeChildOfContent(showingKeyboard); } - if (showingKeyboard) { - keyboardEventListener.onKeyboardEvent(EVENT_KB_WILL_SHOW, Math.round(imeHeight / density)); - } else { - keyboardEventListener.onKeyboardEvent(EVENT_KB_WILL_HIDE, 0); + if (keyboardEventListener != null) { + if (showingKeyboard) { + keyboardEventListener.onKeyboardEvent(EVENT_KB_WILL_SHOW, Math.round(imeHeight / density)); + } else { + keyboardEventListener.onKeyboardEvent(EVENT_KB_WILL_HIDE, 0); + } } return super.onStart(animation, bounds); } @@ -109,16 +113,21 @@ public WindowInsetsAnimationCompat.BoundsCompat onStart( @Override public void onEnd(@NonNull WindowInsetsAnimationCompat animation) { super.onEnd(animation); - boolean showingKeyboard = ViewCompat.getRootWindowInsets(rootView).isVisible(WindowInsetsCompat.Type.ime()); WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(rootView); + if (insets == null) { + return; + } + boolean showingKeyboard = insets.isVisible(WindowInsetsCompat.Type.ime()); int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom; DisplayMetrics dm = activity.getResources().getDisplayMetrics(); final float density = dm.density; - if (showingKeyboard) { - keyboardEventListener.onKeyboardEvent(EVENT_KB_DID_SHOW, Math.round(imeHeight / density)); - } else { - keyboardEventListener.onKeyboardEvent(EVENT_KB_DID_HIDE, 0); + if (keyboardEventListener != null) { + if (showingKeyboard) { + keyboardEventListener.onKeyboardEvent(EVENT_KB_DID_SHOW, Math.round(imeHeight / density)); + } else { + keyboardEventListener.onKeyboardEvent(EVENT_KB_DID_HIDE, 0); + } } } } From 91f4d856d95c97c589c0182bdf760e03cc40ae75 Mon Sep 17 00:00:00 2001 From: Gabriele Mangiavacchi Date: Wed, 15 Apr 2026 16:51:58 +0200 Subject: [PATCH 2/4] fix(android): revert listener to rootView instead of content --- .../main/java/com/capacitorjs/plugins/keyboard/Keyboard.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java b/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java index 3d716fd..4842b3c 100644 --- a/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java +++ b/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java @@ -55,7 +55,7 @@ public Keyboard(AppCompatActivity activity, boolean resizeOnFullScreen) { FrameLayout content = activity.getWindow().getDecorView().findViewById(android.R.id.content); rootView = content.getRootView(); - ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> { + ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> { WindowInsetsCompat rootInsets = ViewCompat.getRootWindowInsets(rootView); if (rootInsets == null) { return insets; From 2ff01383526a444389d0a87bbea3b3c2bb5e244c Mon Sep 17 00:00:00 2001 From: Franjanko Date: Wed, 15 Apr 2026 18:08:25 +0200 Subject: [PATCH 3/4] fix(android): implement setResizeMode and prevent WebView resize when mode is none Capacitor 8.3.0 introduced bridge-level IME inset handling that causes the WebView to shrink when the software keyboard appears, regardless of the resize mode configured via setResizeMode(). Changes: - Replace the constructor-captured boolean resizeOnFullScreen with a mutable resizeEnabled field so the policy can be toggled at runtime. - When resizeEnabled is false and the keyboard becomes visible, consume IME WindowInsetsCompat so the WebView and its siblings never receive the inset, counteracting the new bridge behaviour introduced in 8.3.0. - Use Keyboard.this.resizeEnabled in the WindowInsetsAnimationCompat callback so the animation path also respects runtime changes. - Expose setResizeEnabled(boolean) and isResizeEnabled() API for KeyboardPlugin to call. - Implement setResizeMode() and getResizeMode() plugin methods; they were previously unimplemented. The initial value of currentResizeMode is derived from the resizeOnFullScreen config at load() time. --- .../plugins/keyboard/Keyboard.java | 24 +++++++++++++++++-- .../plugins/keyboard/KeyboardPlugin.java | 14 +++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java b/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java index 4842b3c..7c42c39 100644 --- a/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java +++ b/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java @@ -10,6 +10,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsAnimationCompat; import androidx.core.view.WindowInsetsCompat; @@ -28,6 +29,7 @@ interface KeyboardEventListener { private int usableHeightPrevious; private FrameLayout.LayoutParams frameLayoutParams; private View mChildOfContent; + private boolean resizeEnabled; public void setKeyboardEventListener(@Nullable KeyboardEventListener keyboardEventListener) { this.keyboardEventListener = keyboardEventListener; @@ -50,6 +52,7 @@ public Keyboard(Bridge bridge, boolean resizeOnFullScreen) { // We may want to deprecate this constructor in the future, but we are keeping it now to keep backward compatibility with cap 7 public Keyboard(AppCompatActivity activity, boolean resizeOnFullScreen) { this.activity = activity; + this.resizeEnabled = resizeOnFullScreen; //http://stackoverflow.com/a/4737265/1091751 detect if keyboard is showing FrameLayout content = activity.getWindow().getDecorView().findViewById(android.R.id.content); @@ -62,10 +65,19 @@ public Keyboard(AppCompatActivity activity, boolean resizeOnFullScreen) { } boolean showingKeyboard = rootInsets.isVisible(WindowInsetsCompat.Type.ime()); - if (showingKeyboard && resizeOnFullScreen) { + if (showingKeyboard && resizeEnabled) { possiblyResizeChildOfContent(true); } + // When resize is disabled, consume IME insets so that the WebView + // (and any other child) does not reflow its content when the keyboard + // appears (counteracts Capacitor bridge inset handling added in 8.3.0+). + if (!resizeEnabled && showingKeyboard) { + return new WindowInsetsCompat.Builder(insets) + .setInsets(WindowInsetsCompat.Type.ime(), Insets.NONE) + .build(); + } + return insets; }); @@ -96,7 +108,7 @@ public WindowInsetsAnimationCompat.BoundsCompat onStart( DisplayMetrics dm = activity.getResources().getDisplayMetrics(); final float density = dm.density; - if (resizeOnFullScreen) { + if (Keyboard.this.resizeEnabled) { possiblyResizeChildOfContent(showingKeyboard); } @@ -137,6 +149,14 @@ public void onEnd(@NonNull WindowInsetsAnimationCompat animation) { frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams(); } + public void setResizeEnabled(boolean enabled) { + this.resizeEnabled = enabled; + } + + public boolean isResizeEnabled() { + return this.resizeEnabled; + } + public void show() { ((InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE)).showSoftInput(activity.getCurrentFocus(), 0); } diff --git a/android/src/main/java/com/capacitorjs/plugins/keyboard/KeyboardPlugin.java b/android/src/main/java/com/capacitorjs/plugins/keyboard/KeyboardPlugin.java index bf6735a..69aa28b 100644 --- a/android/src/main/java/com/capacitorjs/plugins/keyboard/KeyboardPlugin.java +++ b/android/src/main/java/com/capacitorjs/plugins/keyboard/KeyboardPlugin.java @@ -12,11 +12,13 @@ public class KeyboardPlugin extends Plugin { private Keyboard implementation; + private volatile String currentResizeMode = "none"; @Override public void load() { execute(() -> { boolean resizeOnFullScreen = getConfig().getBoolean("resizeOnFullScreen", false); + currentResizeMode = resizeOnFullScreen ? "native" : "none"; implementation = new Keyboard(getBridge(), resizeOnFullScreen); implementation.setKeyboardEventListener(this::onKeyboardEvent); @@ -59,12 +61,20 @@ public void setStyle(PluginCall call) { @PluginMethod public void setResizeMode(PluginCall call) { - call.unimplemented(); + String mode = call.getString("mode", "none"); + execute(() -> { + boolean resizeEnabled = !"none".equalsIgnoreCase(mode); + implementation.setResizeEnabled(resizeEnabled); + currentResizeMode = mode; + call.resolve(); + }); } @PluginMethod public void getResizeMode(PluginCall call) { - call.unimplemented(); + JSObject result = new JSObject(); + result.put("mode", currentResizeMode); + call.resolve(result); } @PluginMethod From b683ab5976b5b1e5ce4c1a946feab4d2042f6fc1 Mon Sep 17 00:00:00 2001 From: Franjanko Date: Wed, 15 Apr 2026 18:08:41 +0200 Subject: [PATCH 4/4] fix(ios): prevent WebView resize when resize mode is none Capacitor 8.3.0 added resize logic in the bridge that resizes the WebView frame and/or raises additionalSafeAreaInsets.bottom when the software keyboard appears. This bypassed the ResizeNone policy that the plugin is supposed to enforce via setResizeMode("none"). Add an explicit case ResizeNone branch in _updateFrame that: - Restores the WebView to its full-screen frame (no keyboard padding). - Resets additionalSafeAreaInsets.bottom to 0 on the bridge view controller if the bridge raised it, so that the content inset is fully suppressed while resize mode is none. --- ios/Sources/KeyboardPlugin/Keyboard.m | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ios/Sources/KeyboardPlugin/Keyboard.m b/ios/Sources/KeyboardPlugin/Keyboard.m index 29c0708..9f8ad1f 100644 --- a/ios/Sources/KeyboardPlugin/Keyboard.m +++ b/ios/Sources/KeyboardPlugin/Keyboard.m @@ -240,6 +240,20 @@ - (void)_updateFrame [self.webView setFrame:CGRectMake(wf.origin.x, wf.origin.y, f.size.width - wf.origin.x, f.size.height - wf.origin.y - self.paddingBottom)]; break; } + case ResizeNone: + { + // Actively restore the webview to its full-screen frame to counteract any + // keyboard-induced resize that Capacitor's bridge may apply (8.3.0+). + [self.webView setFrame:CGRectMake(wf.origin.x, wf.origin.y, f.size.width - wf.origin.x, f.size.height - wf.origin.y)]; + // Also reset additionalSafeAreaInsets.bottom in case the bridge raised it + // to push content above the keyboard. + UIEdgeInsets safeInsets = self.bridge.viewController.additionalSafeAreaInsets; + if (safeInsets.bottom != 0) { + safeInsets.bottom = 0; + self.bridge.viewController.additionalSafeAreaInsets = safeInsets; + } + break; + } default: break; }