Skip to content

Android 17: The targetSdk 37 Changes Nobody Wrote About

Everyone covered the resizability mandate. The other targetSdk 37 behaviour changes get far less attention, and at least one of them is likely to break your instrumentation tests before it breaks your app.

Share
A large bright slab dominating the frame with five small dim blocks below it, two of them cracked with light escaping from the fractures

Android 17's resizability mandate got the coverage it deserved, since it's the change most likely to make your app look broken. But it wasn't the only behaviour change that fires when you raise your target to API level 37, and several of the others share an awkward property: they fail at runtime, in specific conditions, on specific networks, rather than showing up the moment you launch the app.

That's a worse failure mode than a stretched layout. A stretched layout is obvious. A certificate transparency check that only fails against your staging server, or a socket that only fails on a customer's Wi-Fi, gets shipped.

Here's what else changes, ordered by how likely I think each is to cost you a bad afternoon.

In this post:

1. Local Network Access Is Blocked by Default

This is the big one, and it's the one most likely to look like a functionality regression when it's really a plumbing change.

Android 17 introduces the ACCESS_LOCAL_NETWORK runtime permission, and enforcement is mandatory for apps targeting API 37 or higher. In Android 16, this was opt-in. Now it's the rule. Without the permission, traffic to and from local network addresses simply doesn't happen, and the permission is only required at SDK 37 and above. Below that, local network access remains implicitly granted through INTERNET.

The scope is wider than "device discovery." Enforcement covers all traffic to and from local network addresses, not just discovery but the actual data exchange. So raw sockets to a device on the same Wi-Fi, mDNS, SSDP, all of it.

Who this hits: casting and media streaming, smart home setup flows, printer discovery, IoT pairing, local dev tooling, and anything that talks to a companion device on the LAN.

Two paths forward, and Google is fairly clearly steering you toward the first:

Use a system-mediated picker and skip the permission entirely. For Google Cast, the output switcher lets users select a streaming device without the app requesting the broad permission. For general connectivity, NsdManager includes a system-run service picker for mDNS discovery: the system shows a dialogue, the user picks one device, and your app never scans the network. This is strictly better for users, and it removes a permission prompt from your funnel.

Or request the permission at runtime. It sits inside the existing NEARBY_DEVICES permission group, so users who've already granted another permission in that group won't be prompted again. For apps needing direct communication that can't use pickers, Google suggests the permission reset counter strategy, which gives you additional opportunities to re-request with a clearer rationale if the user revokes it.

One measurement trap worth flagging. If you migrate a flow to a system picker, your ACCESS_LOCAL_NETWORK grant rate goes to zero, because you're no longer asking. That is success, not failure. Watch completion rate for the pairing or casting flow itself as ground truth, not permission grants as a proxy. I can see a team panicking at a dashboard here and reverting a migration that worked perfectly.

If you're on Flutter or React Native, check your framework's tracking issue before you plan any of this. Flutter has an open issue for Android 17 local network protections covering socket access and the multicast_dns package. Cross-platform toolchains lag platform permission changes, and you may be waiting on someone else.

And for enterprise deployments: IT administrators can pre-grant this permission using setPermissionGrantState(), which avoids disrupting managed workflows. Worth knowing if you ship to enterprise customers.

2. Certificate Transparency Is On by Default

Certificate transparency verification is now enabled by default for network connections, to protect against man-in-the-middle attacks. On Android 16, it was available, but apps had to opt in.

The failure mode is narrow and nasty: connections relying on private or internal certificates may fail unless you explicitly opt out for those domains through a custom Network Security Configuration.

Read that again with your own infrastructure in mind. Your production API almost certainly uses a publicly trusted certificate and will be fine. Your internal endpoints may not be: staging, an internal metrics collector, an on-premises enterprise deployment, a partner integration behind a private CA.

I ranked this second rather than fifth because of the failure pattern. It won't break in a normal QA pass against production. It'll break for the one enterprise customer running your app against their own certificate authority, six weeks after launch, and it'll arrive as "the app doesn't work on our network" with no useful diagnostic attached.

What to do: audit every host your app connects to, not just the ones in your API client. Include analytics, crash reporting, feature flags, image CDNs, and anything an SDK you depend on reaches out to. Any host on a private or internal CA needs an explicit opt-out in your Network Security Configuration.

3. Static Final Fields Are Immutable (This Breaks Tests First)

Apps targeting SDK 37 or higher can no longer modify static final fields, which lets the runtime optimise more aggressively.

Almost no application code does this deliberately. But reflection-heavy test frameworks and mocking libraries do it constantly, as do some older dependency injection and serialisation libraries.

So the practical shape of this change is unusual and worth calling out: it is unlikely to break your app and reasonably likely to break your instrumentation tests. You raise targetSdk on a branch, your app runs fine, and your test suite falls over, which is a confusing few hours if you don't know this is a documented change rather than something you did.

Update your test dependencies before you raise targetSdk, not after. Mocking libraries in particular have had time to adapt. Older pinned versions have not. This is a five-minute fix if you know about it in advance and half a day of bisecting if you don't.

A broader point is buried here. Your test infrastructure is subject to platform behaviour changes exactly like your production code, but nobody audits it that way. Test dependencies get pinned once and left for years precisely because they're not user-facing. This change punishes that.

4. Native Library Loading Is Hardened

The Safer Dynamic Code Loading protection introduced in Android 14 for DEX and JAR files now extends to native libraries at SDK 37. All native files loaded using System.load() must be marked read-only, or the system throws UnsatisfiedLinkError.

