COLONEL MANIC // for publishers
Advisory: Graceful Incompatibility Handling for Android Publishers
Audience: Android app developers and publishers  ·  Date: June 2026
Devices with custom hardware stacks — e.g. the CAT S61 with FLIR thermal camera, running Android 9 — continue to be actively used in professional and industrial settings. Apps that install silently on incompatible devices and then crash or destabilise the system erode user trust and can disable a tool the user depends on for hours. This advisory outlines how to refuse installation cleanly instead.
!
The Problem
what a silent bad install actually does
  • Crash-loops system services — camera, sensor, and media stacks are most vulnerable
  • Android's exponential backoff recovery leaves the device partially non-functional for 2–4 hours
  • The user has no indication the app is the cause
  • Uninstalling after the fact may not immediately resolve a destabilised system_server

A clean refusal at install time costs the user 5 seconds. A silent bad install can cost them half a day.

01
Manifest Declarations
build time — preferred

The most reliable approach. Google Play filters device compatibility using your AndroidManifest.xml before the user ever sees an install button.

Set minSdkVersion honestly

<uses-sdk
    android:minSdkVersion="26"
    android:targetSdkVersion="34" />

If your app genuinely requires API 26+ behaviour, declare it. Do not set a low minSdkVersion to maximise install numbers if the app will not function correctly on those versions.

Declare hardware features you actually require

<!-- Camera2 API -->
<uses-feature android:name="android.hardware.camera2" android:required="true" />

<!-- Standard camera hardware (not custom HAL) -->
<uses-feature android:name="android.hardware.camera" android:required="true" />

<!-- Gyroscope, GPS, NFC — only if truly required -->
<uses-feature android:name="android.hardware.sensor.gyroscope" android:required="true" />
required="true" blocks installation on devices that do not advertise the feature. required="false" (the default if omitted) means your app must handle absence gracefully at runtime. Only set required="true" for features your app cannot meaningfully function without.

Use the Play Console device catalogue

In the Play Console under Release → [Track] → Device catalogue, you can explicitly exclude specific device models — no code required. Use this when you have confirmed incompatibility with a specific SKU (e.g. catmobile_s61) and cannot resolve it at the manifest level.

02
Runtime Checks
first launch — catches what the manifest misses

When manifest filtering is insufficient — for example, when a device advertises a feature via a non-standard HAL that your code cannot actually use — check compatibility at first launch and exit cleanly.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (!isDeviceCompatible()) {
            showIncompatibilityDialog()
            return  // nothing else runs
        }

        setContentView(R.layout.activity_main)
    }

    private fun isDeviceCompatible(): Boolean {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return false

        val cameraManager = getSystemService(CAMERA_SERVICE) as CameraManager
        val hasFullCamera = cameraManager.cameraIdList.any { id ->
            val caps = cameraManager.getCameraCharacteristics(id)
                .get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
            caps == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL ||
            caps == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
        }
        return hasFullCamera
    }

    private fun showIncompatibilityDialog() {
        AlertDialog.Builder(this)
            .setTitle("Device Not Supported")
            .setMessage(
                "This app requires Android 10 or later and a Camera2-compatible camera. " +
                "Your device does not meet these requirements.\n\n" +
                "No changes have been made to your device. " +
                "You can safely uninstall this app."
            )
            .setPositiveButton("Uninstall") { _, _ ->
                val intent = Intent(Intent.ACTION_DELETE).apply {
                    data = Uri.parse("package:$packageName")
                }
                startActivity(intent)
            }
            .setNegativeButton("Close") { _, _ -> finish() }
            .setCancelable(false)
            .show()
    }
}

The dialog must

03
Do Not Touch Hardware You Don't Own
the rule that prevents the multi-hour crash
If your app is not a camera app, do not touch the camera HAL. On devices with non-standard camera stacks — dual HAL configurations, thermal imaging modules, depth sensors — unexpected camera access can destabilise cameraserver system-wide, affecting every other app on the device.
04
Known Problem Devices
blocklist while you investigate
private val knownIncompatibleDevices = setOf(
    "catmobile:s61",  // dual-HAL camera stack, Android 9 max
)

private fun isKnownIncompatibleDevice(): Boolean {
    val fingerprint = "${Build.BRAND}:${Build.MODEL}".lowercase()
    return knownIncompatibleDevices.any { fingerprint.contains(it) }
}
Real-World Contrast
same device, same week, opposite outcomes

✓ State Farm — cleared the bar

An insurance company. Their app correctly identified the CAT S61 as incompatible and declined cleanly. Thirty seconds. No device impact. Compliance culture produced a better outcome than engineering ambition.

✗ Sonos — a masterclass in how not to do it

A hardware company. Their app force-installed and triggered a multi-hour crash cycle by probing sensor APIs it had no business touching. The user's professional-grade tool was unusable for most of a working day.

State Farm, founded 1922, handled Android hardware compatibility better than a consumer electronics company whose entire identity is built on making hardware and software work together. Install numbers are a vanity metric. A user whose device is broken for three hours by your app is not a retained user.
Summary Checklist
all low-to-medium effort
LayerActionEffort
ManifestSet minSdkVersion accuratelyLow
ManifestDeclare uses-feature with correct required valueLow
Play ConsoleExclude confirmed incompatible device models by nameLow
RuntimeCheck compatibility before initialising any servicesMedium
RuntimeShow a clear, actionable incompatibility dialogMedium
Code hygieneNever access hardware speculatively in backgroundOngoing
Crash reportingMonitor by device model and act on patternsOngoing
A user who sees "this app does not support your device" respects your app.
A user whose device goes dark for three hours because your app fought with their camera driver does not come back.