Skip to content

Target api 36 - #233

Open
patriciojofre wants to merge 9 commits into
masterfrom
chore/target-api-36
Open

Target api 36#233
patriciojofre wants to merge 9 commits into
masterfrom
chore/target-api-36

Conversation

@patriciojofre

Copy link
Copy Markdown
Member

No description provided.

patriciojofre and others added 6 commits August 3, 2026 16:40
R.layout.welcomebatch only existed under layout-port, layout-small-port,
layout-large-port and layout-xlarge-port. WelcomeBatchActivity requests a
portrait lock before calling setContentView, so the missing configuration was
unreachable until now — but Android 16 (API 36) ignores that lock on displays
with smallest width >= 600dp, which would make the lookup fail with
Resources$NotFoundException on tablets in landscape.

Add a default layout/welcomebatch.xml that centers the logo with
gravity="center" plus maxHeight/adjustViewBounds instead of the fixed
marginTop="300dp" the portrait variants use, so it holds up at any window
height. The existing -port variants are untouched, so portrait is unchanged.

Also delete device_ready, warning and warning_android7: port-only layouts with
no base variant whose referencing activities were removed long ago
(DeviceReadyActivity in 89ffcaa, R.layout.warning in 9b44e8f). They were the
remaining MissingDefaultResource fatals.

Lint fatals: 11 -> 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Google Play requires the target API level to stay within one year of the latest
Android release: from Aug 31, 2026 an app targeting below API 36 can no longer
be updated. Raise compileSdk and targetSdk to 36 in the Play build, along with
the COMPILE_SDK_VERSION field reported to the API as hardware telemetry.

AGP 8.13.2 and Gradle 8.13 already support API 36, so no toolchain change is
needed. Verified with a clean build: assembleDebug, lintDebug and
testDebugUnitTest all pass.

build.gradle.internal deliberately stays on 33 — the wipe capability it relies
on is blocked at higher target API levels, and that variant ships through an
internal process rather than Play, so the requirement does not apply to it. A
comment now records this so it is not bumped by mistake.

Note: build.gradle.dev is gitignored, so its matching bump to 36 is local only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
R.layout.welcomebatch shipped for years with variants only under layout-port and
friends. That stayed invisible because every activity in the app requests a
portrait lock, so the missing configuration was unreachable — until API 36 made
Android ignore that lock on large screens.

Pin the invariant with a Robolectric test that resolves layouts under landscape
qualifiers. It needs no MDM enrollment, no network and no emulator: resource
resolution is a function of the resource name and the device configuration only,
so a qualifier reproduces the failure exactly. getLayout performs the same
configuration lookup as inflation without needing a theme, a parent view or
resolvable drawables.

Beyond asserting welcomebatch specifically, the test sweeps every R.layout entry
and reports all misses at once, so the next layout added under a narrow-only
qualifier fails CI with its own name instead of crashing on a user's tablet.

Verified the guard actually fails: removing layout/welcomebatch.xml turns 3 of
the 5 tests red with the same Resources$NotFoundException the app would throw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The manifest sets android:enableOnBackInvokedCallback="true", so from Android 13
on the platform dispatches back through OnBackInvokedDispatcher and stops calling
Activity.onBackPressed(). Five activities only overrode onBackPressed, so their
back handling had been dead on modern devices: BarcodeActivity, PanelWebActivity,
ReportActivity and SecurityActivity silently stopped returning to the password
screen, and PermissionInformationActivity — whose empty override exists to keep
the user inside the permission flow — silently stopped blocking back at all.

ReportActivity and SecurityActivity extend AndroidX activities, so they register
an OnBackPressedCallback, which covers both the old and the new path.

The other three extend the platform Activity directly and have no
OnBackPressedDispatcher. Rather than change their base class, they register with
OnBackInvokedDispatcher on Android 13+ through the new BackNavigationCompat
helper and keep their onBackPressed override for API 21-32, where
enableOnBackInvokedCallback is ignored. Those remaining overrides carry a
SuppressLint with that reason, so the lint check keeps flagging real cases.