Most apps don't do this. If you're not calling System.load() with a path you control, you can move on. Note that System.loadLibrary(), which is what the overwhelming majority of apps use for bundled NDK libraries, isn't the concern here.

This bites anything that downloads or extracts a native library at runtime and loads it from a writable location: dynamic feature delivery of native code, some game engines, hot-patching frameworks, and a handful of SDKs that ship native code out-of-band.

Grep for System.load( across your codebase and your dependencies. It's a fast check, and the answer is usually "nothing." If something does turn up, mark the file read-only before loading it.

5. SMS OTP Access Is Delayed by Three Hours

For SMS messages containing a one-time passcode that don't use the WebOTP or SMS Retriever formats, most apps can access the OTP only after three hours have elapsed. This applies to apps targeting API 37 or higher. Certain apps, including the default SMS app, the assistant and connected-device companion apps, are exempt.

A related change applies regardless of target API level. If an app has SMS read permission but isn't the intended recipient as determined by domain verification, a WebOTP-format message is also only accessible after three hours.

Three hours is not a delay. For an OTP, it's a removal.

If you rely on READ_SMS for OTP autofill, this ends that pattern. The migration is to WebOTP or the SMS Retriever API, and the honest warning is that you may not control the timeline. Changing your message format usually means going through your SMS vendor, and in some markets that involves template registration processes measured in weeks, not days. Start that conversation before you start the engineering.

If you serve the same OTP flow on web and Android, move both to WebOTP together so you maintain one message format rather than two.

A Sane Order to Do This In

Pulling the whole thing together, the sequence that minimises pain:

  1. Raise compileSdk first, on its own. You get deprecation warnings and new APIs without opting into any behaviour changes. Ship this, since it's low risk.
  2. Update your test dependencies. Mocking and reflection-based libraries, specifically, before the target bump, so the static-final-fields change doesn't ambush you.
  3. Audit, on a branch, before raising the target. Grep for System.load(. List every host your app and its SDKs connect to and flag anything on a private CA. Identify every local-network code path.
  4. Then raise targetSdk on that branch and run the app on a large-screen emulator, in landscape and in a resizable window. That single run surfaces most of the resizability work.
  5. Test the network changes on a real network, not just an emulator, against production. Certificate transparency and local network permission both fail in conditions your CI doesn't reproduce.
  6. Merge, then check the Play Console for the target-API deadline that actually applies to your app.

My Take

These changes share a family resemblance, and it's worth naming. Resizability, local network access, certificate transparency, SMS OTP: all four remove something apps did because the platform permitted it, not because it was a good idea. Portrait lock, unrestricted LAN scanning, unverified certificates, reading other people's texts. None of those was features. They were affordances that stuck around because withdrawing them breaks things, and Google has decided to take the breakage.

I think that's the right call, and I'd note the direction is consistent across the industry rather than particular to Android. Every platform I've shipped on has spent the last decade closing exactly these doors, usually in the same order.

What annoys me is the testing asymmetry. Google shipped genuinely good tooling for the visible change: DeviceConfigurationOverride, Compose UI Check, screenshot testing across sizes. For the invisible ones, there's essentially nothing. No test rig tells you which of your hosts will fail certificate transparency, no lint check that flags local-network code paths, no assertion you can write that proves an SDK three dependencies deep doesn't call System.load(). You're left grepping and hoping.

That asymmetry is why I'd rank the network changes above the layout change in terms of what actually reaches users as a bug. The layout problem is loud, and loud problems get fixed. The certificate transparency problem is quiet, arrives at one enterprise customer, and takes a week to diagnose because the symptom is "it doesn't work" with no stack trace worth reading.

Practically: audit before you migrate. The instinct with a target bump is to raise the number, run the app, and fix what falls over. That works for the resizability change because the failure is immediate and visible. It does not work for four of the five changes above, because the app will launch fine and fail later, somewhere you weren't looking. An afternoon spent listing hosts, grepping for System.load(, and mapping your local-network code paths is worth more than a week of exploratory testing here.

And the honest one: if your app doesn't cast, doesn't talk to LAN devices, doesn't read SMS, doesn't load native code dynamically, and only connects to publicly trusted hosts, none of this affects you, and you should go and read about resizability instead. The value of a list like this is knowing which items you can dismiss in thirty seconds.

FAQ

Do these changes affect my app if I don't raise targetSdk?
No. All the changes above apply to apps targeting API level 37 or higher, except for the WebOTP domain-verification delay, which applies regardless of target.

Does every app need ACCESS_LOCAL_NETWORK?
Only apps that discover or communicate with devices on the local network. If you can route the flow through a system-mediated picker, either Cast's output switcher or NsdManager's service picker, you skip the permission entirely.

Will certificate transparency break my API calls?
Not if your hosts use publicly-trusted certificates. Connections relying on private or internal certificates may fail unless those domains are opted out via a custom Network Security Configuration.

Why would the static final fields change break my tests but not my app?
Application code rarely modifies static final fields deliberately. Reflection-based mocking and DI libraries do it routinely, so the failure usually appears in your instrumentation suite rather than in the app itself.

Does this affect System.loadLibrary()?
The documented change concerns native files loaded using System.load(), which must be marked read-only. Standard bundled NDK libraries loaded via System.loadLibrary() aren't what this targets.

Can I still use READ_SMS for OTP autofill?
Not usefully at targetSdk 37. For non-WebOTP, non-SMS-Retriever messages, most apps only get access after three hours. Migrate to WebOTP or the SMS Retriever API.