From a07da582a844de70919ff946035c45b7b06a1e82 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 7 Aug 2026 14:31:55 +0200 Subject: [PATCH 1/2] fix(mobile): allow Android tablets to rotate - Add an Expo config plugin that enables all user-allowed orientations on tablets - Keep portrait locking for Android phones --- apps/mobile/app.config.ts | 1 + .../plugins/withAndroidTabletOrientation.cjs | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 apps/mobile/plugins/withAndroidTabletOrientation.cjs diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index c684689fb26..486ede13abc 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -338,6 +338,7 @@ const config: ExpoConfig = { "./plugins/withAndroidModernPopupMenu.cjs", "./plugins/withAndroidModernAlertDialog.cjs", "./plugins/withAndroidPredictiveBackCompat.cjs", + "./plugins/withAndroidTabletOrientation.cjs", ...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []), ], extra: { diff --git a/apps/mobile/plugins/withAndroidTabletOrientation.cjs b/apps/mobile/plugins/withAndroidTabletOrientation.cjs new file mode 100644 index 00000000000..eb4ee1cfeab --- /dev/null +++ b/apps/mobile/plugins/withAndroidTabletOrientation.cjs @@ -0,0 +1,55 @@ +const { withMainActivity } = require("expo/config-plugins"); + +// The top-level `orientation: "portrait"` writes android:screenOrientation="portrait" +// into the manifest, which locks every Android device — including tablets — to +// portrait. iOS doesn't have this problem: iPads must support all orientations +// because the app is multitasking-capable, so only iPhones end up portrait-only. +// Mirror that split on Android: keep the manifest lock for phones and lift it at +// runtime on tablets (smallest width >= 600dp, the standard tablet breakpoint), +// since requestedOrientation set in onCreate overrides the manifest value. +// FULL_USER allows all four orientations while still respecting the user's +// auto-rotate lock, matching iPad behavior. + +const ORIENTATION_OVERRIDE = ` + if (resources.configuration.smallestScreenWidthDp >= 600) { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_USER + }`; + +function insertAfter(contents, anchor, insertion, description) { + const index = contents.indexOf(anchor); + if (index === -1) { + throw new Error( + `withAndroidTabletOrientation: could not find ${description} in MainActivity — the Expo template changed; update the plugin anchors.`, + ); + } + const end = index + anchor.length; + return contents.slice(0, end) + insertion + contents.slice(end); +} + +module.exports = function withAndroidTabletOrientation(config) { + return withMainActivity(config, (nextConfig) => { + let contents = nextConfig.modResults.contents; + if (nextConfig.modResults.language !== "kt") { + throw new Error("withAndroidTabletOrientation: MainActivity must be Kotlin."); + } + if (contents.includes("SCREEN_ORIENTATION_FULL_USER")) { + return nextConfig; + } + + contents = insertAfter( + contents, + "import android.os.Bundle", + "\nimport android.content.pm.ActivityInfo", + "the android.os.Bundle import", + ); + contents = insertAfter( + contents, + "super.onCreate(null)", + ORIENTATION_OVERRIDE, + "the super.onCreate call", + ); + + nextConfig.modResults.contents = contents; + return nextConfig; + }); +}; From 1f42088bc2975297392f519a2da5b31da12ee40e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 10 Aug 2026 21:05:11 +0200 Subject: [PATCH 2/2] fix(mobile): update Android tablet orientation on folding - Reapply orientation policy when foldables cross the tablet breakpoint - Restore portrait locking when folding back to phone dimensions --- .../plugins/withAndroidTabletOrientation.cjs | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/apps/mobile/plugins/withAndroidTabletOrientation.cjs b/apps/mobile/plugins/withAndroidTabletOrientation.cjs index eb4ee1cfeab..2254cdb1921 100644 --- a/apps/mobile/plugins/withAndroidTabletOrientation.cjs +++ b/apps/mobile/plugins/withAndroidTabletOrientation.cjs @@ -6,14 +6,33 @@ const { withMainActivity } = require("expo/config-plugins"); // because the app is multitasking-capable, so only iPhones end up portrait-only. // Mirror that split on Android: keep the manifest lock for phones and lift it at // runtime on tablets (smallest width >= 600dp, the standard tablet breakpoint), -// since requestedOrientation set in onCreate overrides the manifest value. +// since requestedOrientation set at runtime overrides the manifest value. // FULL_USER allows all four orientations while still respecting the user's -// auto-rotate lock, matching iPad behavior. +// auto-rotate lock, matching iPad behavior. Foldables change +// smallestScreenWidthDp on fold/unfold without recreating the activity +// (smallestScreenSize is in the manifest's configChanges), so the policy is +// re-evaluated in onConfigurationChanged: unfolding past the tablet breakpoint +// unlocks rotation, and folding back restores the portrait lock. -const ORIENTATION_OVERRIDE = ` - if (resources.configuration.smallestScreenWidthDp >= 600) { - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_USER - }`; +const ORIENTATION_METHODS = ` + // Applied in onCreate and re-applied on fold/unfold; added by + // withAndroidTabletOrientation. + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + applyTabletOrientation() + } + + private fun applyTabletOrientation() { + requestedOrientation = if (resources.configuration.smallestScreenWidthDp >= 600) { + ActivityInfo.SCREEN_ORIENTATION_FULL_USER + } else { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + } +`; + +const ORIENTATION_ON_CREATE_CALL = ` + applyTabletOrientation()`; function insertAfter(contents, anchor, insertion, description) { const index = contents.indexOf(anchor); @@ -39,13 +58,19 @@ module.exports = function withAndroidTabletOrientation(config) { contents = insertAfter( contents, "import android.os.Bundle", - "\nimport android.content.pm.ActivityInfo", + "\nimport android.content.pm.ActivityInfo\nimport android.content.res.Configuration", "the android.os.Bundle import", ); + contents = insertAfter( + contents, + "class MainActivity : ReactActivity() {", + ORIENTATION_METHODS, + "the MainActivity class declaration", + ); contents = insertAfter( contents, "super.onCreate(null)", - ORIENTATION_OVERRIDE, + ORIENTATION_ON_CREATE_CALL, "the super.onCreate call", );