Lint GestureBackNavigation errors: 5 -> 0. MissingSuperCall: 2 -> 0.

Not runtime-verified: this needs a device pass on Android 13+ to confirm back
lands where intended, especially that PermissionInformationActivity still refuses
to go back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…itly

From Android 14 on, registerReceiver without RECEIVER_EXPORTED or
RECEIVER_NOT_EXPORTED throws SecurityException. Three call sites had no flag:

- PopUpAlertActivity registered inside a try/catch that only logged, so on
  Android 14+ the popup's receivers were never registered and the alert could no
  longer be dismissed remotely — a silent functional failure.
- PreyDisablePowerOptionsService registered with no catch at all, in both onStart
  and onStartCommand, so the service went down every time the
  disable-power-options feature was enabled.

Route all of them through ContextCompat.registerReceiver, which picks the right
overload per API level.

The flag values are not uniform, because the broadcasts are not:

- close_prey and popup_prey_* are only ever sent from inside the app, so they are
  now RECEIVER_NOT_EXPORTED. CheckPasswordHtmlActivity previously used
  RECEIVER_EXPORTED for close_prey, which let any installed app dismiss the
  password screen by broadcasting a bare action string.
- ACTION_APPLICATION_RESTRICTIONS_CHANGED comes from the system when the EMM
  changes restrictions, so it stays exported.
- ACTION_CLOSE_SYSTEM_DIALOGS is a system broadcast and stays exported too,
  though the platform has not delivered it to apps since Android 12.

Making those receivers non-exported exposed the matching problem on the send
side: the app broadcast bare implicit intents that any app with a matching
receiver could pick up. close_prey now goes through
CheckPasswordHtmlActivity.broadcastClosePrey, which scopes the intent with
setPackage, and AlertReceiver does the same for the popup action.

Lint UnspecifiedRegisterReceiverFlag errors: 4 -> 0, UnsafeImplicitIntentLaunch
back to 0. Clean build, lint and 93 unit tests all pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds instrumented coverage for the two things API 36 changed for this app, run
against a Pixel Tablet AVD on Android 16 (2560x1600 @ 320dpi, smallestWidth
800dp) where the platform reports ignoreOrientationRequest=true — so the portrait
lock every activity here requests really is being ignored.

Resource resolution: welcomebatch, plus a sweep over every R.layout entry, are
resolved in the configuration the device is actually in. The Robolectric guard
already covers this with qualifiers; this runs it against the real resource
resolution on a real large screen.

Back navigation: one activity per implementation, SecurityActivity through
AndroidX's OnBackPressedDispatcher and PanelWebActivity through
BackNavigationCompat and the platform dispatcher. The assertion is on the
destination rather than on the launched activity finishing, because the platform's
default back also finishes it — a test that only checked for finishing would pass
with the handler dead. Confirmed both fail when the registration is removed.

These are instrumented rather than local tests because non-exported activities
cannot be started from the adb shell on Android 16 (SecurityException: not
exported), while instrumentation runs as the app's own UID.

The lock screens are deliberately not covered, with the reason recorded in the
file: launching one cold trips the app's own guard, which starts CloseActivity and
tears the lock down within a second. That is correct behaviour, so such a test
would fail for a reason unrelated to rotation. Confirming the lock survives a
rotation still needs a real lock command from the panel and a physical rotate.

Full instrumented suite: 52 tests green on the tablet AVD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
patriciojofre and others added 3 commits August 4, 2026 14:14
github-code-quality flagged largeScreenLandscapeQualifiersAreDeclared as a useless
comparison, and it was right: it asserted that a literal array's length was >= 2,
which cannot fail. The array it checked was not used by anything either — the
sweeps take their configuration from their own @config qualifiers — so it was dead
data that could drift from the tests it claimed to describe. Both are gone.

