From 42113c056beb66886c5e5bca663dcf2afc7abd6d Mon Sep 17 00:00:00 2001 From: christian Date: Sun, 27 Sep 2026 23:19:39 +0800 Subject: [PATCH] fix(android): apply IME insets to root view --- .../cc/agentlabs/opencode/MainActivity.kt | 14 +++++++ app.json | 1 + package.json | 2 +- plugins/withKeyboardInsets.js | 38 +++++++++++++++++++ plugins/withKeyboardInsets.test.js | 10 +++++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 plugins/withKeyboardInsets.js create mode 100644 plugins/withKeyboardInsets.test.js diff --git a/android/app/src/main/java/cc/agentlabs/opencode/MainActivity.kt b/android/app/src/main/java/cc/agentlabs/opencode/MainActivity.kt index 66ff305d..61cfbe0c 100644 --- a/android/app/src/main/java/cc/agentlabs/opencode/MainActivity.kt +++ b/android/app/src/main/java/cc/agentlabs/opencode/MainActivity.kt @@ -2,6 +2,9 @@ package cc.agentlabs.opencode import android.os.Build import android.os.Bundle +import android.view.View +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate @@ -17,6 +20,17 @@ class MainActivity : ReactActivity() { // This is required for expo-splash-screen. setTheme(R.style.AppTheme); super.onCreate(null) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + val root = findViewById(android.R.id.content) + val bottom = root.paddingBottom + // Android 15+ no longer resizes edge-to-edge windows for the IME. + ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets -> + view.setPadding(view.paddingLeft, view.paddingTop, view.paddingRight, bottom + insets.getInsets(WindowInsetsCompat.Type.ime()).bottom) + insets + } + ViewCompat.requestApplyInsets(root) + } } /** diff --git a/app.json b/app.json index f3ff1218..a6499fdc 100644 --- a/app.json +++ b/app.json @@ -48,6 +48,7 @@ }, "plugins": [ "expo-router", + "./plugins/withKeyboardInsets", "expo-secure-store", "expo-local-authentication", [ diff --git a/package.json b/package.json index b24a7338..710fbc37 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "android": "expo run:android", "typecheck": "tsc --noEmit", "check:versions": "node scripts/check-version-parity.mjs", - "test": "node --test 'src/**/*.test.ts' 'scripts/*.test.mjs'" + "test": "node --test 'src/**/*.test.ts' 'scripts/*.test.mjs' 'plugins/*.test.js'" }, "dependencies": { "@expo/vector-icons": "^15.0.3", diff --git a/plugins/withKeyboardInsets.js b/plugins/withKeyboardInsets.js new file mode 100644 index 00000000..295dc944 --- /dev/null +++ b/plugins/withKeyboardInsets.js @@ -0,0 +1,38 @@ +const { withMainActivity } = require("@expo/config-plugins") + +const imports = `import android.view.View +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +` + +const listener = ` + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + val root = findViewById(android.R.id.content) + val bottom = root.paddingBottom + // Android 15+ no longer resizes edge-to-edge windows for the IME. + ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets -> + view.setPadding(view.paddingLeft, view.paddingTop, view.paddingRight, bottom + insets.getInsets(WindowInsetsCompat.Type.ime()).bottom) + insets + } + ViewCompat.requestApplyInsets(root) + } +` + +function addKeyboardInsets(contents) { + if (contents.includes("WindowInsetsCompat.Type.ime()")) return contents + if (!contents.includes("import android.os.Bundle\n") || !contents.includes(" super.onCreate(null)\n")) { + throw new Error("Unsupported MainActivity.kt template") + } + return contents + .replace("import android.os.Bundle\n", `import android.os.Bundle\n${imports}`) + .replace(" super.onCreate(null)\n", ` super.onCreate(null)\n${listener}`) +} + +module.exports = (config) => + withMainActivity(config, (config) => { + if (config.modResults.language !== "kt") throw new Error("Keyboard inset fix requires MainActivity.kt") + config.modResults.contents = addKeyboardInsets(config.modResults.contents) + return config + }) + +module.exports.addKeyboardInsets = addKeyboardInsets diff --git a/plugins/withKeyboardInsets.test.js b/plugins/withKeyboardInsets.test.js new file mode 100644 index 00000000..79ab8ace --- /dev/null +++ b/plugins/withKeyboardInsets.test.js @@ -0,0 +1,10 @@ +const assert = require("node:assert/strict") +const test = require("node:test") +const { addKeyboardInsets } = require("./withKeyboardInsets") + +test("adds Android IME insets listener once", () => { + const source = "import android.os.Bundle\nclass MainActivity {\n super.onCreate(null)\n}\n" + const result = addKeyboardInsets(source) + assert.match(result, /WindowInsetsCompat\.Type\.ime\(\)/) + assert.equal(addKeyboardInsets(result), result) +})