·8 min read

Android lock orientation: landscape or portrait

Locking a Capacitor game to portrait in code instead of with screenOrientation, the adb rotation that never rotated, and the Android 16 tablets that ignore the lock anyway.

capacitorandroidgamedevtesting

Contents

For years the way to lock an Android game's orientation was one attribute on the activity: android:screenOrientation="portrait". It is the standard answer, and it still works. Play Console now flags it, though, under a warning titled Remove resizability and orientation restrictions in your app to support large screen devices. I wanted ASMR Logo Craft Block to stay portrait and the manifest to stay clean.

So the lock moved into code. Writing it was the short part. Proving it worked took far longer, because the first test that passed had not rotated anything, and the last device I tried ignores the lock on purpose.

The setup: a Capacitor 8 game, targetSdkVersion 36, tested on two Android 16 emulators (API 36) — a Pixel 9 Pro, 426dp wide, and a Pixel Tablet, 800dp — plus iOS simulators for the other half.

Lock the Android screen orientation throughout the app, in code

The game runs in one activity, so one call covers the whole app. I put it in a small Capacitor plugin shared by all my games, so every game gets it from its config instead of from Java:

"plugins": {
  "Orientation": { "lock": "portrait" }
}

The Android side is the whole plugin, minus a warning for a missing value:

@CapacitorPlugin(name = "Orientation")
public class OrientationPlugin extends Plugin {
    @Override
    public void load() {
        String lock = getConfig().getString("lock", null);
        if ("portrait".equals(lock)) {
            getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT);
        } else if ("landscape".equals(lock)) {
            getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE);
        }
    }
}

Capacitor calls load() while the activity is being created, so the request is in place before the first frame. The other direction is the same call with USER_LANDSCAPE. My game is portrait, so every device result below is the portrait branch; I have not run a sideways game through it, and I would rather say so than imply I had.

I used the USER_ variants rather than plain PORTRAIT. The difference is whether the player's rotation preference can still flip the screen upside down. I picked it on paper and did not test the upside-down case.

The first plan I got back from Claude Code locked phones only and let tablets rotate. I turned it down: the game is portrait, so it is portrait everywhere, and the game decides that, not the tooling. That decision matters again further down.

The plugin that was never loaded

I wrote a device test for it — open a level, turn the device, check the page is still taller than it is wide, keep playing, then relaunch while held sideways. First run on the phone:

❌ Stays portrait (43.9s): step 6 `expectOrientation: portrait`: the page is 952×426: landscape (after 10s)

The lock had not run at all. I had installed the plugin from a local tarball with npm install --no-save to try it before publishing, and npx cap sync registers native plugins from the dependencies in package.json. A package that is in node_modules but not in package.json was not in capacitor.plugins.json or capacitor.settings.gradle. Adding it to the dependencies and syncing again put it in both, and the sync output listed it among the plugins it found.

If a Capacitor plugin you are testing locally seems to do nothing, check capacitor.plugins.json before you check your code.

The test tool refused to turn a locked phone

Second run, plugin loaded:

❌ Stays portrait (35.1s): step 5 `rotate: landscape`: WebDriverError: Screen rotation cannot be changed to ROTATION_270 after 2000ms. Is it locked programmatically? when running "orientation" with method "POST"

That is the lock working. My runner turned the device through Appium's UiAutomator2 driver, which waits for the app to follow and fails when it does not. Its own error message asks the right question. The iOS simulator gave the same refusal in its own words, Unable To Rotate Device. A rotation step for a locked app has to turn the device and leave the judging to the next step.

Lock the Android system orientation from adb

What I wanted from adb was a player's hand, turning the whole device for every app: tell Android, lock rotation, landscape, and leave the sensor out of it. The first thing I reached for was the settings write:

adb shell settings put system accelerometer_rotation 0
adb shell settings put system user_rotation 1

With that in the runner, the scenario passed. Green, 60 seconds.

Then I asked the obvious question: did the screen turn? A locked app staying portrait looks exactly like a device that was never rotated. So I ran the same user_rotation write by hand and opened the Settings app, which has no lock and should follow:

settings app: mCurrentRotation=ROTATION_0

Settings stayed upright too. The write had changed a value and the display had not moved, so my passing test had proved nothing. I have written about checks that pass while broken before, and here was another one, built the same day.

The window manager's own command does turn it:

$ adb shell cmd window user-rotation lock 1
settings app: mCurrentRotation=ROTATION_90 user_rotation=1 accel=0
game: mCurrentRotation=ROTATION_0
game cold start held landscape: mCurrentRotation=ROTATION_0

Same device, same command, one after the other: Settings went to 90 degrees, the game stayed at 0, and a cold start with the device already sideways still opened upright. That control is what makes the game's result mean something. The runner now turns the device with cmd window user-rotation lock 1 and back with lock 0.

Oddly, after the window manager command settings get system user_rotation reads 1, the same value the plain write had set. I don't know why the plain write left the emulator's display where it was, and I didn't dig into it. I only know it did, which was enough to stop trusting it.

With the rotation real, the full suite ran on the Pixel 9 Pro emulator: 24 of 24, including the new portrait scenario. The built APK's manifest has no screenOrientation at all:

aapt dump xmltree app-debug.apk AndroidManifest.xml | grep -ci screenOrientation
0

Phones portrait, tablets free: Android 16 ignores the app's orientation lock

On the Pixel Tablet emulator the scenario failed at step 2, before any rotation:

❌ Stays portrait (95.2s): step 2 `expectOrientation: portrait`: the page is 1280×800: landscape (after 10s)

I expected this one. By Android's own notes, Android 16 ignores orientation requests from apps targeting API 36 when the screen's smallest width is 600dp or more, which covers tablets and open foldables. I still wanted to see it was the system saying no, and not my request going missing:

requestedOrientation=SCREEN_ORIENTATION_USER_PORTRAIT
ignoreOrientationRequest=true
Physical size: 2560x1600
Physical density: 320

The game asked for portrait and the display was told to ignore the ask. 2560×1600 at density 320 is 800dp on the short side, well over the line. One small trap on the way: one of my dumpsys activity activities reads returned SCREEN_ORIENTATION_UNSPECIFIED, because it lists more than one activity and the first one was not mine. Grep for the record with your package name in it.

This settles the phones-only plan from earlier, and the old question of locking portrait on phones and the other way on tablets. On Android 16 the tablet half of that is not the app's call any more, whether the request lives in the manifest or in code. Moving the lock into code keeps the attribute Play Console flags out of the manifest; it does not buy an exemption. I watched it ignore a portrait request only. Nothing in that flag is specific to portrait, but I have not run the sideways case.

Android's notes on this change list games as an exception when the manifest declares android:appCategory="game". My game does not declare it, and I have not tested whether adding it brings the lock back on the tablet, so I am not shipping it or telling you it works.

iOS gets a plist, and iPadOS 26 a pillarbox

On iOS the allowed orientations live in Info.plist, so the same config drives a small CLI that rewrites it before each build. The game's plist allowed portrait and both sideways orientations on iPhone, and all four on iPad. After the CLI, the built app carried this:

UISupportedInterfaceOrientations       [UIInterfaceOrientationPortrait]
UISupportedInterfaceOrientations~ipad  [UIInterfaceOrientationPortrait, UIInterfaceOrientationPortraitUpsideDown]
UIRequiresFullScreen                   true

UIRequiresFullScreen is there because Apple requires an iPad app that does not support all four orientations to opt out of multitasking before App Store Connect accepts it. That is their rule as documented; I did not trigger the rejection to confirm it.

On the iPhone 16 Pro simulator the full suite passed, 24 of 24, with one ad-close flake that passed on its rerun. The portrait scenario also passed on an iPad Pro simulator on iPadOS 18.1. The control was the old plist: rebuilt with it, the same scenario failed with the page at 874×402, wider than tall, so the test catches a missing lock. On an iPadOS 26.1 simulator held sideways, the game stays portrait in a narrower window with bars at the sides.

What I did not test

  • A game locked sideways. The code path is one constant away from the portrait one, and none of the results above exercised it.
  • A real Android tablet or foldable. The tablet result comes from the Pixel Tablet emulator.
  • android:appCategory="game", for the reason above.
  • Upside-down portrait, on either platform.

Takeaway

Put the lock in code if Play Console's large-screen warning bothers you: setRequestedOrientation from the plugin's load() holds on phones from the first frame and keeps screenOrientation out of the manifest. Test it with a control, an app that does rotate, turned by the same command, because settings put system user_rotation can report success while the screen sits still. And on Android 16, plan the layout for a tablet held whichever way the player holds it, since the lock will not decide that for you.

More posts

New writing when there is something worth saying. By email or by RSS, whichever you prefer.