The instrumented test had the same defect unflagged: assertTrue(smallestWidthDp
> 0) is true on any device. Replaced with an assertion that can actually fail —
PanelWebActivity declares screenOrientation="portrait", so on a large screen
running API 36 it must come up in landscape anyway. The preconditions are now
assumptions, so on a phone AVD or a tablet held in portrait the test reports as
skipped rather than passed, and a green suite cannot be read as coverage it did
not provide. Verified both ways: passes in landscape, skips in portrait.

Rotating the tablet to portrait to check that also surfaced a real false positive
in the sweeps. R.layout holds the merged resources of every dependency, and some
library layouts are config-scoped by design — material_clock_period_toggle_land
exists only for landscape — so the sweep failed on a Material layout that is not
ours to fix. The sweeps now skip third-party layouts by name prefix, a list
verified to cover all 108 library layouts while excluding none of the 38 the app
owns, and the failure message reports both counts so drift stays visible. If a
dependency later adds a config-scoped layout under a new prefix the test fails
naming it, which is the right direction to fail in.

Re-confirmed the guard still bites after the change: hiding layout/welcomebatch.xml
fails 3 of 4 Robolectric tests with "1 of 38 app layout(s) do not resolve ...
(108 library layouts skipped)".

92 unit tests and 52 instrumented tests green on the tablet AVD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI ran the emulator on API 30 with the runner's phone-sized AVD, so it never
exercised the change the app is being migrated for: below 600dp of smallest width
API 36 still honours the portrait lock, and the large-screen tests reported as
skipped while the job went green having covered nothing.

Adds an api-level matrix. API 36 is what the app targets; API 30 stays because
part of the code only runs below Android 13 — BackNavigationCompat falls back to
onBackPressed there, and the three plain-Activity overrides are live only on that
path — so dropping it would leave that untested. fail-fast is off so one failing
level still reports the other.

On the 36 leg the script resizes the display to 2560x1600 at 240dpi, giving 1066dp
of smallest width in landscape, and sets ignore-orientation-request. Verified
locally against both configurations rather than assumed: with phone metrics
(1080x2400 @ 420dpi, 411dp) portraitLockIsIgnoredOnThisDisplay reports skipped,
and with the metrics above it runs and passes, 0 skipped, with the platform
reporting ignoreOrientationRequest=true.

Also guards that test by API level. It assumed a large screen in landscape and
then asserted the portrait lock was ignored, which is false below API 36 — so on
the new API 30 leg it would have failed for a reason unrelated to any defect.
It now skips there instead.

Artifact names are per API level, since upload-artifact@v4 rejects two uploads
under one name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The API 30 leg failed before running anything:

    /usr/bin/sh -c if [ "30" -ge 36 ]; then
    /usr/bin/sh: 1: Syntax error: end of file unexpected (expecting "fi")

reactivecircus/android-emulator-runner executes each line of an inline script in
its own shell, so only the first line of the `if` ever reached sh. Any multi-line
construct inline is unparseable, regardless of API level — the 36 leg would have
failed the same way.

Moves the logic to .github/scripts/run-instrumented-tests.sh, invoked as a single
line with the API level as an argument, and adds a guard for a missing argument so
a future edit to the workflow fails loudly instead of silently skipping the resize.

Verified rather than assumed, against a live API 36 emulator:
- `run-instrumented-tests.sh 36` applies the large-screen configuration, the
  platform reports ignoreOrientationRequest=true, and all 52 instrumented tests
  pass with 0 skipped.
- `run-instrumented-tests.sh 30` takes the phone-sized branch and the suite still
  runs — this is the path that was broken.
- The original failure reproduces locally by feeding just the first line to sh,
  which is what confirmed the diagnosis.
- Syntax checked with both `sh -n` and `dash -n`, dash being /usr/bin/sh on the
  runner.

The file is committed as 100755; without the exec bit the runner cannot invoke it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant