Skip to content
All essays

Mobile architecture

Target SDK 37 changes what a successful network check proves

On Android 17, an app targeting API 37 can reach the cloud while every local socket fails. A seven-scenario test separates app guards from OS enforcement and shows what each result proves.

By Mike Salari. Staff mobile engineer · 500+ technical interviews from the hiring side.

6 min read

When a printer feature stops working after a target SDK bump, I ask a senior Android candidate what they would check first. I listen for one distinction. Did the phone lose its network, or did the app lose permission to reach the local network? Most check the first. Few check the second.

These are different failures. A successful call to a public HTTPS endpoint says nothing about whether the app may talk to a printer, camera or speaker. A failed local connection says nothing about whether the phone has internet.

In a test like this, the public request returns 204 while both local socket probes fail. Switch to the target 36 build on the same Android 17 runtime and the local connections should work. Grant the target 37 build its local-network permission and they should work again. I am a staff mobile engineer with 500+ technical interviews from the hiring side. This is the platform follow-up that separates people who read the changelog from people who tested it.

The two conditions that select the behaviour

Google's local network guide requires ACCESS_LOCAL_NETWORK for direct local traffic on Android 17 when the app targets API 37 or higher. Lower targets get compatibility behaviour, and the guide tells those apps not to request the new permission early. Android 16's opt-in uses a different temporary arrangement.

The two build flavors share one API 37 runtime. Legacy targets 36 and declares INTERNET. Modern targets 37 and adds ACCESS_LOCAL_NETWORK. It also declares BLUETOOTH_CONNECT, only to prepare a test of the NEARBY_DEVICES group grant. Leave Bluetooth out of a product that does not need it.

Kotlin
package dev.salari.localnetwork

import android.content.Context
import android.content.pm.PackageManager
import android.os.Build

object LanCapability {
    const val PERMISSION = "android.permission.ACCESS_LOCAL_NETWORK"

    fun requiresPermission(context: Context): Boolean =
        Build.VERSION.SDK_INT >= 37 && context.applicationInfo.targetSdkVersion >= 37

    fun granted(context: Context): Boolean =
        !requiresPermission(context) ||
            context.checkSelfPermission(PERMISSION) == PackageManager.PERMISSION_GRANTED
}

The guarded button checks the grant right before it connects, and the screen re-reads it on resume. A grant remembered from earlier in the process is not permanent authorisation. The raw diagnostic buttons skip this guard on purpose, so an app-level refusal cannot pass as proof of OS enforcement.

Give the test a known destination and answer

A host endpoint binds to 127.0.0.1 only: TCP 18737 and UDP 18738. The emulator reaches it through its 10.0.2.2 alias. TCP must return fixture-ok and a UDP ping must return fixture-pong. Opening a socket is not success. Receiving the payload is.

The public control makes a real HTTPS request to gstatic's generate_204 and requires HTTP 204. No user data is sent.

The test did not assume 10.0.2.2 counts as restricted local traffic. Paired denied and granted cases show whether it does in your configuration. That is regression evidence for that setup, not proof for every physical LAN. In your own test, use a device you control. Establish a working baseline first. Then repeat the same exchange after grant and after revocation. A timeout with no known-good control leaves too many explanations open.

A network grant does not authenticate a device or authorise a command. A real printer or camera integration still needs its own identity and command security.

Seven scenarios and what each should show

Prepared stateTCP shouldUDP shouldPublic HTTPS
Target 36, fresh installPayload returnedPayload returned204
Target 37, no grantFailsFails204
Target 37, user taps AllowPayload returnedPayload returned204
Target 37, revoked with adbFailsFails204
Target 37, user grants againPayload returnedPayload returned204
Target 37, user taps Don't allowFailsFailsnot checked
Target 37, Bluetooth group pre-grantedPayload returnedPayload returnednot checked

The raw probes must fail in blocked states and return the expected bytes in allowed states. The guarded button separately must show Permission required when access is missing. Revoke with adb pm revoke, then relaunch; regrant through the real system dialog. A test like this does not revoke mid-stream or walk through the Settings app.

In the last row, adb grants BLUETOOTH_CONNECT before the LAN request, and the app can reach the ready state without another Allow dialog. That matches the guide's warning that an existing NEARBY_DEVICES grant can change whether a prompt appears. It is a reason to read permission state from the system, not from what a tester remembers seeing. A real earlier Bluetooth consent followed by an app update is a separate case to reproduce.

Keep capability and failure apart

A product screen should tell the user whether permission is missing, discovery found nothing, or a chosen endpoint could not be reached. They need different next steps. Showing "No devices found" for a denied capability sends people off to restart a router the app was never allowed to query.

Carry the permission state separately from discovery results. After revocation, clear or revalidate saved device selections before you show them as connected. Bound your retries, offer the action the user can take, and keep cloud features working while they do. Google documents different TCP and UDP failure behaviour, so a bare timeout is not a verdict.

One lifecycle detail to watch in UI tests: the permission callback shows "LAN granted" and then onResume replaces it with "LAN ready". The grant is already in the package state, so a test that waits for transient text is fragile. Render the same capability state on both paths and use the socket checks as the oracle. A banner is not evidence that the feature works.

The scoped picker is another integration

Broad LAN access is not the only design. DiscoveryRequest.FLAG_SHOW_PICKER asks the system to show a service picker, and the documented selection path can avoid the broad permission. It is a different capability from scanning or connecting to stored addresses.

The project includes an NsdPicker that builds a DiscoveryRequest for _http._tcp with the picker flag and a ServiceInfoCallback, and unregisters it when the activity is destroyed. A test of the table above covers the broad-permission path only; it does not run a discovery, a user selection or a scoped connection through the picker.

For a printer product, ask whether selecting one advertised printer meets the feature. If it does, build and test that flow. If you need a persistent inventory of devices, write that down before you ask for broad access. Do not replace a user's choice with an old IP address from a cache.

What the table does not cover

Run the rows on an API 37 emulator image and on a stock test device, and compare. An emulator is not a stock device: physical Wi-Fi, IPv6, inbound connections, roaming and a real printer or camera need their own rows. Check the current AGP, Gradle and compile SDK requirements in Google's release notes before you copy any build setup.

Next steps

Before you raise target SDK to 37, list every feature that talks to a device on the local network. Give each its own row in a table like the one above. For the interview version, how you would classify a failed connection and what an emulator result is worth, use the Android preparation route and The Senior Signal.

Mobile System Design Blueprint turns this into decision chapters and worked scenarios, so you can rehearse the choice before the room asks for it.

Related resources

Free resources that put this essay to work: short enough to use this week, and each one works on its own, with or without a book.

Related books

These books take the subject of this essay further, written from the hiring side from 500+ technical interviews. Each book page shows what is inside, who it is for, and a free sample where there is one.

Share this essay