authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-08-10 21:18:13+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-10 21:18:13+02:00
log428d9f9f67b73688317e362d9f658d06b2a42dfe
treed71b347094aaa0c2c0035807ab014e9dc5cfe124
parent1b120f352f6cc518d0b2822d4ec303d050aaef10
signature Signed by PGP key B5690EEEBB952194

Allow Android tablets to rotate (#5613)


2 files changed, 81 insertions(+), 0 deletions(-)

apps/mobile/app.config.ts+1
......@@ -338,6 +338,7 @@ const config: ExpoConfig = {
338338 "./plugins/withAndroidModernPopupMenu.cjs",
339339 "./plugins/withAndroidModernAlertDialog.cjs",
340340 "./plugins/withAndroidPredictiveBackCompat.cjs",
341 "./plugins/withAndroidTabletOrientation.cjs",
341342 ...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []),
342343 ],
343344 extra: {
apps/mobile/plugins/withAndroidTabletOrientation.cjs created+80
......@@ -0,0 +1,80 @@
1const { withMainActivity } = require("expo/config-plugins");
2
3// The top-level `orientation: "portrait"` writes android:screenOrientation="portrait"
4// into the manifest, which locks every Android device — including tablets — to
5// portrait. iOS doesn't have this problem: iPads must support all orientations
6// because the app is multitasking-capable, so only iPhones end up portrait-only.
7// Mirror that split on Android: keep the manifest lock for phones and lift it at
8// runtime on tablets (smallest width >= 600dp, the standard tablet breakpoint),
9// since requestedOrientation set at runtime overrides the manifest value.
10// FULL_USER allows all four orientations while still respecting the user's
11// auto-rotate lock, matching iPad behavior. Foldables change
12// smallestScreenWidthDp on fold/unfold without recreating the activity
13// (smallestScreenSize is in the manifest's configChanges), so the policy is
14// re-evaluated in onConfigurationChanged: unfolding past the tablet breakpoint
15// unlocks rotation, and folding back restores the portrait lock.
16
17const ORIENTATION_METHODS = `
18 // Applied in onCreate and re-applied on fold/unfold; added by
19 // withAndroidTabletOrientation.
20 override fun onConfigurationChanged(newConfig: Configuration) {
21 super.onConfigurationChanged(newConfig)
22 applyTabletOrientation()
23 }
24
25 private fun applyTabletOrientation() {
26 requestedOrientation = if (resources.configuration.smallestScreenWidthDp >= 600) {
27 ActivityInfo.SCREEN_ORIENTATION_FULL_USER
28 } else {
29 ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
30 }
31 }
32`;
33
34const ORIENTATION_ON_CREATE_CALL = `
35 applyTabletOrientation()`;
36
37function insertAfter(contents, anchor, insertion, description) {
38 const index = contents.indexOf(anchor);
39 if (index === -1) {
40 throw new Error(
41 `withAndroidTabletOrientation: could not find ${description} in MainActivity — the Expo template changed; update the plugin anchors.`,
42 );
43 }
44 const end = index + anchor.length;
45 return contents.slice(0, end) + insertion + contents.slice(end);
46}
47
48module.exports = function withAndroidTabletOrientation(config) {
49 return withMainActivity(config, (nextConfig) => {
50 let contents = nextConfig.modResults.contents;
51 if (nextConfig.modResults.language !== "kt") {
52 throw new Error("withAndroidTabletOrientation: MainActivity must be Kotlin.");
53 }
54 if (contents.includes("SCREEN_ORIENTATION_FULL_USER")) {
55 return nextConfig;
56 }
57
58 contents = insertAfter(
59 contents,
60 "import android.os.Bundle",
61 "\nimport android.content.pm.ActivityInfo\nimport android.content.res.Configuration",
62 "the android.os.Bundle import",
63 );
64 contents = insertAfter(
65 contents,
66 "class MainActivity : ReactActivity() {",
67 ORIENTATION_METHODS,
68 "the MainActivity class declaration",
69 );
70 contents = insertAfter(
71 contents,
72 "super.onCreate(null)",
73 ORIENTATION_ON_CREATE_CALL,
74 "the super.onCreate call",
75 );
76
77 nextConfig.modResults.contents = contents;
78 return nextConfig;
79 });
80};